tests.py 20 KB

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