tests.py 19 KB

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