test_issues_api.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. import datetime
  2. import logging
  3. from timeit import default_timer as timer
  4. from django.contrib.postgres.search import SearchVector
  5. from django.db.models import F, Value
  6. from django.test import TestCase
  7. from django.urls import reverse
  8. from django.utils import timezone
  9. from freezegun import freeze_time
  10. from model_bakery import baker
  11. from apps.event_ingest.model_functions import PipeConcat
  12. from glitchtip.test_utils.test_case import APIPermissionTestCase, GlitchTipTestCaseMixin
  13. from ..constants import EventStatus, LogLevel
  14. from ..models import Issue
  15. logger = logging.getLogger(__name__)
  16. def get_issue_url(issue_id: int) -> str:
  17. return reverse("api:get_issue", kwargs={"issue_id": issue_id})
  18. def get_organization_issue_url(organization_slug: str, issue_id: int) -> str:
  19. return reverse(
  20. "api:update_organization_issue",
  21. kwargs={"organization_slug": organization_slug, "issue_id": issue_id},
  22. )
  23. class IssueAPITestCase(GlitchTipTestCaseMixin, TestCase):
  24. def setUp(self):
  25. super().create_logged_in_user()
  26. self.list_url = reverse(
  27. "api:list_issues", kwargs={"organization_slug": self.organization.slug}
  28. )
  29. def test_retrieve(self):
  30. issue = baker.make("issue_events.Issue", project=self.project, short_id=1)
  31. event = baker.make("issue_events.IssueEvent", issue=issue)
  32. baker.make(
  33. "issue_events.UserReport",
  34. project=self.project,
  35. issue=issue,
  36. event_id=event.pk.hex,
  37. _quantity=1,
  38. )
  39. baker.make("issue_events.Comment", issue=issue, _quantity=3)
  40. url = reverse(
  41. "api:get_issue",
  42. kwargs={"issue_id": issue.id},
  43. )
  44. res = self.client.get(url)
  45. data = res.json()
  46. self.assertEqual(
  47. data.get("shortId"), f"{self.project.slug.upper()}-{issue.short_id}"
  48. )
  49. self.assertEqual(data.get("count"), str(issue.count))
  50. self.assertEqual(data.get("userReportCount"), 1)
  51. self.assertEqual(data.get("numComments"), 3)
  52. def test_list(self):
  53. res = self.client.get(self.list_url)
  54. self.assertEqual(res.status_code, 200)
  55. not_my_issue = baker.make("issue_events.Issue")
  56. issue = baker.make("issue_events.Issue", project=self.project, short_id=1)
  57. baker.make("issue_events.IssueEvent", issue=issue)
  58. res = self.client.get(self.list_url)
  59. self.assertContains(res, issue.title)
  60. self.assertNotContains(res, not_my_issue.title)
  61. self.assertEqual(len(res.json()), 1)
  62. def test_project_issue_list(self):
  63. not_my_project = baker.make("projects.Project", organization=self.organization)
  64. not_my_issue = baker.make("issue_events.Issue", project=not_my_project)
  65. issue = baker.make("issue_events.Issue", project=self.project, short_id=1)
  66. baker.make("issue_events.IssueEvent", issue=issue)
  67. url = reverse(
  68. "api:list_project_issues",
  69. kwargs={
  70. "organization_slug": self.organization.slug,
  71. "project_slug": self.project.slug,
  72. },
  73. )
  74. res = self.client.get(url)
  75. self.assertContains(res, issue.title)
  76. self.assertNotContains(res, not_my_issue.title)
  77. self.assertEqual(len(res.json()), 1)
  78. def test_filter_by_date(self):
  79. """
  80. A user should be able to filter by start and end datetimes.
  81. In the future, this should filter events, not first_seen.
  82. """
  83. issue1 = baker.make(
  84. "issue_events.Issue",
  85. first_seen=timezone.make_aware(timezone.datetime(1999, 1, 1)),
  86. project=self.project,
  87. )
  88. issue2 = baker.make(
  89. "issue_events.Issue",
  90. first_seen=timezone.make_aware(timezone.datetime(2010, 1, 1)),
  91. project=self.project,
  92. )
  93. issue3 = baker.make(
  94. "issue_events.Issue",
  95. first_seen=timezone.make_aware(timezone.datetime(2020, 1, 1)),
  96. project=self.project,
  97. )
  98. res = self.client.get(
  99. self.list_url
  100. + "?start=2000-01-01T05:00:00.000Z&end=2019-01-01T05:00:00.000Z"
  101. )
  102. self.assertContains(res, issue2.title)
  103. self.assertNotContains(res, issue1.title)
  104. self.assertNotContains(res, issue3.title)
  105. def test_sort(self):
  106. issue1 = baker.make("issue_events.Issue", project=self.project)
  107. issue2 = baker.make("issue_events.Issue", project=self.project, count=2)
  108. issue3 = baker.make("issue_events.Issue", project=self.project)
  109. res = self.client.get(self.list_url)
  110. self.assertEqual(res.json()[0]["id"], str(issue3.id))
  111. res = self.client.get(self.list_url + "?sort=-count")
  112. self.assertEqual(res.json()[0]["id"], str(issue2.id))
  113. res = self.client.get(self.list_url + "?sort=priority")
  114. self.assertEqual(res.json()[0]["id"], str(issue1.id))
  115. res = self.client.get(self.list_url + "?sort=-priority")
  116. self.assertEqual(res.json()[0]["id"], str(issue2.id))
  117. def test_search(self):
  118. issue = baker.make(
  119. "issue_events.Issue",
  120. project=self.project,
  121. search_vector=SearchVector(Value("apple sauce")),
  122. )
  123. event = baker.make("issue_events.IssueEvent", issue=issue)
  124. other_issue = baker.make("issue_events.Issue", project=self.project)
  125. res = self.client.get(self.list_url + "?query=is:unresolved apple+sauce")
  126. self.assertContains(res, issue.title)
  127. self.assertNotContains(res, other_issue.title)
  128. # Not sure how to do this in Ninja without always removing None field values
  129. # self.assertNotContains(res, "matchingEventId")
  130. self.assertNotIn("X-Sentry-Direct-Hit", res.headers)
  131. res = self.client.get(self.list_url + "?query=is:unresolved apple sauce")
  132. self.assertContains(res, issue.title)
  133. self.assertNotContains(res, other_issue.title)
  134. res = self.client.get(self.list_url + '?query=is:unresolved "apple sauce"')
  135. self.assertContains(res, issue.title)
  136. self.assertNotContains(res, other_issue.title)
  137. res = self.client.get(self.list_url + "?query=" + event.id.hex)
  138. self.assertContains(res, issue.title)
  139. self.assertNotContains(res, other_issue.title)
  140. self.assertContains(res, "matchingEventId")
  141. self.assertContains(res, event.id.hex)
  142. self.assertEqual(res.headers.get("X-Sentry-Direct-Hit"), "1")
  143. event3 = baker.make(
  144. "issue_events.IssueEvent", issue=issue, data={"name": "plum sauce"}
  145. )
  146. Issue.objects.filter(id=issue.id).update(
  147. search_vector=SearchVector(
  148. PipeConcat(F("search_vector"), SearchVector(Value(event3.data["name"])))
  149. )
  150. )
  151. issue.search_vector = SearchVector(Value("apple sauce plum "))
  152. res = self.client.get(self.list_url + '?query=is:unresolved "plum sauce"')
  153. self.assertContains(res, event3.issue.title)
  154. res = self.client.get(self.list_url + '?query=is:unresolved "apple sauce"')
  155. self.assertContains(res, event.issue.title)
  156. def test_list_relative_datetime_filter(self):
  157. now = timezone.now()
  158. last_minute = now - datetime.timedelta(minutes=1)
  159. with freeze_time(last_minute):
  160. baker.make("issue_events.IssueEvent", issue__project=self.project)
  161. two_minutes_ago = now - datetime.timedelta(minutes=2)
  162. with freeze_time(two_minutes_ago):
  163. baker.make("issue_events.IssueEvent", issue__project=self.project)
  164. yesterday = now - datetime.timedelta(days=1)
  165. with freeze_time(yesterday):
  166. baker.make("issue_events.IssueEvent", issue__project=self.project)
  167. url = self.list_url
  168. with freeze_time(now):
  169. res = self.client.get(url, {"start": "now-1m"})
  170. self.assertEqual(res.status_code, 200)
  171. self.assertEqual(len(res.json()), 1)
  172. with freeze_time(now):
  173. res = self.client.get(url, {"start": "now-2m"})
  174. self.assertEqual(res.status_code, 200)
  175. self.assertEqual(len(res.json()), 2)
  176. with freeze_time(now):
  177. res = self.client.get(url, {"start": "now-24h", "end": "now"})
  178. self.assertEqual(res.status_code, 200)
  179. self.assertEqual(len(res.json()), 3)
  180. with freeze_time(now):
  181. res = self.client.get(url, {"end": "now-3m"})
  182. self.assertEqual(res.status_code, 200)
  183. self.assertEqual(len(res.json()), 1)
  184. def test_tag_space(self):
  185. tag_name = "os.name"
  186. tag_value = "Linux Vista"
  187. event = baker.make(
  188. "issue_events.IssueEvent",
  189. issue__project=self.project,
  190. tags={tag_name: tag_value, "foo": "bar"},
  191. )
  192. baker.make(
  193. "issue_events.IssueTag",
  194. issue=event.issue,
  195. tag_key__key=tag_name,
  196. tag_value__value=tag_value,
  197. )
  198. baker.make(
  199. "issue_events.IssueTag",
  200. issue=event.issue,
  201. tag_key__key="foo",
  202. tag_value__value="bar",
  203. )
  204. event2 = baker.make(
  205. "issue_events.IssueEvent",
  206. issue__project=self.project,
  207. tags={tag_name: "BananaOS 7"},
  208. )
  209. res = self.client.get(
  210. self.list_url + f'?query={tag_name}:"Linux+Vista" foo:bar'
  211. )
  212. self.assertContains(res, event.issue.title)
  213. self.assertNotContains(res, event2.issue.title)
  214. def test_filter_by_tag(self):
  215. tag_browser = "browser.name"
  216. tag_value_firefox = "Firefox"
  217. tag_value_chrome = "Chrome"
  218. tag_value_cthulhu = "Cthulhu"
  219. tag_mythic_animal = "mythic_animal"
  220. key_browser = baker.make("issue_events.TagKey", key=tag_browser)
  221. key_mythic_animal = baker.make("issue_events.TagKey", key=tag_mythic_animal)
  222. value_firefox = baker.make("issue_events.TagValue", value=tag_value_firefox)
  223. value_chrome = baker.make("issue_events.TagValue", value=tag_value_chrome)
  224. value_cthulhu = baker.make("issue_events.TagValue", value=tag_value_cthulhu)
  225. event_only_firefox = baker.make(
  226. "issue_events.IssueEvent",
  227. issue__project=self.project,
  228. tags={tag_browser: tag_value_firefox},
  229. )
  230. baker.make(
  231. "issue_events.IssueTag",
  232. issue=event_only_firefox.issue,
  233. tag_key=key_browser,
  234. tag_value=value_firefox,
  235. )
  236. event_only_firefox2 = baker.make(
  237. "issue_events.IssueEvent",
  238. issue=event_only_firefox.issue,
  239. tags={tag_mythic_animal: tag_value_cthulhu},
  240. )
  241. baker.make(
  242. "issue_events.IssueTag",
  243. issue=event_only_firefox2.issue,
  244. tag_key=key_mythic_animal,
  245. tag_value=value_cthulhu,
  246. )
  247. event_firefox_chrome = baker.make(
  248. "issue_events.IssueEvent",
  249. issue__project=self.project,
  250. tags={tag_browser: tag_value_firefox},
  251. )
  252. baker.make(
  253. "issue_events.IssueTag",
  254. issue=event_firefox_chrome.issue,
  255. tag_key=key_browser,
  256. tag_value=value_firefox,
  257. )
  258. event_firefox_chrome2 = baker.make(
  259. "issue_events.IssueEvent",
  260. issue=event_firefox_chrome.issue,
  261. tags={tag_browser: tag_value_chrome},
  262. )
  263. baker.make(
  264. "issue_events.IssueTag",
  265. issue=event_firefox_chrome2.issue,
  266. tag_key=key_browser,
  267. tag_value=value_chrome,
  268. )
  269. event_no_tags = baker.make(
  270. "issue_events.IssueEvent", issue__project=self.project
  271. )
  272. event_browser_chrome_mythic_animal_firefox = baker.make(
  273. "issue_events.IssueEvent",
  274. issue__project=self.project,
  275. tags={tag_mythic_animal: tag_value_firefox, tag_browser: tag_value_chrome},
  276. )
  277. baker.make(
  278. "issue_events.IssueTag",
  279. issue=event_browser_chrome_mythic_animal_firefox.issue,
  280. tag_key=key_mythic_animal,
  281. tag_value=value_firefox,
  282. )
  283. baker.make(
  284. "issue_events.IssueTag",
  285. issue=event_browser_chrome_mythic_animal_firefox.issue,
  286. tag_key=key_browser,
  287. tag_value=value_chrome,
  288. )
  289. url = self.list_url
  290. res = self.client.get(url + f'?query={tag_browser}:"{tag_value_firefox}"')
  291. self.assertContains(res, event_only_firefox.issue.title)
  292. self.assertContains(res, event_firefox_chrome.issue.title)
  293. self.assertNotContains(res, event_no_tags.issue.title)
  294. self.assertNotContains(
  295. res, event_browser_chrome_mythic_animal_firefox.issue.title
  296. )
  297. # Browser is Firefox AND Chrome
  298. res = self.client.get(
  299. url
  300. + f"?query={tag_browser}:{tag_value_firefox} {tag_browser}:{tag_value_chrome}"
  301. )
  302. self.assertNotContains(res, event_only_firefox.issue.title)
  303. self.assertContains(res, event_firefox_chrome.issue.title)
  304. self.assertNotContains(res, event_no_tags.issue.title)
  305. self.assertNotContains(
  306. res, event_browser_chrome_mythic_animal_firefox.issue.title
  307. )
  308. # Browser mythic_animal is Firefox
  309. res = self.client.get(url + f"?query={tag_mythic_animal}:{tag_value_firefox}")
  310. self.assertNotContains(res, event_only_firefox.issue.title)
  311. self.assertNotContains(res, event_firefox_chrome.issue.title)
  312. self.assertNotContains(res, event_no_tags.issue.title)
  313. self.assertContains(res, event_browser_chrome_mythic_animal_firefox.issue.title)
  314. # Browser is Chrome AND mythic_animal is Firefox
  315. res = self.client.get(
  316. url
  317. + f"?query={tag_browser}:{tag_value_chrome} {tag_mythic_animal}:{tag_value_firefox}"
  318. )
  319. self.assertNotContains(res, event_only_firefox.issue.title)
  320. self.assertNotContains(res, event_firefox_chrome.issue.title)
  321. self.assertNotContains(res, event_no_tags.issue.title)
  322. self.assertContains(res, event_browser_chrome_mythic_animal_firefox.issue.title)
  323. # Browser is Firefox AND mythic_animal is Firefox
  324. res = self.client.get(
  325. url
  326. + f"?query={tag_browser}:{tag_value_firefox} {tag_mythic_animal}:{tag_value_firefox}"
  327. )
  328. self.assertNotContains(res, event_only_firefox.issue.title)
  329. self.assertNotContains(res, event_firefox_chrome.issue.title)
  330. self.assertNotContains(res, event_no_tags.issue.title)
  331. self.assertNotContains(
  332. res, event_browser_chrome_mythic_animal_firefox.issue.title
  333. )
  334. def test_filter_by_tag_distinct(self):
  335. tag_browser = "browser.name"
  336. tag_value = "Firefox"
  337. tag_value2 = "Chrome"
  338. key_browser = baker.make("issue_events.TagKey", key=tag_browser)
  339. value = baker.make("issue_events.TagValue", value=tag_value)
  340. value2 = baker.make("issue_events.TagValue", value=tag_value2)
  341. event = baker.make(
  342. "issue_events.IssueEvent",
  343. issue__project=self.project,
  344. tags={tag_browser: tag_value},
  345. )
  346. baker.make(
  347. "issue_events.IssueTag",
  348. issue=event.issue,
  349. tag_key=key_browser,
  350. tag_value=value,
  351. )
  352. baker.make(
  353. "issue_events.IssueEvent",
  354. issue=event.issue,
  355. tags={tag_browser: tag_value},
  356. _quantity=2,
  357. )
  358. baker.make(
  359. "issue_events.IssueTag",
  360. issue=event.issue,
  361. tag_key=key_browser,
  362. tag_value=value,
  363. )
  364. baker.make(
  365. "issue_events.IssueEvent",
  366. issue=event.issue,
  367. tags={tag_browser: tag_value},
  368. _quantity=5,
  369. )
  370. baker.make(
  371. "issue_events.IssueTag",
  372. issue=event.issue,
  373. tag_key=key_browser,
  374. tag_value=value,
  375. )
  376. baker.make(
  377. "issue_events.IssueTag",
  378. issue=event.issue,
  379. tag_key=key_browser,
  380. tag_value=value2,
  381. )
  382. baker.make(
  383. "issue_events.IssueEvent",
  384. issue=event.issue,
  385. tags={tag_browser: tag_value2},
  386. _quantity=5,
  387. )
  388. baker.make(
  389. "issue_events.IssueTag",
  390. issue=event.issue,
  391. tag_key=key_browser,
  392. tag_value=value2,
  393. )
  394. res = self.client.get(self.list_url + f'?query={tag_browser}:"{tag_value}"')
  395. self.assertEqual(len(res.json()), 1)
  396. def test_filter_environment(self):
  397. environment1_name = "prod"
  398. environment2_name = "staging"
  399. key_environment = baker.make("issue_events.TagKey", key="environment")
  400. environment1_value = baker.make(
  401. "issue_events.TagValue", value=environment1_name
  402. )
  403. environment2_value = baker.make(
  404. "issue_events.TagValue", value=environment2_name
  405. )
  406. environment3_value = baker.make("issue_events.TagValue", value="dev")
  407. issue1 = baker.make(
  408. "issue_events.Issue",
  409. project=self.project,
  410. )
  411. baker.make(
  412. "issue_events.IssueTag",
  413. issue=issue1,
  414. tag_key=key_environment,
  415. tag_value=environment1_value,
  416. )
  417. issue2 = baker.make(
  418. "issue_events.Issue",
  419. project=self.project,
  420. )
  421. baker.make(
  422. "issue_events.IssueTag",
  423. issue=issue2,
  424. tag_key=key_environment,
  425. tag_value=environment2_value,
  426. )
  427. issue3 = baker.make("issue_events.Issue", project=self.project)
  428. baker.make(
  429. "issue_events.IssueTag",
  430. issue=issue3,
  431. tag_key=key_environment,
  432. tag_value=environment3_value,
  433. )
  434. res = self.client.get(
  435. self.list_url
  436. + f"?environment={environment1_name}&environment={environment2_name}"
  437. )
  438. data = res.json()
  439. self.assertEqual(len(data), 2)
  440. self.assertNotIn(str(issue3.id), [data[0]["id"], data[1]["id"]])
  441. def test_filter_by_level(self):
  442. """
  443. A user should be able to filter by issue levels.
  444. """
  445. level_warning = LogLevel.WARNING
  446. level_fatal = LogLevel.FATAL
  447. issue1 = baker.make(
  448. "issue_events.Issue", project=self.project, level=level_warning
  449. )
  450. issue2 = baker.make(
  451. "issue_events.Issue", project=self.project, level=level_fatal
  452. )
  453. baker.make("issue_events.Issue", project=self.project)
  454. res = self.client.get(self.list_url + f"?query=level:{level_warning.label}")
  455. data = res.json()
  456. self.assertEqual(len(data), 1)
  457. self.assertEqual(data[0]["id"], str(issue1.id))
  458. res = self.client.get(self.list_url + f"?query=level:{level_fatal.label}")
  459. data = res.json()
  460. self.assertEqual(len(data), 1)
  461. self.assertEqual(data[0]["id"], str(issue2.id))
  462. res = self.client.get(self.list_url)
  463. self.assertEqual(len(res.json()), 3)
  464. def test_issue_delete(self):
  465. issue = baker.make("issue_events.Issue", project=self.project)
  466. not_my_issue = baker.make("issue_events.Issue")
  467. res = self.client.delete(get_issue_url(issue.id))
  468. self.assertEqual(res.status_code, 204)
  469. res = self.client.delete(get_issue_url(not_my_issue.id))
  470. self.assertEqual(res.status_code, 404)
  471. def test_issue_update(self):
  472. issue = baker.make("issue_events.Issue", project=self.project)
  473. self.assertEqual(issue.status, EventStatus.UNRESOLVED)
  474. data = {"status": "resolved"}
  475. res = self.client.put(
  476. get_organization_issue_url(self.organization.slug, issue.pk),
  477. data,
  478. content_type="application/json",
  479. )
  480. self.assertEqual(res.status_code, 200)
  481. issue.refresh_from_db()
  482. self.assertEqual(issue.status, EventStatus.RESOLVED)
  483. def test_bulk_update(self):
  484. """Bulk update only supports Issue status"""
  485. issues = baker.make("issue_events.Issue", project=self.project, _quantity=2)
  486. url = f"{self.list_url}?id={issues[0].id}&id={issues[1].id}"
  487. status_to_set = EventStatus.RESOLVED
  488. data = {"status": status_to_set.label}
  489. res = self.client.put(url, data, content_type="application/json")
  490. self.assertContains(res, status_to_set.label)
  491. issues = Issue.objects.all()
  492. self.assertEqual(issues[0].status, status_to_set)
  493. self.assertEqual(issues[1].status, status_to_set)
  494. def test_bulk_delete_via_ids(self):
  495. """Bulk delete Issues with ids"""
  496. issues = baker.make("issue_events.Issue", project=self.project, _quantity=2)
  497. url = f"{self.list_url}?id={issues[0].id}&id={issues[1].id}"
  498. self.client.delete(url)
  499. issues = Issue.objects.all().count()
  500. self.assertEqual(issues, 0)
  501. def test_bulk_delete_via_search(self):
  502. """Bulk delete Issues via search string"""
  503. project2 = baker.make("projects.Project", organization=self.organization)
  504. project2.teams.add(self.team)
  505. issue1 = baker.make(Issue, project=self.project)
  506. issue2 = baker.make(Issue, project=project2)
  507. url = f"{self.list_url}?query=is:unresolved&project={self.project.id}"
  508. self.client.delete(url)
  509. self.assertFalse(Issue.objects.filter(id=issue1.id).exists())
  510. self.assertTrue(Issue.objects.filter(id=issue2.id).exists())
  511. def test_bulk_update_query(self):
  512. """Bulk update only supports Issue status"""
  513. project2 = baker.make("projects.Project", organization=self.organization)
  514. project2.teams.add(self.team)
  515. issue1 = baker.make(Issue, project=self.project)
  516. issue2 = baker.make(Issue, project=project2)
  517. url = f"{self.list_url}?query=is:unresolved&project={self.project.id}"
  518. status_to_set = EventStatus.RESOLVED
  519. data = {"status": status_to_set.label}
  520. res = self.client.put(url, data, content_type="application/json")
  521. self.assertContains(res, status_to_set.label)
  522. issue1.refresh_from_db()
  523. issue2.refresh_from_db()
  524. self.assertEqual(issue1.status, status_to_set)
  525. self.assertEqual(issue2.status, EventStatus.UNRESOLVED)
  526. class IssueEventAPIPermissionTestCase(APIPermissionTestCase):
  527. def setUp(self):
  528. self.create_org_team_project()
  529. self.set_client_credentials(self.auth_token.token)
  530. self.issue = baker.make("issue_events.Issue", project=self.project)
  531. self.list_url = reverse(
  532. "api:list_issues", kwargs={"organization_slug": self.organization.slug}
  533. )
  534. def test_list(self):
  535. self.assertGetReqStatusCode(self.list_url, 403)
  536. self.auth_token.add_permission("event:read")
  537. self.assertGetReqStatusCode(self.list_url, 200)
  538. class IssueEventTagsAPITestCase(GlitchTipTestCaseMixin, TestCase):
  539. def get_url(self, issue_id: int) -> str:
  540. return reverse("api:list_issue_tags", kwargs={"issue_id": issue_id})
  541. def setUp(self):
  542. super().create_logged_in_user()
  543. def test_issue_tags(self):
  544. issue = baker.make("issue_events.Issue", project=self.project)
  545. key_foo = baker.make("issue_events.TagKey", key="foo")
  546. key_animal = baker.make("issue_events.TagKey", key="animal")
  547. value_bar = baker.make("issue_events.TagValue", value="bar")
  548. value_cat = baker.make("issue_events.TagValue", value="cat")
  549. value_dog = baker.make("issue_events.TagValue", value="dog")
  550. baker.make(
  551. "issue_events.IssueTag",
  552. issue=issue,
  553. tag_key=key_foo,
  554. tag_value=value_bar,
  555. count=2,
  556. )
  557. baker.make(
  558. "issue_events.IssueTag",
  559. issue=issue,
  560. tag_key=key_foo,
  561. tag_value=value_bar,
  562. count=1,
  563. )
  564. baker.make(
  565. "issue_events.IssueTag",
  566. issue=issue,
  567. tag_key=key_animal,
  568. tag_value=value_cat,
  569. )
  570. baker.make(
  571. "issue_events.IssueTag",
  572. issue=issue,
  573. tag_key=key_animal,
  574. tag_value=value_dog,
  575. count=4,
  576. )
  577. baker.make(
  578. "issue_events.IssueTag",
  579. issue=issue,
  580. tag_key=key_foo,
  581. tag_value=value_cat,
  582. count=4,
  583. )
  584. url = self.get_url(issue.id)
  585. res = self.client.get(url)
  586. data = res.json()
  587. # Order is random
  588. if data[0]["name"] == "animal":
  589. animal = data[0]
  590. foo = data[1]
  591. else:
  592. animal = data[1]
  593. foo = data[0]
  594. self.assertEqual(animal["totalValues"], 5)
  595. self.assertEqual(animal["topValues"][0]["value"], "dog")
  596. self.assertEqual(animal["topValues"][0]["count"], 4)
  597. self.assertEqual(animal["uniqueValues"], 2)
  598. self.assertEqual(foo["totalValues"], 7)
  599. self.assertEqual(foo["topValues"][0]["value"], "cat")
  600. self.assertEqual(foo["topValues"][0]["count"], 4)
  601. self.assertEqual(foo["uniqueValues"], 2)
  602. def test_issue_tags_filter(self):
  603. issue = baker.make("issue_events.Issue", project=self.project)
  604. value_bar = baker.make("issue_events.TagValue", value="bar")
  605. baker.make(
  606. "issue_events.IssueTag",
  607. issue=issue,
  608. tag_key__key="foo",
  609. tag_value=value_bar,
  610. )
  611. baker.make(
  612. "issue_events.IssueTag",
  613. issue=issue,
  614. tag_key__key="lol",
  615. tag_value=value_bar,
  616. )
  617. baker.make(
  618. "issue_events.IssueEvent", issue=issue, tags={"foo": "bar", "lol": "bar"}
  619. )
  620. url = self.get_url(issue.id)
  621. res = self.client.get(url + "?key=foo")
  622. self.assertEqual(len(res.json()), 1)
  623. def test_issue_tags_performance(self):
  624. issue = baker.make("issue_events.Issue", project=self.project)
  625. key_foo = baker.make("issue_events.TagKey", key="foo")
  626. key_animal = baker.make("issue_events.TagKey", key="animal")
  627. value_bar = baker.make("issue_events.TagValue", value="bar")
  628. value_cat = baker.make("issue_events.TagValue", value="cat")
  629. value_dog = baker.make("issue_events.TagValue", value="dog")
  630. quantity = 2
  631. baker.make(
  632. "issue_events.IssueTag",
  633. issue=issue,
  634. tag_key=key_foo,
  635. tag_value=value_bar,
  636. count=5,
  637. _quantity=quantity,
  638. _bulk_create=True,
  639. )
  640. baker.make(
  641. "issue_events.IssueTag",
  642. tag_key=key_foo,
  643. tag_value=value_bar,
  644. _quantity=quantity,
  645. _bulk_create=True,
  646. )
  647. baker.make(
  648. "issue_events.IssueTag",
  649. issue=issue,
  650. tag_key=key_animal,
  651. tag_value=value_cat,
  652. count=5,
  653. _quantity=quantity,
  654. _bulk_create=True,
  655. )
  656. baker.make(
  657. "issue_events.IssueTag",
  658. _quantity=quantity,
  659. _bulk_create=True,
  660. )
  661. baker.make(
  662. "issue_events.IssueTag",
  663. issue=issue,
  664. tag_key=key_animal,
  665. tag_value=value_dog,
  666. count=5,
  667. _quantity=quantity,
  668. _bulk_create=True,
  669. )
  670. url = self.get_url(issue.id)
  671. with self.assertNumQueries(2): # Includes many auth related queries
  672. start = timer()
  673. self.client.get(url)
  674. end = timer()
  675. logger.info(end - start)