tests.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. from timeit import default_timer as timer
  2. from django.shortcuts import reverse
  3. from model_bakery import baker
  4. from glitchtip.test_utils.test_case import GlitchTipTestCase
  5. from issues.models import Issue, EventStatus
  6. from ..tasks import update_search_index_all_issues
  7. class EventTestCase(GlitchTipTestCase):
  8. def setUp(self):
  9. self.create_user_and_project()
  10. self.url = reverse(
  11. "project-events-list",
  12. kwargs={
  13. "project_pk": f"{self.project.organization.slug}/{self.project.slug}"
  14. },
  15. )
  16. def test_project_events_list(self):
  17. event = baker.make("events.Event", issue__project=self.project)
  18. baker.make("events.Event", issue__project=self.project, _quantity=3)
  19. not_my_event = baker.make("events.Event")
  20. with self.assertNumQueries(4):
  21. res = self.client.get(self.url)
  22. self.assertContains(res, event.pk.hex)
  23. self.assertNotContains(res, not_my_event.pk.hex)
  24. def test_events_latest(self):
  25. """
  26. Should show more recent event with previousEventID of previous/first event
  27. """
  28. event = baker.make("events.Event", issue__project=self.project)
  29. event2 = baker.make("events.Event", issue=event.issue)
  30. url = f"/api/0/issues/{event.issue.id}/events/latest/"
  31. res = self.client.get(url)
  32. self.assertContains(res, event2.pk.hex)
  33. self.assertEqual(res.data["previousEventID"], event.pk.hex)
  34. self.assertEqual(res.data["nextEventID"], None)
  35. def test_next_prev_event(self):
  36. """ Get next and previous event IDs that belong to same issue """
  37. issue1 = baker.make("issues.Issue", project=self.project)
  38. issue2 = baker.make("issues.Issue", project=self.project)
  39. baker.make("events.Event")
  40. issue1_event1 = baker.make("events.Event", issue=issue1)
  41. issue2_event1 = baker.make("events.Event", issue=issue2)
  42. issue1_event2 = baker.make("events.Event", issue=issue1)
  43. url = reverse("issue-events-latest", args=[issue1.id])
  44. res = self.client.get(url)
  45. self.assertContains(res, issue1_event2.pk.hex)
  46. self.assertEqual(res.data["previousEventID"], issue1_event1.pk.hex)
  47. def test_entries_emtpy(self):
  48. """ A minimal or incomplete data set should result in an empty entries array """
  49. data = {
  50. "sdk": {
  51. "name": "sentry",
  52. "version": "5",
  53. "packages": [],
  54. "integrations": [],
  55. },
  56. "type": "error",
  57. "title": "<unknown>",
  58. "culprit": "",
  59. "request": {
  60. "url": "http://localhost",
  61. "headers": [],
  62. "inferred_content_type": None,
  63. },
  64. "contexts": None,
  65. "metadata": {"value": "Non-Error exception"},
  66. "packages": None,
  67. "platform": "javascript",
  68. "exception": {
  69. "values": [
  70. {
  71. "type": "Error",
  72. "value": "Non-Error exception",
  73. "mechanism": {
  74. "data": {"function": "<anonymous>"},
  75. "type": "instrument",
  76. "handled": True,
  77. },
  78. }
  79. ]
  80. },
  81. }
  82. event = baker.make("events.Event", issue__project=self.project, data=data)
  83. res = self.client.get(self.url)
  84. self.assertTrue("entries" in res.data[0])
  85. def test_event_json(self):
  86. event = baker.make("events.Event", issue__project=self.project)
  87. url = reverse(
  88. "event_json",
  89. kwargs={
  90. "org": self.organization.slug,
  91. "issue": event.issue.id,
  92. "event": event.event_id_hex,
  93. },
  94. )
  95. res = self.client.get(url)
  96. self.assertContains(res, event.event_id_hex)
  97. class IssuesAPITestCase(GlitchTipTestCase):
  98. def setUp(self):
  99. self.create_user_and_project()
  100. self.url = reverse("issue-list")
  101. def test_issue_list(self):
  102. issue = baker.make("issues.Issue", project=self.project)
  103. not_my_issue = baker.make("issues.Issue")
  104. res = self.client.get(self.url)
  105. self.assertContains(res, issue.title)
  106. self.assertNotContains(res, not_my_issue.title)
  107. self.assertEqual(res.get("X-Hits"), "1")
  108. def test_no_duplicate_issues(self):
  109. """
  110. Addresses https://gitlab.com/glitchtip/glitchtip-backend/-/issues/109
  111. Ensure issues can be filtered by org membership but not duplicated
  112. """
  113. baker.make("issues.Issue", project=self.project)
  114. team2 = baker.make("teams.Team", organization=self.organization)
  115. team2.members.add(self.org_user)
  116. self.project.team_set.add(team2)
  117. res = self.client.get(self.url)
  118. self.assertEqual(len(res.data), 1)
  119. team2.delete()
  120. self.team.delete()
  121. res = self.client.get(self.url)
  122. self.assertEqual(len(res.data), 1)
  123. self.org_user.delete()
  124. res = self.client.get(self.url)
  125. self.assertEqual(len(res.data), 0)
  126. def test_issue_retrieve(self):
  127. issue = baker.make("issues.Issue", project=self.project)
  128. not_my_issue = baker.make("issues.Issue")
  129. url = reverse("issue-detail", args=[issue.id])
  130. res = self.client.get(url)
  131. self.assertContains(res, issue.title)
  132. url = reverse("issue-detail", args=[not_my_issue.id])
  133. res = self.client.get(url)
  134. self.assertEqual(res.status_code, 404)
  135. def test_issue_last_seen(self):
  136. issue = baker.make("issues.Issue", project=self.project)
  137. events = baker.make("events.Event", issue=issue, _quantity=2)
  138. res = self.client.get(self.url)
  139. self.assertEqual(
  140. res.data[0]["lastSeen"][:20], events[1].created.isoformat()[:20]
  141. )
  142. def test_issue_delete(self):
  143. issue = baker.make("issues.Issue", project=self.project)
  144. not_my_issue = baker.make("issues.Issue")
  145. url = reverse("issue-detail", args=[issue.id])
  146. res = self.client.delete(url)
  147. self.assertEqual(res.status_code, 204)
  148. url = reverse("issue-detail", args=[not_my_issue.id])
  149. res = self.client.delete(url)
  150. self.assertEqual(res.status_code, 404)
  151. def test_issue_update(self):
  152. issue = baker.make(Issue, project=self.project)
  153. self.assertEqual(issue.status, EventStatus.UNRESOLVED)
  154. url = reverse("issue-detail", kwargs={"pk": issue.pk})
  155. data = {"status": "resolved"}
  156. res = self.client.put(url, data)
  157. self.assertEqual(res.status_code, 200)
  158. issue.refresh_from_db()
  159. self.assertEqual(issue.status, EventStatus.RESOLVED)
  160. def test_bulk_update(self):
  161. """ Bulk update only supports Issue status """
  162. issues = baker.make(Issue, project=self.project, _quantity=2)
  163. url = f"{self.url}?id={issues[0].id}&id={issues[1].id}"
  164. status_to_set = EventStatus.RESOLVED
  165. data = {"status": status_to_set.label}
  166. res = self.client.put(url, data)
  167. self.assertContains(res, status_to_set.label)
  168. issues = Issue.objects.all()
  169. self.assertEqual(issues[0].status, status_to_set)
  170. self.assertEqual(issues[1].status, status_to_set)
  171. def test_bulk_update_query(self):
  172. """ Bulk update only supports Issue status """
  173. project2 = baker.make("projects.Project", organization=self.organization)
  174. project2.team_set.add(self.team)
  175. issue1 = baker.make(Issue, project=self.project)
  176. issue2 = baker.make(Issue, project=project2)
  177. url = f"{self.url}?query=is:unresolved&project={self.project.id}"
  178. status_to_set = EventStatus.RESOLVED
  179. data = {"status": status_to_set.label}
  180. res = self.client.put(url, data)
  181. self.assertContains(res, status_to_set.label)
  182. issue1.refresh_from_db()
  183. issue2.refresh_from_db()
  184. self.assertEqual(issue1.status, status_to_set)
  185. self.assertEqual(issue2.status, EventStatus.UNRESOLVED)
  186. def test_filter_project(self):
  187. baker.make(Issue, project=self.project)
  188. project = baker.make("projects.Project", organization=self.organization)
  189. project.team_set.add(self.team)
  190. issue = baker.make(Issue, project=project)
  191. res = self.client.get(self.url, {"project": project.id})
  192. self.assertEqual(len(res.data), 1)
  193. self.assertContains(res, issue.id)
  194. res = self.client.get(self.url, {"project": "nothing"})
  195. self.assertEqual(res.status_code, 400)
  196. def test_filter_environment(self):
  197. environment1_name = "prod"
  198. environment2_name = "staging"
  199. issue1 = baker.make(
  200. Issue, project=self.project, event_set__tags={"environment": "??"},
  201. )
  202. baker.make(
  203. Issue, project=self.project, event_set__tags={"foos": environment1_name},
  204. )
  205. baker.make(
  206. "events.Event", issue=issue1, tags={"environment": environment1_name}
  207. )
  208. issue2 = baker.make(
  209. Issue,
  210. project=self.project,
  211. event_set__tags={"environment": environment2_name},
  212. )
  213. baker.make(
  214. "events.Event", issue=issue2, tags={"environment": environment2_name}
  215. )
  216. baker.make(Issue, project=self.project)
  217. baker.make(Issue, project=self.project, event_set__tags={"environment": "dev"})
  218. baker.make(
  219. Issue, project=self.project, event_set__tags={"lol": environment2_name}
  220. )
  221. update_search_index_all_issues()
  222. res = self.client.get(
  223. self.url, {"environment": [environment1_name, environment2_name]},
  224. )
  225. self.assertEqual(len(res.data), 2)
  226. self.assertContains(res, issue1.id)
  227. self.assertContains(res, issue2.id)
  228. def test_issue_list_filter(self):
  229. project1 = self.project
  230. project2 = baker.make("projects.Project", organization=self.organization)
  231. project2.team_set.add(self.team)
  232. project3 = baker.make("projects.Project", organization=self.organization)
  233. project3.team_set.add(self.team)
  234. issue1 = baker.make("issues.Issue", project=project1)
  235. issue2 = baker.make("issues.Issue", project=project2)
  236. issue3 = baker.make("issues.Issue", project=project3)
  237. res = self.client.get(
  238. self.url + f"?project={project1.id}&project={project2.id}"
  239. )
  240. self.assertContains(res, issue1.title)
  241. self.assertContains(res, issue2.title)
  242. self.assertNotContains(res, issue3.title)
  243. def test_issue_list_sort(self):
  244. issue1 = baker.make("issues.Issue", project=self.project)
  245. issue2 = baker.make("issues.Issue", project=self.project)
  246. issue3 = baker.make("issues.Issue", project=self.project)
  247. baker.make("events.Event", issue=issue2, _quantity=2)
  248. baker.make("events.Event", issue=issue1)
  249. update_search_index_all_issues()
  250. res = self.client.get(self.url)
  251. self.assertEqual(res.data[0]["id"], issue1.id)
  252. res = self.client.get(self.url + "?sort=-count")
  253. self.assertEqual(res.data[0]["id"], issue2.id)
  254. res = self.client.get(self.url + "?sort=priority")
  255. self.assertEqual(res.data[0]["id"], issue3.id)
  256. res = self.client.get(self.url + "?sort=-priority")
  257. self.assertEqual(res.data[0]["id"], issue2.id)
  258. def test_filter_is_status(self):
  259. """ Match sentry's usage of "is" for status filtering """
  260. resolved_issue = baker.make(
  261. Issue, status=EventStatus.RESOLVED, project=self.project
  262. )
  263. unresolved_issue = baker.make(
  264. Issue,
  265. status=EventStatus.UNRESOLVED,
  266. project=self.project,
  267. tags={"platform": "Linux"},
  268. )
  269. res = self.client.get(self.url, {"query": "is:unresolved has:platform"})
  270. self.assertEqual(len(res.data), 1)
  271. self.assertContains(res, unresolved_issue.title)
  272. self.assertNotContains(res, resolved_issue.title)
  273. def test_issue_serializer_type(self):
  274. """
  275. Ensure type field is show in serializer
  276. https://gitlab.com/glitchtip/glitchtip-backend/-/issues/9
  277. """
  278. issue = baker.make("issues.Issue", project=self.project)
  279. url = reverse("issue-detail", args=[issue.id])
  280. res = self.client.get(url)
  281. self.assertContains(res, issue.get_type_display())
  282. def test_event_release(self):
  283. release = baker.make("releases.Release", organization=self.organization)
  284. event = baker.make("events.Event", issue__project=self.project, release=release)
  285. url = reverse(
  286. "project-events-list",
  287. kwargs={
  288. "project_pk": f"{self.project.organization.slug}/{self.project.slug}",
  289. },
  290. )
  291. res = self.client.get(url)
  292. # Not in list view
  293. self.assertNotContains(res, release.version)
  294. url = reverse(
  295. "project-events-detail",
  296. kwargs={
  297. "project_pk": f"{self.project.organization.slug}/{self.project.slug}",
  298. "pk": event.pk,
  299. },
  300. )
  301. res = self.client.get(url)
  302. self.assertContains(res, release.version)
  303. def test_issue_tags(self):
  304. issue = baker.make("issues.Issue", project=self.project)
  305. baker.make("events.Event", issue=issue, tags={"foo": "bar"}, _quantity=2)
  306. baker.make("events.Event", issue=issue, tags={"foo": "bar", "animal": "cat"})
  307. baker.make(
  308. "events.Event",
  309. issue=issue,
  310. tags={"animal": "dog", "foo": "cat"},
  311. _quantity=4,
  312. )
  313. url = reverse("issue-detail", args=[issue.id])
  314. res = self.client.get(url + "tags/")
  315. # Order is random
  316. if res.data[0]["name"] == "animal":
  317. animal = res.data[0]
  318. foo = res.data[1]
  319. else:
  320. animal = res.data[1]
  321. foo = res.data[0]
  322. self.assertEqual(animal["totalValues"], 5)
  323. self.assertEqual(animal["topValues"][0]["value"], "dog")
  324. self.assertEqual(animal["topValues"][0]["count"], 4)
  325. self.assertEqual(animal["uniqueValues"], 2)
  326. self.assertEqual(foo["totalValues"], 7)
  327. self.assertEqual(foo["topValues"][0]["value"], "cat")
  328. self.assertEqual(foo["topValues"][0]["count"], 4)
  329. self.assertEqual(foo["uniqueValues"], 2)
  330. def test_issue_tags_filter(self):
  331. issue = baker.make("issues.Issue", project=self.project)
  332. baker.make("events.Event", issue=issue, tags={"foo": "bar", "lol": "bar"})
  333. url = reverse("issue-detail", args=[issue.id])
  334. res = self.client.get(url + "tags/?key=foo")
  335. self.assertEqual(len(res.data), 1)
  336. def test_issue_tags_performance(self):
  337. issue = baker.make("issues.Issue", project=self.project)
  338. baker.make("events.Event", issue=issue, tags={"foo": "bar"}, _quantity=50)
  339. baker.make(
  340. "events.Event",
  341. issue=issue,
  342. tags={"foo": "bar", "animal": "cat"},
  343. _quantity=100,
  344. )
  345. baker.make(
  346. "events.Event",
  347. issue=issue,
  348. tags={"type": "a", "animal": "cat"},
  349. _quantity=100,
  350. )
  351. baker.make(
  352. "events.Event",
  353. issue=issue,
  354. tags={"haha": "a", "arg": "cat", "b": "b"},
  355. _quantity=100,
  356. )
  357. baker.make(
  358. "events.Event", issue=issue, tags={"type": "b", "foo": "bar"}, _quantity=200
  359. )
  360. url = reverse("issue-detail", args=[issue.id])
  361. with self.assertNumQueries(7): # Includes many auth related queries
  362. start = timer()
  363. res = self.client.get(url + "tags/")
  364. end = timer()
  365. # print(end - start)
  366. def test_issue_tag_detail(self):
  367. issue = baker.make("issues.Issue", project=self.project)
  368. baker.make(
  369. "events.Event", issue=issue, tags={"foo": "bar", "a": "b"}, _quantity=2
  370. )
  371. baker.make("events.Event", issue=issue, tags={"foo": "foobar"})
  372. baker.make("events.Event", issue=issue, tags={"type": "a"})
  373. url = reverse("issue-detail", args=[issue.id])
  374. res = self.client.get(url + "tags/foo/")
  375. self.assertContains(res, "foobar")
  376. self.assertEqual(res.data["totalValues"], 3)
  377. self.assertEqual(res.data["uniqueValues"], 2)
  378. res = self.client.get(url + "tags/ahh/")
  379. self.assertEqual(res.status_code, 404)
  380. def test_issue_greatest_level(self):
  381. """
  382. The issue should be the greatest level seen in events
  383. This is a deviation from Sentry OSS
  384. """
  385. issue = baker.make("issues.Issue", level=1)
  386. baker.make("events.Event", issue=issue, level=1)
  387. baker.make("events.Event", issue=issue, level=3)
  388. baker.make("events.Event", issue=issue, level=2)
  389. Issue.update_index(issue.pk)
  390. issue.refresh_from_db()
  391. self.assertEqual(issue.level, 3)