tests.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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 EventStatus, Issue
  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. team = baker.make("teams.Team", organization=self.organization)
  88. team.members.add(self.org_user)
  89. self.project.team_set.add(team)
  90. url = reverse(
  91. "event_json",
  92. kwargs={
  93. "org": self.organization.slug,
  94. "issue": event.issue.id,
  95. "event": event.event_id_hex,
  96. },
  97. )
  98. res = self.client.get(url)
  99. self.assertContains(res, event.event_id_hex)
  100. url = reverse(
  101. "event_json",
  102. kwargs={
  103. "org": "nope",
  104. "issue": event.issue.id,
  105. "event": event.event_id_hex,
  106. },
  107. )
  108. res = self.client.get(url)
  109. self.assertEqual(res.status_code, 404)
  110. class IssuesAPITestCase(GlitchTipTestCase):
  111. def setUp(self):
  112. self.create_user_and_project()
  113. self.url = reverse("issue-list")
  114. def test_issue_list(self):
  115. issue = baker.make("issues.Issue", project=self.project)
  116. not_my_issue = baker.make("issues.Issue")
  117. res = self.client.get(self.url)
  118. self.assertContains(res, issue.title)
  119. self.assertNotContains(res, not_my_issue.title)
  120. self.assertEqual(res.get("X-Hits"), "1")
  121. def test_no_duplicate_issues(self):
  122. """
  123. Addresses https://gitlab.com/glitchtip/glitchtip-backend/-/issues/109
  124. Ensure issues can be filtered by org membership but not duplicated
  125. """
  126. baker.make("issues.Issue", project=self.project)
  127. team2 = baker.make("teams.Team", organization=self.organization)
  128. team2.members.add(self.org_user)
  129. self.project.team_set.add(team2)
  130. res = self.client.get(self.url)
  131. self.assertEqual(len(res.data), 1)
  132. team2.delete()
  133. self.team.delete()
  134. res = self.client.get(self.url)
  135. self.assertEqual(len(res.data), 1)
  136. self.org_user.delete()
  137. res = self.client.get(self.url)
  138. self.assertEqual(len(res.data), 0)
  139. def test_issue_retrieve(self):
  140. issue = baker.make("issues.Issue", project=self.project)
  141. not_my_issue = baker.make("issues.Issue")
  142. url = reverse("issue-detail", args=[issue.id])
  143. res = self.client.get(url)
  144. self.assertContains(res, issue.title)
  145. url = reverse("issue-detail", args=[not_my_issue.id])
  146. res = self.client.get(url)
  147. self.assertEqual(res.status_code, 404)
  148. def test_issue_last_seen(self):
  149. issue = baker.make("issues.Issue", project=self.project)
  150. events = baker.make("events.Event", issue=issue, _quantity=2)
  151. res = self.client.get(self.url)
  152. self.assertEqual(
  153. res.data[0]["lastSeen"][:20], events[1].created.isoformat()[:20]
  154. )
  155. def test_issue_delete(self):
  156. issue = baker.make("issues.Issue", project=self.project)
  157. not_my_issue = baker.make("issues.Issue")
  158. url = reverse("issue-detail", args=[issue.id])
  159. res = self.client.delete(url)
  160. self.assertEqual(res.status_code, 204)
  161. url = reverse("issue-detail", args=[not_my_issue.id])
  162. res = self.client.delete(url)
  163. self.assertEqual(res.status_code, 404)
  164. def test_issue_update(self):
  165. issue = baker.make(Issue, project=self.project)
  166. self.assertEqual(issue.status, EventStatus.UNRESOLVED)
  167. url = reverse("issue-detail", kwargs={"pk": issue.pk})
  168. data = {"status": "resolved"}
  169. res = self.client.put(url, data)
  170. self.assertEqual(res.status_code, 200)
  171. issue.refresh_from_db()
  172. self.assertEqual(issue.status, EventStatus.RESOLVED)
  173. def test_bulk_update(self):
  174. """Bulk update only supports Issue status"""
  175. issues = baker.make(Issue, project=self.project, _quantity=2)
  176. url = f"{self.url}?id={issues[0].id}&id={issues[1].id}"
  177. status_to_set = EventStatus.RESOLVED
  178. data = {"status": status_to_set.label}
  179. res = self.client.put(url, data)
  180. self.assertContains(res, status_to_set.label)
  181. issues = Issue.objects.all()
  182. self.assertEqual(issues[0].status, status_to_set)
  183. self.assertEqual(issues[1].status, status_to_set)
  184. def test_bulk_update_query(self):
  185. """Bulk update only supports Issue status"""
  186. project2 = baker.make("projects.Project", organization=self.organization)
  187. project2.team_set.add(self.team)
  188. issue1 = baker.make(Issue, project=self.project)
  189. issue2 = baker.make(Issue, project=project2)
  190. url = f"{self.url}?query=is:unresolved&project={self.project.id}"
  191. status_to_set = EventStatus.RESOLVED
  192. data = {"status": status_to_set.label}
  193. res = self.client.put(url, data)
  194. self.assertContains(res, status_to_set.label)
  195. issue1.refresh_from_db()
  196. issue2.refresh_from_db()
  197. self.assertEqual(issue1.status, status_to_set)
  198. self.assertEqual(issue2.status, EventStatus.UNRESOLVED)
  199. def test_filter_project(self):
  200. baker.make(Issue, project=self.project)
  201. project = baker.make("projects.Project", organization=self.organization)
  202. project.team_set.add(self.team)
  203. issue = baker.make(Issue, project=project)
  204. res = self.client.get(self.url, {"project": project.id})
  205. self.assertEqual(len(res.data), 1)
  206. self.assertContains(res, issue.id)
  207. res = self.client.get(self.url, {"project": "nothing"})
  208. self.assertEqual(res.status_code, 400)
  209. def test_filter_environment(self):
  210. environment1_name = "prod"
  211. environment2_name = "staging"
  212. issue1 = baker.make(
  213. Issue,
  214. project=self.project,
  215. event_set__tags={"environment": "??"},
  216. )
  217. baker.make(
  218. Issue,
  219. project=self.project,
  220. event_set__tags={"foos": environment1_name},
  221. )
  222. baker.make(
  223. "events.Event", issue=issue1, tags={"environment": environment1_name}
  224. )
  225. issue2 = baker.make(
  226. Issue,
  227. project=self.project,
  228. event_set__tags={"environment": environment2_name},
  229. )
  230. baker.make(
  231. "events.Event", issue=issue2, tags={"environment": environment2_name}
  232. )
  233. baker.make(Issue, project=self.project)
  234. baker.make(Issue, project=self.project, event_set__tags={"environment": "dev"})
  235. baker.make(
  236. Issue, project=self.project, event_set__tags={"lol": environment2_name}
  237. )
  238. update_search_index_all_issues()
  239. res = self.client.get(
  240. self.url,
  241. {"environment": [environment1_name, environment2_name]},
  242. )
  243. self.assertEqual(len(res.data), 2)
  244. self.assertContains(res, issue1.id)
  245. self.assertContains(res, issue2.id)
  246. def test_issue_list_filter(self):
  247. project1 = self.project
  248. project2 = baker.make("projects.Project", organization=self.organization)
  249. project2.team_set.add(self.team)
  250. project3 = baker.make("projects.Project", organization=self.organization)
  251. project3.team_set.add(self.team)
  252. issue1 = baker.make("issues.Issue", project=project1)
  253. issue2 = baker.make("issues.Issue", project=project2)
  254. issue3 = baker.make("issues.Issue", project=project3)
  255. res = self.client.get(
  256. self.url + f"?project={project1.id}&project={project2.id}"
  257. )
  258. self.assertContains(res, issue1.title)
  259. self.assertContains(res, issue2.title)
  260. self.assertNotContains(res, issue3.title)
  261. def test_issue_list_sort(self):
  262. issue1 = baker.make("issues.Issue", project=self.project)
  263. issue2 = baker.make("issues.Issue", project=self.project)
  264. issue3 = baker.make("issues.Issue", project=self.project)
  265. baker.make("events.Event", issue=issue2, _quantity=2)
  266. baker.make("events.Event", issue=issue1)
  267. update_search_index_all_issues()
  268. res = self.client.get(self.url)
  269. self.assertEqual(res.data[0]["id"], str(issue1.id))
  270. res = self.client.get(self.url + "?sort=-count")
  271. self.assertEqual(res.data[0]["id"], str(issue2.id))
  272. res = self.client.get(self.url + "?sort=priority")
  273. self.assertEqual(res.data[0]["id"], str(issue3.id))
  274. res = self.client.get(self.url + "?sort=-priority")
  275. self.assertEqual(res.data[0]["id"], str(issue2.id))
  276. def test_filter_is_status(self):
  277. """Match sentry's usage of "is" for status filtering"""
  278. resolved_issue = baker.make(
  279. Issue, status=EventStatus.RESOLVED, project=self.project
  280. )
  281. unresolved_issue = baker.make(
  282. Issue,
  283. status=EventStatus.UNRESOLVED,
  284. project=self.project,
  285. tags={"platform": "Linux"},
  286. )
  287. res = self.client.get(self.url, {"query": "is:unresolved has:platform"})
  288. self.assertEqual(len(res.data), 1)
  289. self.assertContains(res, unresolved_issue.title)
  290. self.assertNotContains(res, resolved_issue.title)
  291. def test_issue_serializer_type(self):
  292. """
  293. Ensure type field is show in serializer
  294. https://gitlab.com/glitchtip/glitchtip-backend/-/issues/9
  295. """
  296. issue = baker.make("issues.Issue", project=self.project)
  297. url = reverse("issue-detail", args=[issue.id])
  298. res = self.client.get(url)
  299. self.assertContains(res, issue.get_type_display())
  300. def test_event_release(self):
  301. release = baker.make("releases.Release", organization=self.organization)
  302. event = baker.make("events.Event", issue__project=self.project, release=release)
  303. url = reverse(
  304. "project-events-list",
  305. kwargs={
  306. "project_pk": f"{self.project.organization.slug}/{self.project.slug}",
  307. },
  308. )
  309. res = self.client.get(url)
  310. # Not in list view
  311. self.assertNotContains(res, release.version)
  312. url = reverse(
  313. "project-events-detail",
  314. kwargs={
  315. "project_pk": f"{self.project.organization.slug}/{self.project.slug}",
  316. "pk": event.pk,
  317. },
  318. )
  319. res = self.client.get(url)
  320. self.assertContains(res, release.version)
  321. def test_issue_tags(self):
  322. issue = baker.make("issues.Issue", project=self.project)
  323. baker.make("events.Event", issue=issue, tags={"foo": "bar"}, _quantity=2)
  324. baker.make("events.Event", issue=issue, tags={"foo": "bar", "animal": "cat"})
  325. baker.make(
  326. "events.Event",
  327. issue=issue,
  328. tags={"animal": "dog", "foo": "cat"},
  329. _quantity=4,
  330. )
  331. url = reverse("issue-detail", args=[issue.id])
  332. res = self.client.get(url + "tags/")
  333. # Order is random
  334. if res.data[0]["name"] == "animal":
  335. animal = res.data[0]
  336. foo = res.data[1]
  337. else:
  338. animal = res.data[1]
  339. foo = res.data[0]
  340. self.assertEqual(animal["totalValues"], 5)
  341. self.assertEqual(animal["topValues"][0]["value"], "dog")
  342. self.assertEqual(animal["topValues"][0]["count"], 4)
  343. self.assertEqual(animal["uniqueValues"], 2)
  344. self.assertEqual(foo["totalValues"], 7)
  345. self.assertEqual(foo["topValues"][0]["value"], "cat")
  346. self.assertEqual(foo["topValues"][0]["count"], 4)
  347. self.assertEqual(foo["uniqueValues"], 2)
  348. def test_issue_tags_filter(self):
  349. issue = baker.make("issues.Issue", project=self.project)
  350. baker.make("events.Event", issue=issue, tags={"foo": "bar", "lol": "bar"})
  351. url = reverse("issue-detail", args=[issue.id])
  352. res = self.client.get(url + "tags/?key=foo")
  353. self.assertEqual(len(res.data), 1)
  354. def test_issue_tags_performance(self):
  355. issue = baker.make("issues.Issue", project=self.project)
  356. baker.make("events.Event", issue=issue, tags={"foo": "bar"}, _quantity=50)
  357. baker.make(
  358. "events.Event",
  359. issue=issue,
  360. tags={"foo": "bar", "animal": "cat"},
  361. _quantity=100,
  362. )
  363. baker.make(
  364. "events.Event",
  365. issue=issue,
  366. tags={"type": "a", "animal": "cat"},
  367. _quantity=100,
  368. )
  369. baker.make(
  370. "events.Event",
  371. issue=issue,
  372. tags={"haha": "a", "arg": "cat", "b": "b"},
  373. _quantity=100,
  374. )
  375. baker.make(
  376. "events.Event", issue=issue, tags={"type": "b", "foo": "bar"}, _quantity=200
  377. )
  378. url = reverse("issue-detail", args=[issue.id])
  379. with self.assertNumQueries(7): # Includes many auth related queries
  380. start = timer()
  381. res = self.client.get(url + "tags/")
  382. end = timer()
  383. # print(end - start)
  384. def test_issue_tag_detail(self):
  385. issue = baker.make("issues.Issue", project=self.project)
  386. baker.make(
  387. "events.Event", issue=issue, tags={"foo": "bar", "a": "b"}, _quantity=2
  388. )
  389. baker.make("events.Event", issue=issue, tags={"foo": "foobar"})
  390. baker.make("events.Event", issue=issue, tags={"type": "a"})
  391. url = reverse("issue-detail", args=[issue.id])
  392. res = self.client.get(url + "tags/foo/")
  393. self.assertContains(res, "foobar")
  394. self.assertEqual(res.data["totalValues"], 3)
  395. self.assertEqual(res.data["uniqueValues"], 2)
  396. res = self.client.get(url + "tags/ahh/")
  397. self.assertEqual(res.status_code, 404)
  398. def test_issue_greatest_level(self):
  399. """
  400. The issue should be the greatest level seen in events
  401. This is a deviation from Sentry OSS
  402. """
  403. issue = baker.make("issues.Issue", level=1)
  404. baker.make("events.Event", issue=issue, level=1)
  405. baker.make("events.Event", issue=issue, level=3)
  406. baker.make("events.Event", issue=issue, level=2)
  407. Issue.update_index(issue.pk)
  408. issue.refresh_from_db()
  409. self.assertEqual(issue.level, 3)