tests.py 21 KB

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