test_feature.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. from dataclasses import dataclass
  2. from datetime import datetime, timezone
  3. import orjson
  4. import pytest
  5. import yaml
  6. from flagpole import ContextBuilder, EvaluationContext, Feature, InvalidFeatureFlagConfiguration
  7. from flagpole.conditions import ConditionOperatorKind
  8. @dataclass
  9. class SimpleTestContextData:
  10. pass
  11. class TestParseFeatureConfig:
  12. def get_is_true_context_builder(self, is_true_value: bool):
  13. return ContextBuilder().add_context_transformer(lambda _data: dict(is_true=is_true_value))
  14. def test_feature_with_empty_segments(self):
  15. feature = Feature.from_feature_config_json(
  16. "foobar",
  17. """
  18. {
  19. "created_at": "2023-10-12T00:00:00.000Z",
  20. "owner": "test-owner",
  21. "segments": []
  22. }
  23. """,
  24. )
  25. assert feature.name == "foobar"
  26. assert feature.created_at == datetime(2023, 10, 12, tzinfo=timezone.utc)
  27. assert feature.owner == "test-owner"
  28. assert feature.segments == []
  29. assert not feature.match(EvaluationContext(dict()))
  30. def test_feature_with_rollout_zero(self):
  31. feature = Feature.from_feature_config_json(
  32. "foobar",
  33. """
  34. {
  35. "created_at": "2023-10-12T00:00:00.000Z",
  36. "owner": "test-owner",
  37. "segments": [
  38. {
  39. "name": "exclude",
  40. "rollout": 0,
  41. "conditions": [
  42. {
  43. "property": "user_email",
  44. "operator": "equals",
  45. "value": "nope@example.com"
  46. }
  47. ]
  48. },
  49. {
  50. "name": "friends",
  51. "rollout": 100,
  52. "conditions": [
  53. {
  54. "property": "organization_slug",
  55. "operator": "in",
  56. "value": ["acme", "sentry"]
  57. }
  58. ]
  59. }
  60. ]
  61. }
  62. """,
  63. )
  64. exclude_user = {"user_email": "nope@example.com", "organization_slug": "acme"}
  65. assert not feature.match(EvaluationContext(exclude_user))
  66. match_user = {"user_email": "yes@example.com", "organization_slug": "acme"}
  67. assert feature.match(EvaluationContext(match_user))
  68. def test_all_conditions_in_segment(self):
  69. feature = Feature.from_feature_config_json(
  70. "foobar",
  71. """
  72. {
  73. "created_at": "2023-10-12T00:00:00.000Z",
  74. "owner": "test-owner",
  75. "segments": [
  76. {
  77. "name": "multiple conditions",
  78. "rollout": 100,
  79. "conditions": [
  80. {
  81. "property": "user_email",
  82. "operator": "equals",
  83. "value": "yes@example.com"
  84. },
  85. {
  86. "property": "organization_slug",
  87. "operator": "in",
  88. "value": ["acme", "sentry"]
  89. }
  90. ]
  91. }
  92. ]
  93. }
  94. """,
  95. )
  96. exclude_user = {"user_email": "yes@example.com"}
  97. assert not feature.match(EvaluationContext(exclude_user))
  98. match_user = {"user_email": "yes@example.com", "organization_slug": "acme"}
  99. assert feature.match(EvaluationContext(match_user))
  100. def test_valid_with_all_nesting(self):
  101. feature = Feature.from_feature_config_json(
  102. "foobar",
  103. """
  104. {
  105. "created_at": "2023-10-12T00:00:00.000Z",
  106. "owner": "test-owner",
  107. "segments": [{
  108. "name": "segment1",
  109. "rollout": 100,
  110. "conditions": [{
  111. "property": "test_property",
  112. "operator": "in",
  113. "value": ["foobar"]
  114. }]
  115. }]
  116. }
  117. """,
  118. )
  119. assert feature.name == "foobar"
  120. assert len(feature.segments) == 1
  121. assert feature.segments[0].name == "segment1"
  122. assert feature.segments[0].rollout == 100
  123. assert len(feature.segments[0].conditions) == 1
  124. condition = feature.segments[0].conditions[0]
  125. assert condition.property == "test_property"
  126. assert condition.operator
  127. assert condition.operator == ConditionOperatorKind.IN
  128. assert condition.value == ["foobar"]
  129. assert feature.match(EvaluationContext(dict(test_property="foobar")))
  130. assert not feature.match(EvaluationContext(dict(test_property="barfoo")))
  131. def test_invalid_json(self):
  132. with pytest.raises(InvalidFeatureFlagConfiguration):
  133. Feature.from_feature_config_json("foobar", "{")
  134. def test_empty_string_name(self):
  135. with pytest.raises(InvalidFeatureFlagConfiguration) as exception:
  136. Feature.from_feature_config_json("", '{"segments":[]}')
  137. assert "Provided JSON is not a valid feature" in str(exception)
  138. def test_missing_segments(self):
  139. with pytest.raises(InvalidFeatureFlagConfiguration) as exception:
  140. Feature.from_feature_config_json("foo", "{}")
  141. assert "Provided JSON is not a valid feature" in str(exception)
  142. def test_enabled_feature(self):
  143. feature = Feature.from_feature_config_json(
  144. "foo",
  145. """
  146. {
  147. "owner": "test-user",
  148. "created_at": "2023-10-12T00:00:00.000Z",
  149. "segments": [{
  150. "name": "always_pass_segment",
  151. "rollout": 100,
  152. "conditions": [{
  153. "name": "Always true",
  154. "property": "is_true",
  155. "operator": "equals",
  156. "value": true
  157. }]
  158. }]
  159. }
  160. """,
  161. )
  162. context_builder = self.get_is_true_context_builder(is_true_value=True)
  163. assert feature.match(context_builder.build(SimpleTestContextData()))
  164. def test_disabled_feature(self):
  165. feature = Feature.from_feature_config_json(
  166. "foo",
  167. """
  168. {
  169. "owner": "test-user",
  170. "enabled": false,
  171. "created_at": "2023-12-12T00:00:00.000Z",
  172. "segments": [{
  173. "name": "always_pass_segment",
  174. "rollout": 100,
  175. "conditions": [{
  176. "name": "Always true",
  177. "property": "is_true",
  178. "operator": "equals",
  179. "value": true
  180. }]
  181. }]
  182. }
  183. """,
  184. )
  185. context_builder = self.get_is_true_context_builder(is_true_value=True)
  186. assert not feature.match(context_builder.build(SimpleTestContextData()))
  187. def test_dump_yaml(self):
  188. feature = Feature.from_feature_config_json(
  189. "foo",
  190. """
  191. {
  192. "owner": "test-user",
  193. "created_at": "2023-12-12T00:00:00.000Z",
  194. "segments": [{
  195. "name": "always_pass_segment",
  196. "rollout": 100,
  197. "conditions": [{
  198. "name": "Always true",
  199. "property": "is_true",
  200. "operator": "equals",
  201. "value": true
  202. }]
  203. }]
  204. }
  205. """,
  206. )
  207. parsed_json = orjson.loads(feature.json())
  208. parsed_yaml = dict(yaml.safe_load(feature.to_yaml_str()))
  209. assert "foo" in parsed_yaml
  210. parsed_json.pop("name")
  211. assert parsed_yaml["foo"] == parsed_json
  212. features_from_yaml = Feature.from_bulk_yaml(feature.to_yaml_str())
  213. assert features_from_yaml == [feature]