test_issues_api.py 27 KB

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