test_conditions.py 12 KB

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