test_conditions.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. from typing import Any
  2. import orjson
  3. import pytest
  4. from pydantic import ValidationError
  5. from flagpole import EvaluationContext
  6. from flagpole.conditions import (
  7. ConditionBase,
  8. ConditionTypeMismatchException,
  9. ContainsCondition,
  10. EqualsCondition,
  11. InCondition,
  12. NotContainsCondition,
  13. NotEqualsCondition,
  14. NotInCondition,
  15. create_case_insensitive_set_from_list,
  16. )
  17. class TestCreateCaseInsensitiveSetFromList:
  18. def test_empty_set(self):
  19. assert create_case_insensitive_set_from_list([]) == set()
  20. def test_returns_int_set(self):
  21. assert create_case_insensitive_set_from_list([1, 2, 3]) == {1, 2, 3}
  22. def test_returns_float_set(self):
  23. assert create_case_insensitive_set_from_list([1.1, 2.2, 3.3]) == {1.1, 2.2, 3.3}
  24. def test_returns_lowercase_string_set(self):
  25. assert create_case_insensitive_set_from_list(["AbC", "DEF"]) == {"abc", "def"}
  26. def assert_valid_types(condition: type[ConditionBase], expected_types: list[Any]):
  27. for value in expected_types:
  28. condition_dict = dict(property="test", value=value)
  29. json_condition = orjson.dumps(condition_dict)
  30. try:
  31. parsed_condition = condition.parse_raw(json_condition)
  32. except ValidationError as exc:
  33. raise AssertionError(
  34. f"Expected value `{value}` to be a valid value for condition '{condition}'"
  35. ) from exc
  36. assert parsed_condition.value == value
  37. def assert_invalid_types(condition: type[ConditionBase], invalid_types: list[Any]):
  38. for value in invalid_types:
  39. json_dict = dict(value=value)
  40. condition_json = orjson.dumps(json_dict)
  41. try:
  42. condition.parse_raw(condition_json)
  43. except ValidationError:
  44. continue
  45. raise AssertionError(
  46. f"Expected validation error for value: `{value}` for condition `{condition}`"
  47. )
  48. class TestInConditions:
  49. def test_invalid_values(self):
  50. with pytest.raises(ValidationError):
  51. InCondition(property="foo", value="bar")
  52. with pytest.raises(ValidationError):
  53. InCondition(property="foo", value=1234)
  54. def test_is_in(self):
  55. values = ["bar", "baz"]
  56. condition = InCondition(property="foo", value=values)
  57. assert condition.match(context=EvaluationContext({"foo": "bar"}), segment_name="test")
  58. not_condition = NotInCondition(property="foo", value=values)
  59. assert not not_condition.match(
  60. context=EvaluationContext({"foo": "bar"}), segment_name="test"
  61. )
  62. int_values = [1, 2]
  63. condition = InCondition(property="foo", value=int_values)
  64. # Validation check to ensure no type coercion occurs
  65. assert condition.value == int_values
  66. assert condition.match(context=EvaluationContext({"foo": 2}), segment_name="test")
  67. assert not condition.match(context=EvaluationContext({"foo": 3}), segment_name="test")
  68. def test_is_in_numeric_string(self):
  69. values = ["123", "456"]
  70. condition = InCondition(property="foo", value=values)
  71. assert condition.value == values
  72. assert not condition.match(context=EvaluationContext({"foo": 123}), segment_name="test")
  73. assert condition.match(context=EvaluationContext({"foo": "123"}), segment_name="test")
  74. def test_is_not_in(self):
  75. values = ["bar", "baz"]
  76. condition = InCondition(property="foo", value=values)
  77. assert not condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  78. not_condition = NotInCondition(property="foo", value=values)
  79. assert not_condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  80. def test_is_in_case_insensitivity(self):
  81. values = ["bAr", "baz"]
  82. condition = InCondition(property="foo", value=values)
  83. assert condition.match(context=EvaluationContext({"foo": "BaR"}), segment_name="test")
  84. not_condition = NotInCondition(property="foo", value=values)
  85. assert not not_condition.match(
  86. context=EvaluationContext({"foo": "BaR"}), segment_name="test"
  87. )
  88. def test_invalid_property_value(self):
  89. values = ["bar", "baz"]
  90. condition = InCondition(property="foo", value=values)
  91. with pytest.raises(ConditionTypeMismatchException):
  92. condition.match(context=EvaluationContext({"foo": []}), segment_name="test")
  93. not_condition = NotInCondition(property="foo", value=values)
  94. with pytest.raises(ConditionTypeMismatchException):
  95. not_condition.match(context=EvaluationContext({"foo": []}), segment_name="test")
  96. def test_valid_json_and_reparse(self):
  97. values = [["foo", "bar"], [1, 2], [1.1, 2.2], []]
  98. assert_valid_types(condition=InCondition, expected_types=values)
  99. assert_valid_types(condition=NotInCondition, expected_types=values)
  100. def test_invalid_value_type_parsing(self):
  101. values = ["abc", 1, 2.2, True, None, ["a", 1], [True], [[]], [1, 2.2], [1.1, "2.2"]]
  102. assert_invalid_types(condition=InCondition, invalid_types=values)
  103. assert_invalid_types(condition=NotInCondition, invalid_types=values)
  104. def test_missing_context_property(self):
  105. values = ["bar", "baz"]
  106. in_condition = InCondition(property="foo", value=values)
  107. assert not in_condition.match(
  108. context=EvaluationContext({"bar": "bar"}), segment_name="test"
  109. )
  110. not_on_condition = NotInCondition(property="foo", value=values)
  111. assert not_on_condition.match(
  112. context=EvaluationContext({"bar": "bar"}), segment_name="test"
  113. )
  114. class TestContainsConditions:
  115. def test_does_contain(self):
  116. condition = ContainsCondition(property="foo", value="bar")
  117. assert condition.match(
  118. context=EvaluationContext({"foo": ["foo", "bar"]}), segment_name="test"
  119. )
  120. not_condition = NotContainsCondition(property="foo", value="bar")
  121. assert not not_condition.match(
  122. context=EvaluationContext({"foo": ["foo", "bar"]}), segment_name="test"
  123. )
  124. condition = ContainsCondition(property="foo", value=1)
  125. assert condition.match(context=EvaluationContext({"foo": [1, 2]}), segment_name="test")
  126. assert not condition.match(context=EvaluationContext({"foo": [3, 4]}), segment_name="test")
  127. def test_does_not_contain(self):
  128. values = "baz"
  129. condition = ContainsCondition(property="foo", value=values)
  130. assert not condition.match(
  131. context=EvaluationContext({"foo": ["foo", "bar"]}), segment_name="test"
  132. )
  133. not_condition = NotContainsCondition(property="foo", value=values)
  134. assert not_condition.match(
  135. context=EvaluationContext({"foo": ["foo", "bar"]}), segment_name="test"
  136. )
  137. def test_invalid_property_provided(self):
  138. values = "baz"
  139. with pytest.raises(ConditionTypeMismatchException):
  140. condition = ContainsCondition(property="foo", value=values)
  141. assert not condition.match(
  142. context=EvaluationContext({"foo": "oops"}), segment_name="test"
  143. )
  144. with pytest.raises(ConditionTypeMismatchException):
  145. not_condition = NotContainsCondition(property="foo", value=values)
  146. assert not_condition.match(
  147. context=EvaluationContext({"foo": "oops"}), segment_name="test"
  148. )
  149. def test_valid_json_parsing_with_types(self):
  150. values = [1, 2.2, "abc"]
  151. assert_valid_types(condition=ContainsCondition, expected_types=values)
  152. assert_valid_types(condition=NotContainsCondition, expected_types=values)
  153. def test_invalid_value_type_parsing(self):
  154. values: list[Any] = [
  155. None,
  156. [],
  157. dict(foo="bar"),
  158. [[]],
  159. ]
  160. assert_invalid_types(condition=ContainsCondition, invalid_types=values)
  161. assert_invalid_types(condition=NotContainsCondition, invalid_types=values)
  162. def test_missing_context_property(self):
  163. condition = ContainsCondition(property="foo", value="bar")
  164. with pytest.raises(ConditionTypeMismatchException):
  165. condition.match(context=EvaluationContext({"bar": ["foo", "bar"]}), segment_name="test")
  166. not_condition = NotContainsCondition(property="foo", value="bar")
  167. with pytest.raises(ConditionTypeMismatchException):
  168. not_condition.match(
  169. context=EvaluationContext({"bar": ["foo", "bar"]}), segment_name="test"
  170. )
  171. class TestEqualsConditions:
  172. def test_is_equal_string(self):
  173. value = "foo"
  174. condition = EqualsCondition(property="foo", value=value)
  175. assert condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  176. not_condition = NotEqualsCondition(property="foo", value=value)
  177. assert not not_condition.match(
  178. context=EvaluationContext({"foo": "foo"}), segment_name="test"
  179. )
  180. def test_is_not_equals(self):
  181. values = "bar"
  182. condition = EqualsCondition(property="foo", value=values)
  183. assert not condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  184. not_condition = NotEqualsCondition(property="foo", value=values)
  185. assert not_condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  186. def test_is_equal_case_insensitive(self):
  187. values = "bAr"
  188. condition = EqualsCondition(property="foo", value=values)
  189. assert condition.match(context=EvaluationContext({"foo": "BaR"}), segment_name="test")
  190. not_condition = NotEqualsCondition(property="foo", value=values)
  191. assert not not_condition.match(
  192. context=EvaluationContext({"foo": "BaR"}), segment_name="test"
  193. )
  194. def test_equality_type_mismatch_strings(self):
  195. values = ["foo", "bar"]
  196. condition = EqualsCondition(property="foo", value=values)
  197. with pytest.raises(ConditionTypeMismatchException):
  198. condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  199. not_condition = NotEqualsCondition(property="foo", value=values)
  200. with pytest.raises(ConditionTypeMismatchException):
  201. not_condition.match(context=EvaluationContext({"foo": "foo"}), segment_name="test")
  202. def test_valid_json_parsing_with_types(self):
  203. values = [1, 2.2, "abc", True, False, [], ["foo"], [1], [1.1]]
  204. assert_valid_types(condition=EqualsCondition, expected_types=values)
  205. assert_valid_types(condition=NotEqualsCondition, expected_types=values)
  206. def test_invalid_value_type_parsing(self):
  207. values = [None, dict(foo="bar")]
  208. assert_invalid_types(condition=EqualsCondition, invalid_types=values)
  209. assert_invalid_types(condition=NotEqualsCondition, invalid_types=values)
  210. def test_with_missing_context_property(self):
  211. value = "foo"
  212. condition = EqualsCondition(property="foo", value=value, strict_validation=True)
  213. with pytest.raises(ConditionTypeMismatchException):
  214. condition.match(context=EvaluationContext({"bar": value}), segment_name="test")
  215. not_condition = NotEqualsCondition(property="foo", value=value, strict_validation=True)
  216. with pytest.raises(ConditionTypeMismatchException):
  217. not_condition.match(context=EvaluationContext({"bar": value}), segment_name="test")
  218. # Test non-strict validation for both conditions
  219. condition = EqualsCondition(property="foo", value=value)
  220. assert (
  221. condition.match(context=EvaluationContext({"bar": value}), segment_name="test") is False
  222. )
  223. not_condition = NotEqualsCondition(property="foo", value=value)
  224. assert (
  225. not_condition.match(context=EvaluationContext({"bar": value}), segment_name="test")
  226. is True
  227. )