test_issue_search.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import unittest
  2. import pytest
  3. from sentry.api.event_search import (
  4. AggregateFilter,
  5. AggregateKey,
  6. SearchFilter,
  7. SearchKey,
  8. SearchValue,
  9. )
  10. from sentry.api.issue_search import (
  11. convert_actor_or_none_value,
  12. convert_category_value,
  13. convert_first_release_value,
  14. convert_query_values,
  15. convert_release_value,
  16. convert_type_value,
  17. convert_user_value,
  18. parse_search_query,
  19. value_converters,
  20. )
  21. from sentry.exceptions import InvalidSearchQuery
  22. from sentry.models.group import STATUS_QUERY_CHOICES
  23. from sentry.testutils import TestCase
  24. from sentry.testutils.silo import region_silo_test
  25. from sentry.types.issues import GROUP_CATEGORY_TO_TYPES, GroupCategory
  26. class ParseSearchQueryTest(unittest.TestCase):
  27. def test_key_mappings(self):
  28. # Test a couple of keys to ensure things are working as expected
  29. assert parse_search_query("bookmarks:123") == [
  30. SearchFilter(
  31. key=SearchKey(name="bookmarked_by"), operator="=", value=SearchValue("123")
  32. )
  33. ]
  34. assert parse_search_query("first-release:123") == [
  35. SearchFilter(
  36. key=SearchKey(name="first_release"), operator="=", value=SearchValue("123")
  37. )
  38. ]
  39. assert parse_search_query("first-release:123 non_mapped:456") == [
  40. SearchFilter(
  41. key=SearchKey(name="first_release"), operator="=", value=SearchValue("123")
  42. ),
  43. SearchFilter(key=SearchKey(name="non_mapped"), operator="=", value=SearchValue("456")),
  44. ]
  45. def test_is_query_unassigned(self):
  46. assert parse_search_query("is:unassigned") == [
  47. SearchFilter(key=SearchKey(name="unassigned"), operator="=", value=SearchValue(True))
  48. ]
  49. assert parse_search_query("is:assigned") == [
  50. SearchFilter(key=SearchKey(name="unassigned"), operator="=", value=SearchValue(False))
  51. ]
  52. assert parse_search_query("!is:unassigned") == [
  53. SearchFilter(key=SearchKey(name="unassigned"), operator="!=", value=SearchValue(True))
  54. ]
  55. assert parse_search_query("!is:assigned") == [
  56. SearchFilter(key=SearchKey(name="unassigned"), operator="!=", value=SearchValue(False))
  57. ]
  58. def test_is_query_linked(self):
  59. assert parse_search_query("is:linked") == [
  60. SearchFilter(key=SearchKey(name="linked"), operator="=", value=SearchValue(True))
  61. ]
  62. assert parse_search_query("is:unlinked") == [
  63. SearchFilter(key=SearchKey(name="linked"), operator="=", value=SearchValue(False))
  64. ]
  65. assert parse_search_query("!is:linked") == [
  66. SearchFilter(key=SearchKey(name="linked"), operator="!=", value=SearchValue(True))
  67. ]
  68. assert parse_search_query("!is:unlinked") == [
  69. SearchFilter(key=SearchKey(name="linked"), operator="!=", value=SearchValue(False))
  70. ]
  71. def test_is_query_status(self):
  72. for status_string, status_val in STATUS_QUERY_CHOICES.items():
  73. assert parse_search_query("is:%s" % status_string) == [
  74. SearchFilter(
  75. key=SearchKey(name="status"), operator="=", value=SearchValue(status_val)
  76. )
  77. ]
  78. assert parse_search_query("!is:%s" % status_string) == [
  79. SearchFilter(
  80. key=SearchKey(name="status"), operator="!=", value=SearchValue(status_val)
  81. )
  82. ]
  83. def test_is_query_invalid(self):
  84. with pytest.raises(InvalidSearchQuery) as excinfo:
  85. parse_search_query("is:wrong")
  86. assert str(excinfo.value).startswith('Invalid value for "is" search, valid values are')
  87. def test_is_query_inbox(self):
  88. assert parse_search_query("is:for_review") == [
  89. SearchFilter(key=SearchKey(name="for_review"), operator="=", value=SearchValue(True))
  90. ]
  91. def test_numeric_filter(self):
  92. # test numeric format
  93. assert parse_search_query("times_seen:500") == [
  94. SearchFilter(
  95. key=SearchKey(name="times_seen"), operator="=", value=SearchValue(raw_value=500)
  96. )
  97. ]
  98. assert parse_search_query("times_seen:>500") == [
  99. SearchFilter(
  100. key=SearchKey(name="times_seen"), operator=">", value=SearchValue(raw_value=500)
  101. )
  102. ]
  103. assert parse_search_query("times_seen:<500") == [
  104. SearchFilter(
  105. key=SearchKey(name="times_seen"), operator="<", value=SearchValue(raw_value=500)
  106. )
  107. ]
  108. invalid_queries = [
  109. "times_seen:<hello",
  110. "times_seen:<512.1.0",
  111. "times_seen:2018-01-01",
  112. "times_seen:+7d",
  113. "times_seen:>2018-01-01",
  114. 'times_seen:"<10"',
  115. ]
  116. for invalid_query in invalid_queries:
  117. with pytest.raises(InvalidSearchQuery, match="Invalid number"):
  118. parse_search_query(invalid_query)
  119. def test_boolean_operators_not_allowed(self):
  120. invalid_queries = [
  121. "user.email:foo@example.com OR user.email:bar@example.com",
  122. "user.email:foo@example.com AND user.email:bar@example.com",
  123. "user.email:foo@example.com OR user.email:bar@example.com OR user.email:foobar@example.com",
  124. "user.email:foo@example.com AND user.email:bar@example.com AND user.email:foobar@example.com",
  125. ]
  126. for invalid_query in invalid_queries:
  127. with pytest.raises(
  128. InvalidSearchQuery,
  129. match='Boolean statements containing "OR" or "AND" are not supported in this search',
  130. ):
  131. parse_search_query(invalid_query)
  132. def test_parens_in_query(self):
  133. assert parse_search_query(
  134. "TypeError Anonymous function(app/javascript/utils/transform-object-keys)"
  135. ) == [
  136. SearchFilter(
  137. key=SearchKey(name="message"),
  138. operator="=",
  139. value=SearchValue(
  140. raw_value="TypeError Anonymous function(app/javascript/utils/transform-object-keys)"
  141. ),
  142. ),
  143. ]
  144. @region_silo_test(stable=True)
  145. class ConvertJavaScriptConsoleTagTest(TestCase):
  146. def test_valid(self):
  147. filters = [SearchFilter(SearchKey("empty_stacktrace.js_console"), "=", SearchValue(True))]
  148. with self.feature("organizations:javascript-console-error-tag"):
  149. result = convert_query_values(filters, [self.project], self.user, None)
  150. assert result[0].value.raw_value is True
  151. def test_invalid(self):
  152. filters = [SearchFilter(SearchKey("empty_stacktrace.js_console"), "=", SearchValue(True))]
  153. with self.feature({"organizations:javascript-console-error-tag": False}) and pytest.raises(
  154. InvalidSearchQuery,
  155. match="The empty_stacktrace.js_console filter is not supported for this organization",
  156. ):
  157. convert_query_values(filters, [self.project], self.user, None)
  158. @region_silo_test(stable=True)
  159. class ConvertQueryValuesTest(TestCase):
  160. def test_valid_converter(self):
  161. filters = [SearchFilter(SearchKey("assigned_to"), "=", SearchValue("me"))]
  162. expected = value_converters["assigned_to"](
  163. [filters[0].value.raw_value], [self.project], self.user, None
  164. )
  165. filters = convert_query_values(filters, [self.project], self.user, None)
  166. assert filters[0].value.raw_value == expected
  167. def test_no_converter(self):
  168. search_val = SearchValue("me")
  169. filters = [SearchFilter(SearchKey("something"), "=", search_val)]
  170. filters = convert_query_values(filters, [self.project], self.user, None)
  171. assert filters[0].value.raw_value == search_val.raw_value
  172. @region_silo_test(stable=True)
  173. class ConvertStatusValueTest(TestCase):
  174. def test_valid(self):
  175. for status_string, status_val in STATUS_QUERY_CHOICES.items():
  176. filters = [SearchFilter(SearchKey("status"), "=", SearchValue([status_string]))]
  177. result = convert_query_values(filters, [self.project], self.user, None)
  178. assert result[0].value.raw_value == [status_val]
  179. filters = [SearchFilter(SearchKey("status"), "=", SearchValue([status_val]))]
  180. result = convert_query_values(filters, [self.project], self.user, None)
  181. assert result[0].value.raw_value == [status_val]
  182. def test_invalid(self):
  183. filters = [SearchFilter(SearchKey("status"), "=", SearchValue("wrong"))]
  184. with pytest.raises(InvalidSearchQuery, match="invalid status value"):
  185. convert_query_values(filters, [self.project], self.user, None)
  186. filters = [AggregateFilter(AggregateKey("count_unique(user)"), ">", SearchValue("1"))]
  187. with pytest.raises(
  188. InvalidSearchQuery,
  189. match=r"Aggregate filters \(count_unique\(user\)\) are not supported in issue searches.",
  190. ):
  191. convert_query_values(filters, [self.project], self.user, None)
  192. @region_silo_test(stable=True)
  193. class ConvertActorOrNoneValueTest(TestCase):
  194. def test_user(self):
  195. assert convert_actor_or_none_value(
  196. ["me"], [self.project], self.user, None
  197. ) == convert_user_value(["me"], [self.project], self.user, None)
  198. def test_none(self):
  199. assert convert_actor_or_none_value(["none"], [self.project], self.user, None) == [None]
  200. def test_team(self):
  201. assert convert_actor_or_none_value(
  202. [f"#{self.team.slug}"], [self.project], self.user, None
  203. ) == [self.team]
  204. def test_invalid_team(self):
  205. assert (
  206. convert_actor_or_none_value(["#never_upgrade"], [self.project], self.user, None)[0].id
  207. == 0
  208. )
  209. @region_silo_test
  210. class ConvertUserValueTest(TestCase):
  211. def test_me(self):
  212. assert convert_user_value(["me"], [self.project], self.user, None) == [self.user]
  213. def test_specified_user(self):
  214. user = self.create_user()
  215. assert convert_user_value([user.username], [self.project], self.user, None) == [user]
  216. def test_invalid_user(self):
  217. assert convert_user_value(["fake-user"], [], None, None)[0].id == 0
  218. @region_silo_test(stable=True)
  219. class ConvertReleaseValueTest(TestCase):
  220. def test(self):
  221. assert convert_release_value(["123"], [self.project], self.user, None) == "123"
  222. def test_latest(self):
  223. release = self.create_release(self.project)
  224. assert convert_release_value(["latest"], [self.project], self.user, None) == release.version
  225. assert convert_release_value(["14.*"], [self.project], self.user, None) == "14.*"
  226. @region_silo_test(stable=True)
  227. class ConvertFirstReleaseValueTest(TestCase):
  228. def test(self):
  229. assert convert_first_release_value(["123"], [self.project], self.user, None) == ["123"]
  230. def test_latest(self):
  231. release = self.create_release(self.project)
  232. assert convert_first_release_value(["latest"], [self.project], self.user, None) == [
  233. release.version
  234. ]
  235. assert convert_first_release_value(["14.*"], [self.project], self.user, None) == ["14.*"]
  236. @region_silo_test(stable=True)
  237. class ConvertCategoryValueTest(TestCase):
  238. def test(self):
  239. with self.feature("organizations:performance-issues"):
  240. assert set(convert_category_value(["error"], [self.project], self.user, None)) == {
  241. gt.value for gt in GROUP_CATEGORY_TO_TYPES[GroupCategory.ERROR]
  242. }
  243. assert set(
  244. convert_category_value(["performance"], [self.project], self.user, None)
  245. ) == {gt.value for gt in GROUP_CATEGORY_TO_TYPES[GroupCategory.PERFORMANCE]}
  246. assert set(
  247. convert_category_value(["error", "performance"], [self.project], self.user, None)
  248. ) == {
  249. gt.value
  250. for gt in GROUP_CATEGORY_TO_TYPES[GroupCategory.ERROR]
  251. + GROUP_CATEGORY_TO_TYPES[GroupCategory.PERFORMANCE]
  252. }
  253. with pytest.raises(InvalidSearchQuery):
  254. convert_category_value(["hellboy"], [self.project], self.user, None)
  255. @region_silo_test(stable=True)
  256. class ConvertTypeValueTest(TestCase):
  257. def test(self):
  258. with self.feature("organizations:performance-issues"):
  259. assert convert_type_value(["error"], [self.project], self.user, None) == [1]
  260. assert convert_type_value(
  261. ["performance_n_plus_one_db_queries"], [self.project], self.user, None
  262. ) == [1006]
  263. assert convert_type_value(
  264. ["performance_slow_span"], [self.project], self.user, None
  265. ) == [1001]
  266. assert convert_type_value(
  267. ["error", "performance_n_plus_one_db_queries"], [self.project], self.user, None
  268. ) == [1, 1006]
  269. with pytest.raises(InvalidSearchQuery):
  270. convert_type_value(["hellboy"], [self.project], self.user, None)