test_project_group_index.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516
  1. import time
  2. from datetime import timedelta
  3. from functools import cached_property
  4. from unittest.mock import Mock, patch
  5. from urllib.parse import quote
  6. from uuid import uuid4
  7. from django.conf import settings
  8. from django.utils import timezone
  9. from sentry.issues.grouptype import PerformanceSlowDBQueryGroupType
  10. from sentry.models import (
  11. Activity,
  12. ApiToken,
  13. ExternalIssue,
  14. Group,
  15. GroupAssignee,
  16. GroupBookmark,
  17. GroupHash,
  18. GroupLink,
  19. GroupResolution,
  20. GroupSeen,
  21. GroupShare,
  22. GroupSnooze,
  23. GroupStatus,
  24. GroupSubscription,
  25. GroupTombstone,
  26. Integration,
  27. OrganizationIntegration,
  28. Release,
  29. UserOption,
  30. )
  31. from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox
  32. from sentry.testutils import APITestCase, SnubaTestCase
  33. from sentry.testutils.helpers import parse_link_header
  34. from sentry.testutils.helpers.datetime import before_now, iso_format
  35. from sentry.testutils.silo import region_silo_test
  36. from sentry.types.activity import ActivityType
  37. from sentry.utils import json
  38. @region_silo_test
  39. class GroupListTest(APITestCase, SnubaTestCase):
  40. def setUp(self):
  41. super().setUp()
  42. self.min_ago = before_now(minutes=1)
  43. def _parse_links(self, header):
  44. # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
  45. links = {}
  46. for url, attrs in parse_link_header(header).items():
  47. links[attrs["rel"]] = attrs
  48. attrs["href"] = url
  49. return links
  50. @cached_property
  51. def path(self):
  52. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  53. def test_sort_by_date_with_tag(self):
  54. # XXX(dcramer): this tests a case where an ambiguous column name existed
  55. group1 = self.create_group(last_seen=before_now(seconds=1))
  56. self.login_as(user=self.user)
  57. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  58. assert response.status_code == 200
  59. assert len(response.data) == 1
  60. assert response.data[0]["id"] == str(group1.id)
  61. def test_invalid_query(self):
  62. self.create_group(last_seen=before_now(seconds=1))
  63. self.login_as(user=self.user)
  64. response = self.client.get(f"{self.path}?sort_by=date&query=timesSeen:>1t", format="json")
  65. assert response.status_code == 400
  66. assert "Error parsing search query" in response.data["detail"]
  67. def test_simple_pagination(self):
  68. event1 = self.store_event(
  69. data={
  70. "fingerprint": ["put-me-in-group-1"],
  71. "timestamp": iso_format(self.min_ago - timedelta(seconds=2)),
  72. },
  73. project_id=self.project.id,
  74. )
  75. event2 = self.store_event(
  76. data={
  77. "fingerprint": ["put-me-in-group-2"],
  78. "timestamp": iso_format(self.min_ago - timedelta(seconds=1)),
  79. },
  80. project_id=self.project.id,
  81. )
  82. self.login_as(user=self.user)
  83. response = self.client.get(f"{self.path}?sort_by=date&limit=1", format="json")
  84. assert response.status_code == 200
  85. assert len(response.data) == 1
  86. assert response.data[0]["id"] == str(event2.group.id)
  87. links = self._parse_links(response["Link"])
  88. assert links["previous"]["results"] == "false"
  89. assert links["next"]["results"] == "true"
  90. response = self.client.get(links["next"]["href"], format="json")
  91. assert response.status_code == 200
  92. assert len(response.data) == 1
  93. assert response.data[0]["id"] == str(event1.group.id)
  94. links = self._parse_links(response["Link"])
  95. assert links["previous"]["results"] == "true"
  96. assert links["next"]["results"] == "false"
  97. def test_stats_period(self):
  98. # TODO(dcramer): this test really only checks if validation happens
  99. # on statsPeriod
  100. self.create_group(last_seen=before_now(seconds=1))
  101. self.create_group(last_seen=timezone.now())
  102. self.login_as(user=self.user)
  103. response = self.client.get(f"{self.path}?statsPeriod=24h", format="json")
  104. assert response.status_code == 200
  105. response = self.client.get(f"{self.path}?statsPeriod=14d", format="json")
  106. assert response.status_code == 200
  107. response = self.client.get(f"{self.path}?statsPeriod=", format="json")
  108. assert response.status_code == 200
  109. time.sleep(1)
  110. response = self.client.get(f"{self.path}?statsPeriod=48h", format="json")
  111. assert response.status_code == 400
  112. def test_environment(self):
  113. self.store_event(
  114. data={
  115. "fingerprint": ["put-me-in-group1"],
  116. "timestamp": iso_format(self.min_ago),
  117. "environment": "production",
  118. },
  119. project_id=self.project.id,
  120. )
  121. self.store_event(
  122. data={
  123. "fingerprint": ["put-me-in-group2"],
  124. "timestamp": iso_format(self.min_ago),
  125. "environment": "staging",
  126. },
  127. project_id=self.project.id,
  128. )
  129. self.login_as(user=self.user)
  130. response = self.client.get(self.path + "?environment=production", format="json")
  131. assert response.status_code == 200
  132. assert len(response.data) == 1
  133. response = self.client.get(self.path + "?environment=garbage", format="json")
  134. assert response.status_code == 200
  135. assert len(response.data) == 0
  136. def test_auto_resolved(self):
  137. project = self.project
  138. project.update_option("sentry:resolve_age", 1)
  139. self.create_group(last_seen=before_now(days=1))
  140. group2 = self.create_group(last_seen=timezone.now())
  141. self.login_as(user=self.user)
  142. response = self.client.get(self.path, format="json")
  143. assert response.status_code == 200
  144. assert len(response.data) == 1
  145. assert response.data[0]["id"] == str(group2.id)
  146. def test_lookup_by_event_id(self):
  147. project = self.project
  148. project.update_option("sentry:resolve_age", 1)
  149. event_id = "c" * 32
  150. event = self.store_event(
  151. data={"event_id": event_id, "timestamp": iso_format(self.min_ago)},
  152. project_id=self.project.id,
  153. )
  154. self.login_as(user=self.user)
  155. response = self.client.get("{}?query={}".format(self.path, "c" * 32), format="json")
  156. assert response.status_code == 200
  157. assert len(response.data) == 1
  158. assert response.data[0]["id"] == str(event.group.id)
  159. assert response.data[0]["matchingEventId"] == event.event_id
  160. def test_lookup_by_event_with_matching_environment(self):
  161. project = self.project
  162. project.update_option("sentry:resolve_age", 1)
  163. self.create_environment(name="test", project=project)
  164. event = self.store_event(
  165. data={"environment": "test", "timestamp": iso_format(self.min_ago)},
  166. project_id=self.project.id,
  167. )
  168. self.login_as(user=self.user)
  169. response = self.client.get(
  170. f"{self.path}?query={event.event_id}&environment=test", format="json"
  171. )
  172. assert response.status_code == 200
  173. assert len(response.data) == 1
  174. assert response.data[0]["id"] == str(event.group.id)
  175. assert response.data[0]["matchingEventId"] == event.event_id
  176. assert response.data[0]["matchingEventEnvironment"] == "test"
  177. def test_lookup_by_event_id_with_whitespace(self):
  178. project = self.project
  179. project.update_option("sentry:resolve_age", 1)
  180. event = self.store_event(
  181. data={"event_id": "c" * 32, "timestamp": iso_format(self.min_ago)},
  182. project_id=self.project.id,
  183. )
  184. self.login_as(user=self.user)
  185. response = self.client.get(
  186. "{}?query=%20%20{}%20%20".format(self.path, "c" * 32), format="json"
  187. )
  188. assert response.status_code == 200
  189. assert len(response.data) == 1
  190. assert response.data[0]["id"] == str(event.group.id)
  191. def test_lookup_by_unknown_event_id(self):
  192. project = self.project
  193. project.update_option("sentry:resolve_age", 1)
  194. self.create_group()
  195. self.create_group()
  196. self.login_as(user=self.user)
  197. response = self.client.get("{}?query={}".format(self.path, "c" * 32), format="json")
  198. assert response.status_code == 200
  199. assert len(response.data) == 0
  200. def test_lookup_by_short_id(self):
  201. group = self.group
  202. short_id = group.qualified_short_id
  203. self.login_as(user=self.user)
  204. response = self.client.get(f"{self.path}?query={short_id}&shortIdLookup=1", format="json")
  205. assert response.status_code == 200
  206. assert len(response.data) == 1
  207. def test_lookup_by_short_id_no_perms(self):
  208. organization = self.create_organization()
  209. project = self.create_project(organization=organization)
  210. project2 = self.create_project(organization=organization)
  211. team = self.create_team(organization=organization)
  212. project2.add_team(team)
  213. group = self.create_group(project=project)
  214. user = self.create_user()
  215. self.create_member(organization=organization, user=user, teams=[team])
  216. short_id = group.qualified_short_id
  217. self.login_as(user=user)
  218. path = f"/api/0/projects/{organization.slug}/{project2.slug}/issues/"
  219. response = self.client.get(f"{path}?query={short_id}&shortIdLookup=1", format="json")
  220. assert response.status_code == 200
  221. assert len(response.data) == 0
  222. def test_lookup_by_first_release(self):
  223. self.login_as(self.user)
  224. project = self.project
  225. project2 = self.create_project(name="baz", organization=project.organization)
  226. release = Release.objects.create(organization=project.organization, version="12345")
  227. release.add_project(project)
  228. release.add_project(project2)
  229. group = self.store_event(
  230. data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
  231. project_id=project.id,
  232. ).group
  233. self.store_event(
  234. data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
  235. project_id=project2.id,
  236. )
  237. url = "{}?query={}".format(self.path, 'first-release:"%s"' % release.version)
  238. response = self.client.get(url, format="json")
  239. issues = json.loads(response.content)
  240. assert response.status_code == 200
  241. assert len(issues) == 1
  242. assert int(issues[0]["id"]) == group.id
  243. def test_lookup_by_release(self):
  244. self.login_as(self.user)
  245. version = "12345"
  246. event = self.store_event(
  247. data={"tags": {"sentry:release": version}}, project_id=self.project.id
  248. )
  249. group = event.group
  250. url = "{}?query={}".format(self.path, quote('release:"%s"' % version))
  251. response = self.client.get(url, format="json")
  252. issues = json.loads(response.content)
  253. assert response.status_code == 200
  254. assert len(issues) == 1
  255. assert int(issues[0]["id"]) == group.id
  256. def test_lookup_by_release_wildcard(self):
  257. self.login_as(self.user)
  258. version = "12345"
  259. event = self.store_event(
  260. data={"tags": {"sentry:release": version}}, project_id=self.project.id
  261. )
  262. group = event.group
  263. release_wildcard = version[:3] + "*"
  264. url = "{}?query={}".format(self.path, quote('release:"%s"' % release_wildcard))
  265. response = self.client.get(url, format="json")
  266. issues = json.loads(response.content)
  267. assert response.status_code == 200
  268. assert len(issues) == 1
  269. assert int(issues[0]["id"]) == group.id
  270. def test_pending_delete_pending_merge_excluded(self):
  271. self.create_group(status=GroupStatus.PENDING_DELETION)
  272. group = self.create_group()
  273. self.create_group(status=GroupStatus.DELETION_IN_PROGRESS)
  274. self.create_group(status=GroupStatus.PENDING_MERGE)
  275. self.login_as(user=self.user)
  276. response = self.client.get(self.path, format="json")
  277. assert len(response.data) == 1
  278. assert response.data[0]["id"] == str(group.id)
  279. def test_filters_based_on_retention(self):
  280. self.login_as(user=self.user)
  281. self.create_group(last_seen=timezone.now() - timedelta(days=2))
  282. with self.options({"system.event-retention-days": 1}):
  283. response = self.client.get(self.path)
  284. assert response.status_code == 200, response.content
  285. assert len(response.data) == 0
  286. def test_token_auth(self):
  287. token = ApiToken.objects.create(user=self.user, scopes=256)
  288. response = self.client.get(
  289. self.path, format="json", HTTP_AUTHORIZATION=f"Bearer {token.token}"
  290. )
  291. assert response.status_code == 200, response.content
  292. def test_filter_not_unresolved(self):
  293. event = self.store_event(
  294. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  295. project_id=self.project.id,
  296. )
  297. event.group.update(status=GroupStatus.RESOLVED)
  298. self.login_as(user=self.user)
  299. response = self.client.get(f"{self.path}?query=!is:unresolved", format="json")
  300. assert response.status_code == 200
  301. assert [int(r["id"]) for r in response.data] == [event.group.id]
  302. @region_silo_test
  303. class GroupUpdateTest(APITestCase, SnubaTestCase):
  304. def setUp(self):
  305. super().setUp()
  306. self.min_ago = timezone.now() - timedelta(minutes=1)
  307. @cached_property
  308. def path(self):
  309. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  310. def assertNoResolution(self, group):
  311. assert not GroupResolution.objects.filter(group=group).exists()
  312. def test_global_resolve(self):
  313. group1 = self.create_group(status=GroupStatus.RESOLVED)
  314. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  315. group3 = self.create_group(status=GroupStatus.IGNORED)
  316. group4 = self.create_group(
  317. project=self.create_project(slug="foo"),
  318. status=GroupStatus.UNRESOLVED,
  319. )
  320. self.login_as(user=self.user)
  321. response = self.client.put(
  322. f"{self.path}?status=unresolved", data={"status": "resolved"}, format="json"
  323. )
  324. assert response.status_code == 200, response.data
  325. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  326. # the previously resolved entry should not be included
  327. new_group1 = Group.objects.get(id=group1.id)
  328. assert new_group1.status == GroupStatus.RESOLVED
  329. assert new_group1.resolved_at is None
  330. # this wont exist because it wasn't affected
  331. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group1).exists()
  332. new_group2 = Group.objects.get(id=group2.id)
  333. assert new_group2.status == GroupStatus.RESOLVED
  334. assert new_group2.resolved_at is not None
  335. assert GroupSubscription.objects.filter(
  336. user_id=self.user.id, group=new_group2, is_active=True
  337. ).exists()
  338. # the ignored entry should not be included
  339. new_group3 = Group.objects.get(id=group3.id)
  340. assert new_group3.status == GroupStatus.IGNORED
  341. assert new_group3.resolved_at is None
  342. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group3)
  343. new_group4 = Group.objects.get(id=group4.id)
  344. assert new_group4.status == GroupStatus.UNRESOLVED
  345. assert new_group4.resolved_at is None
  346. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group4)
  347. def test_bulk_resolve(self):
  348. self.login_as(user=self.user)
  349. for i in range(200):
  350. self.create_group(status=GroupStatus.UNRESOLVED)
  351. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  352. assert len(response.data) == 100
  353. response = self.client.put(
  354. f"{self.path}?status=unresolved", data={"status": "resolved"}, format="json"
  355. )
  356. assert response.status_code == 200, response.data
  357. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  358. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  359. assert len(response.data) == 0
  360. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  361. def test_resolve_with_integration(self, mock_sync_status_outbound):
  362. self.login_as(user=self.user)
  363. org = self.organization
  364. integration = Integration.objects.create(provider="example", name="Example")
  365. integration.add_organization(org, self.user)
  366. group = self.create_group(status=GroupStatus.UNRESOLVED)
  367. OrganizationIntegration.objects.filter(
  368. integration_id=integration.id, organization_id=group.organization.id
  369. ).update(
  370. config={
  371. "sync_comments": True,
  372. "sync_status_outbound": True,
  373. "sync_status_inbound": True,
  374. "sync_assignee_outbound": True,
  375. "sync_assignee_inbound": True,
  376. }
  377. )
  378. external_issue = ExternalIssue.objects.get_or_create(
  379. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  380. )[0]
  381. GroupLink.objects.get_or_create(
  382. group_id=group.id,
  383. project_id=group.project_id,
  384. linked_type=GroupLink.LinkedType.issue,
  385. linked_id=external_issue.id,
  386. relationship=GroupLink.Relationship.references,
  387. )[0]
  388. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  389. assert len(response.data) == 1
  390. with self.tasks():
  391. with self.feature({"organizations:integrations-issue-sync": True}):
  392. response = self.client.put(
  393. f"{self.path}?status=unresolved",
  394. data={"status": "resolved"},
  395. format="json",
  396. )
  397. assert response.status_code == 200, response.data
  398. group = Group.objects.get(id=group.id)
  399. assert group.status == GroupStatus.RESOLVED
  400. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  401. mock_sync_status_outbound.assert_called_once_with(
  402. external_issue, True, group.project_id
  403. )
  404. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  405. assert len(response.data) == 0
  406. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  407. def test_set_unresolved_with_integration(self, mock_sync_status_outbound):
  408. release = self.create_release(project=self.project, version="abc")
  409. group = self.create_group(status=GroupStatus.RESOLVED)
  410. org = self.organization
  411. integration = Integration.objects.create(provider="example", name="Example")
  412. integration.add_organization(org, self.user)
  413. OrganizationIntegration.objects.filter(
  414. integration_id=integration.id, organization_id=group.organization.id
  415. ).update(
  416. config={
  417. "sync_comments": True,
  418. "sync_status_outbound": True,
  419. "sync_status_inbound": True,
  420. "sync_assignee_outbound": True,
  421. "sync_assignee_inbound": True,
  422. }
  423. )
  424. GroupResolution.objects.create(group=group, release=release)
  425. external_issue = ExternalIssue.objects.get_or_create(
  426. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  427. )[0]
  428. GroupLink.objects.get_or_create(
  429. group_id=group.id,
  430. project_id=group.project_id,
  431. linked_type=GroupLink.LinkedType.issue,
  432. linked_id=external_issue.id,
  433. relationship=GroupLink.Relationship.references,
  434. )[0]
  435. self.login_as(user=self.user)
  436. url = f"{self.path}?id={group.id}"
  437. with self.tasks():
  438. with self.feature({"organizations:integrations-issue-sync": True}):
  439. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  440. assert response.status_code == 200
  441. assert response.data == {"status": "unresolved", "statusDetails": {}}
  442. group = Group.objects.get(id=group.id)
  443. assert group.status == GroupStatus.UNRESOLVED
  444. self.assertNoResolution(group)
  445. assert GroupSubscription.objects.filter(
  446. user_id=self.user.id, group=group, is_active=True
  447. ).exists()
  448. mock_sync_status_outbound.assert_called_once_with(
  449. external_issue, False, group.project_id
  450. )
  451. def test_self_assign_issue(self):
  452. group = self.create_group(status=GroupStatus.UNRESOLVED)
  453. user = self.user
  454. uo1 = UserOption.objects.create(key="self_assign_issue", value="1", project=None, user=user)
  455. self.login_as(user=user)
  456. url = f"{self.path}?id={group.id}"
  457. response = self.client.put(url, data={"status": "resolved"}, format="json")
  458. assert response.status_code == 200, response.data
  459. assert response.data["assignedTo"]["id"] == str(user.id)
  460. assert response.data["assignedTo"]["type"] == "user"
  461. assert response.data["status"] == "resolved"
  462. assert GroupAssignee.objects.filter(group=group, user_id=user.id).exists()
  463. assert GroupSubscription.objects.filter(
  464. user_id=user.id, group=group, is_active=True
  465. ).exists()
  466. uo1.delete()
  467. def test_self_assign_issue_next_release(self):
  468. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  469. release.add_project(self.project)
  470. group = self.create_group(status=GroupStatus.UNRESOLVED)
  471. uo1 = UserOption.objects.create(
  472. key="self_assign_issue", value="1", project=None, user=self.user
  473. )
  474. self.login_as(user=self.user)
  475. url = f"{self.path}?id={group.id}"
  476. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  477. assert response.status_code == 200
  478. assert response.data["status"] == "resolved"
  479. assert response.data["statusDetails"]["inNextRelease"]
  480. assert response.data["assignedTo"]["id"] == str(self.user.id)
  481. assert response.data["assignedTo"]["type"] == "user"
  482. group = Group.objects.get(id=group.id)
  483. assert group.status == GroupStatus.RESOLVED
  484. assert GroupResolution.objects.filter(group=group, release=release).exists()
  485. assert GroupSubscription.objects.filter(
  486. user_id=self.user.id, group=group, is_active=True
  487. ).exists()
  488. activity = Activity.objects.get(
  489. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  490. )
  491. assert activity.data["version"] == ""
  492. uo1.delete()
  493. def test_selective_status_update(self):
  494. group1 = self.create_group(status=GroupStatus.RESOLVED)
  495. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  496. group3 = self.create_group(status=GroupStatus.IGNORED)
  497. group4 = self.create_group(
  498. project=self.create_project(slug="foo"),
  499. status=GroupStatus.UNRESOLVED,
  500. )
  501. self.login_as(user=self.user)
  502. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  503. response = self.client.put(url, data={"status": "resolved"}, format="json")
  504. assert response.status_code == 200
  505. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  506. new_group1 = Group.objects.get(id=group1.id)
  507. assert new_group1.resolved_at is not None
  508. assert new_group1.status == GroupStatus.RESOLVED
  509. new_group2 = Group.objects.get(id=group2.id)
  510. assert new_group2.resolved_at is not None
  511. assert new_group2.status == GroupStatus.RESOLVED
  512. assert GroupSubscription.objects.filter(
  513. user_id=self.user.id, group=new_group2, is_active=True
  514. ).exists()
  515. new_group3 = Group.objects.get(id=group3.id)
  516. assert new_group3.resolved_at is None
  517. assert new_group3.status == GroupStatus.IGNORED
  518. new_group4 = Group.objects.get(id=group4.id)
  519. assert new_group4.resolved_at is None
  520. assert new_group4.status == GroupStatus.UNRESOLVED
  521. def test_set_resolved_in_current_release(self):
  522. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  523. release.add_project(self.project)
  524. group = self.create_group(status=GroupStatus.UNRESOLVED)
  525. self.login_as(user=self.user)
  526. url = f"{self.path}?id={group.id}"
  527. response = self.client.put(
  528. url,
  529. data={"status": "resolved", "statusDetails": {"inRelease": "latest"}},
  530. format="json",
  531. )
  532. assert response.status_code == 200
  533. assert response.data["status"] == "resolved"
  534. assert response.data["statusDetails"]["inRelease"] == release.version
  535. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  536. assert "activity" in response.data
  537. group = Group.objects.get(id=group.id)
  538. assert group.status == GroupStatus.RESOLVED
  539. resolution = GroupResolution.objects.get(group=group)
  540. assert resolution.release == release
  541. assert resolution.type == GroupResolution.Type.in_release
  542. assert resolution.status == GroupResolution.Status.resolved
  543. assert resolution.actor_id == self.user.id
  544. assert GroupSubscription.objects.filter(
  545. user_id=self.user.id, group=group, is_active=True
  546. ).exists()
  547. activity = Activity.objects.get(
  548. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  549. )
  550. assert activity.data["version"] == release.version
  551. def test_set_resolved_in_explicit_release(self):
  552. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  553. release.add_project(self.project)
  554. release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
  555. release2.add_project(self.project)
  556. group = self.create_group(status=GroupStatus.UNRESOLVED)
  557. self.login_as(user=self.user)
  558. url = f"{self.path}?id={group.id}"
  559. response = self.client.put(
  560. url,
  561. data={"status": "resolved", "statusDetails": {"inRelease": release.version}},
  562. format="json",
  563. )
  564. assert response.status_code == 200
  565. assert response.data["status"] == "resolved"
  566. assert response.data["statusDetails"]["inRelease"] == release.version
  567. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  568. assert "activity" in response.data
  569. group = Group.objects.get(id=group.id)
  570. assert group.status == GroupStatus.RESOLVED
  571. resolution = GroupResolution.objects.get(group=group)
  572. assert resolution.release == release
  573. assert resolution.type == GroupResolution.Type.in_release
  574. assert resolution.status == GroupResolution.Status.resolved
  575. assert resolution.actor_id == self.user.id
  576. assert GroupSubscription.objects.filter(
  577. user_id=self.user.id, group=group, is_active=True
  578. ).exists()
  579. activity = Activity.objects.get(
  580. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  581. )
  582. assert activity.data["version"] == release.version
  583. def test_set_resolved_in_next_release(self):
  584. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  585. release.add_project(self.project)
  586. group = self.create_group(status=GroupStatus.UNRESOLVED)
  587. self.login_as(user=self.user)
  588. url = f"{self.path}?id={group.id}"
  589. response = self.client.put(
  590. url,
  591. data={"status": "resolved", "statusDetails": {"inNextRelease": True}},
  592. format="json",
  593. )
  594. assert response.status_code == 200
  595. assert response.data["status"] == "resolved"
  596. assert response.data["statusDetails"]["inNextRelease"]
  597. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  598. assert "activity" in response.data
  599. group = Group.objects.get(id=group.id)
  600. assert group.status == GroupStatus.RESOLVED
  601. resolution = GroupResolution.objects.get(group=group)
  602. assert resolution.release == release
  603. assert resolution.type == GroupResolution.Type.in_next_release
  604. assert resolution.status == GroupResolution.Status.pending
  605. assert resolution.actor_id == self.user.id
  606. assert GroupSubscription.objects.filter(
  607. user_id=self.user.id, group=group, is_active=True
  608. ).exists()
  609. activity = Activity.objects.get(
  610. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  611. )
  612. assert activity.data["version"] == ""
  613. def test_set_resolved_in_next_release_legacy(self):
  614. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  615. release.add_project(self.project)
  616. group = self.create_group(status=GroupStatus.UNRESOLVED)
  617. self.login_as(user=self.user)
  618. url = f"{self.path}?id={group.id}"
  619. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  620. assert response.status_code == 200
  621. assert response.data["status"] == "resolved"
  622. assert response.data["statusDetails"]["inNextRelease"]
  623. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  624. group = Group.objects.get(id=group.id)
  625. assert group.status == GroupStatus.RESOLVED
  626. resolution = GroupResolution.objects.get(group=group)
  627. assert resolution.release == release
  628. assert resolution.type == GroupResolution.Type.in_next_release
  629. assert resolution.status == GroupResolution.Status.pending
  630. assert resolution.actor_id == self.user.id
  631. assert GroupSubscription.objects.filter(
  632. user_id=self.user.id, group=group, is_active=True
  633. ).exists()
  634. activity = Activity.objects.get(
  635. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  636. )
  637. assert activity.data["version"] == ""
  638. def test_set_resolved_in_explicit_commit_unreleased(self):
  639. repo = self.create_repo(project=self.project, name=self.project.name)
  640. commit = self.create_commit(project=self.project, repo=repo)
  641. group = self.create_group(status=GroupStatus.UNRESOLVED)
  642. self.login_as(user=self.user)
  643. url = f"{self.path}?id={group.id}"
  644. response = self.client.put(
  645. url,
  646. data={
  647. "status": "resolved",
  648. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  649. },
  650. format="json",
  651. )
  652. assert response.status_code == 200
  653. assert response.data["status"] == "resolved"
  654. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  655. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  656. group = Group.objects.get(id=group.id)
  657. assert group.status == GroupStatus.RESOLVED
  658. link = GroupLink.objects.get(group_id=group.id)
  659. assert link.linked_type == GroupLink.LinkedType.commit
  660. assert link.relationship == GroupLink.Relationship.resolves
  661. assert link.linked_id == commit.id
  662. assert GroupSubscription.objects.filter(
  663. user_id=self.user.id, group=group, is_active=True
  664. ).exists()
  665. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  666. assert activity.data["commit"] == commit.id
  667. def test_set_resolved_in_explicit_commit_released(self):
  668. release = self.create_release(project=self.project)
  669. repo = self.create_repo(project=self.project, name=self.project.name)
  670. commit = self.create_commit(project=self.project, repo=repo, release=release)
  671. group = self.create_group(status=GroupStatus.UNRESOLVED)
  672. self.login_as(user=self.user)
  673. url = f"{self.path}?id={group.id}"
  674. response = self.client.put(
  675. url,
  676. data={
  677. "status": "resolved",
  678. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  679. },
  680. format="json",
  681. )
  682. assert response.status_code == 200
  683. assert response.data["status"] == "resolved"
  684. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  685. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  686. group = Group.objects.get(id=group.id)
  687. assert group.status == GroupStatus.RESOLVED
  688. link = GroupLink.objects.get(group_id=group.id)
  689. assert link.project_id == self.project.id
  690. assert link.linked_type == GroupLink.LinkedType.commit
  691. assert link.relationship == GroupLink.Relationship.resolves
  692. assert link.linked_id == commit.id
  693. assert GroupSubscription.objects.filter(
  694. user_id=self.user.id, group=group, is_active=True
  695. ).exists()
  696. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  697. assert activity.data["commit"] == commit.id
  698. resolution = GroupResolution.objects.get(group=group)
  699. assert resolution.type == GroupResolution.Type.in_release
  700. assert resolution.status == GroupResolution.Status.resolved
  701. def test_set_resolved_in_explicit_commit_missing(self):
  702. repo = self.create_repo(project=self.project, name=self.project.name)
  703. group = self.create_group(status=GroupStatus.UNRESOLVED)
  704. self.login_as(user=self.user)
  705. url = f"{self.path}?id={group.id}"
  706. response = self.client.put(
  707. url,
  708. data={
  709. "status": "resolved",
  710. "statusDetails": {"inCommit": {"commit": "a" * 40, "repository": repo.name}},
  711. },
  712. format="json",
  713. )
  714. assert response.status_code == 400
  715. assert (
  716. response.data["statusDetails"]["inCommit"]["commit"][0]
  717. == "Unable to find the given commit."
  718. )
  719. def test_set_unresolved(self):
  720. release = self.create_release(project=self.project, version="abc")
  721. group = self.create_group(status=GroupStatus.RESOLVED)
  722. GroupResolution.objects.create(group=group, release=release)
  723. self.login_as(user=self.user)
  724. url = f"{self.path}?id={group.id}"
  725. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  726. assert response.status_code == 200
  727. assert response.data == {"status": "unresolved", "statusDetails": {}}
  728. group = Group.objects.get(id=group.id)
  729. assert group.status == GroupStatus.UNRESOLVED
  730. self.assertNoResolution(group)
  731. assert GroupSubscription.objects.filter(
  732. user_id=self.user.id, group=group, is_active=True
  733. ).exists()
  734. def test_set_unresolved_on_snooze(self):
  735. group = self.create_group(status=GroupStatus.IGNORED)
  736. GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(days=1))
  737. self.login_as(user=self.user)
  738. url = f"{self.path}?id={group.id}"
  739. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  740. assert response.status_code == 200
  741. assert response.data == {"status": "unresolved", "statusDetails": {}}
  742. group = Group.objects.get(id=group.id)
  743. assert group.status == GroupStatus.UNRESOLVED
  744. def test_basic_ignore(self):
  745. group = self.create_group(status=GroupStatus.RESOLVED)
  746. snooze = GroupSnooze.objects.create(group=group, until=timezone.now())
  747. self.login_as(user=self.user)
  748. url = f"{self.path}?id={group.id}"
  749. response = self.client.put(url, data={"status": "ignored"}, format="json")
  750. assert response.status_code == 200
  751. # existing snooze objects should be cleaned up
  752. assert not GroupSnooze.objects.filter(id=snooze.id).exists()
  753. group = Group.objects.get(id=group.id)
  754. assert group.status == GroupStatus.IGNORED
  755. assert response.data == {"status": "ignored", "statusDetails": {}, "inbox": None}
  756. def test_snooze_duration(self):
  757. group = self.create_group(status=GroupStatus.RESOLVED)
  758. self.login_as(user=self.user)
  759. url = f"{self.path}?id={group.id}"
  760. response = self.client.put(
  761. url, data={"status": "ignored", "ignoreDuration": 30}, format="json"
  762. )
  763. assert response.status_code == 200
  764. snooze = GroupSnooze.objects.get(group=group)
  765. now = timezone.now()
  766. assert snooze.count is None
  767. assert snooze.until > now + timedelta(minutes=29)
  768. assert snooze.until < now + timedelta(minutes=31)
  769. assert snooze.user_count is None
  770. assert snooze.user_window is None
  771. assert snooze.window is None
  772. assert response.data["status"] == "ignored"
  773. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  774. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  775. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  776. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  777. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  778. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  779. def test_snooze_count(self):
  780. group = self.create_group(status=GroupStatus.RESOLVED, times_seen=1)
  781. self.login_as(user=self.user)
  782. url = f"{self.path}?id={group.id}"
  783. response = self.client.put(
  784. url, data={"status": "ignored", "ignoreCount": 100}, format="json"
  785. )
  786. assert response.status_code == 200
  787. snooze = GroupSnooze.objects.get(group=group)
  788. assert snooze.count == 100
  789. assert snooze.until is None
  790. assert snooze.user_count is None
  791. assert snooze.user_window is None
  792. assert snooze.window is None
  793. assert snooze.state["times_seen"] == 1
  794. assert response.data["status"] == "ignored"
  795. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  796. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  797. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  798. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  799. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  800. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  801. def test_snooze_user_count(self):
  802. event = {}
  803. for i in range(10):
  804. event = self.store_event(
  805. data={
  806. "fingerprint": ["put-me-in-group-1"],
  807. "user": {"id": str(i)},
  808. "timestamp": iso_format(self.min_ago + timedelta(seconds=i)),
  809. },
  810. project_id=self.project.id,
  811. )
  812. group = Group.objects.get(id=event.group.id)
  813. group.status = GroupStatus.RESOLVED
  814. group.save()
  815. self.login_as(user=self.user)
  816. url = f"{self.path}?id={group.id}"
  817. response = self.client.put(
  818. url, data={"status": "ignored", "ignoreUserCount": 10}, format="json"
  819. )
  820. assert response.status_code == 200
  821. snooze = GroupSnooze.objects.get(group=group)
  822. assert snooze.count is None
  823. assert snooze.until is None
  824. assert snooze.user_count == 10
  825. assert snooze.user_window is None
  826. assert snooze.window is None
  827. assert snooze.state["users_seen"] == 10
  828. assert response.data["status"] == "ignored"
  829. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  830. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  831. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  832. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  833. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  834. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  835. def test_set_bookmarked(self):
  836. group1 = self.create_group(status=GroupStatus.RESOLVED)
  837. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  838. group3 = self.create_group(status=GroupStatus.IGNORED)
  839. group4 = self.create_group(
  840. project=self.create_project(slug="foo"),
  841. status=GroupStatus.UNRESOLVED,
  842. )
  843. self.login_as(user=self.user)
  844. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  845. response = self.client.put(url, data={"isBookmarked": "true"}, format="json")
  846. assert response.status_code == 200
  847. assert response.data == {"isBookmarked": True}
  848. bookmark1 = GroupBookmark.objects.filter(group=group1, user_id=self.user.id)
  849. assert bookmark1.exists()
  850. assert GroupSubscription.objects.filter(
  851. user_id=self.user.id, group=group1, is_active=True
  852. ).exists()
  853. bookmark2 = GroupBookmark.objects.filter(group=group2, user_id=self.user.id)
  854. assert bookmark2.exists()
  855. assert GroupSubscription.objects.filter(
  856. user_id=self.user.id, group=group2, is_active=True
  857. ).exists()
  858. bookmark3 = GroupBookmark.objects.filter(group=group3, user_id=self.user.id)
  859. assert not bookmark3.exists()
  860. bookmark4 = GroupBookmark.objects.filter(group=group4, user_id=self.user.id)
  861. assert not bookmark4.exists()
  862. def test_subscription(self):
  863. group1 = self.create_group()
  864. group2 = self.create_group()
  865. group3 = self.create_group()
  866. group4 = self.create_group(project=self.create_project(slug="foo"))
  867. self.login_as(user=self.user)
  868. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  869. response = self.client.put(url, data={"isSubscribed": "true"}, format="json")
  870. assert response.status_code == 200
  871. assert response.data == {"isSubscribed": True, "subscriptionDetails": {"reason": "unknown"}}
  872. assert GroupSubscription.objects.filter(
  873. group=group1, user_id=self.user.id, is_active=True
  874. ).exists()
  875. assert GroupSubscription.objects.filter(
  876. group=group2, user_id=self.user.id, is_active=True
  877. ).exists()
  878. assert not GroupSubscription.objects.filter(group=group3, user_id=self.user.id).exists()
  879. assert not GroupSubscription.objects.filter(group=group4, user_id=self.user.id).exists()
  880. def test_set_public(self):
  881. group1 = self.create_group()
  882. group2 = self.create_group()
  883. self.login_as(user=self.user)
  884. url = f"{self.path}?id={group1.id}&id={group2.id}"
  885. response = self.client.put(url, data={"isPublic": "true"}, format="json")
  886. assert response.status_code == 200
  887. assert response.data["isPublic"] is True
  888. assert "shareId" in response.data
  889. new_group1 = Group.objects.get(id=group1.id)
  890. assert bool(new_group1.get_share_id())
  891. new_group2 = Group.objects.get(id=group2.id)
  892. assert bool(new_group2.get_share_id())
  893. def test_set_private(self):
  894. group1 = self.create_group()
  895. group2 = self.create_group()
  896. # Manually mark them as shared
  897. for g in group1, group2:
  898. GroupShare.objects.create(project_id=g.project_id, group=g)
  899. assert bool(g.get_share_id())
  900. self.login_as(user=self.user)
  901. url = f"{self.path}?id={group1.id}&id={group2.id}"
  902. response = self.client.put(url, data={"isPublic": "false"}, format="json")
  903. assert response.status_code == 200
  904. assert response.data == {"isPublic": False, "shareId": None}
  905. new_group1 = Group.objects.get(id=group1.id)
  906. assert not bool(new_group1.get_share_id())
  907. new_group2 = Group.objects.get(id=group2.id)
  908. assert not bool(new_group2.get_share_id())
  909. def test_set_has_seen(self):
  910. group1 = self.create_group(status=GroupStatus.RESOLVED)
  911. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  912. group3 = self.create_group(status=GroupStatus.IGNORED)
  913. group4 = self.create_group(
  914. project=self.create_project(slug="foo"),
  915. status=GroupStatus.UNRESOLVED,
  916. )
  917. self.login_as(user=self.user)
  918. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  919. response = self.client.put(url, data={"hasSeen": "true"}, format="json")
  920. assert response.status_code == 200
  921. assert response.data == {"hasSeen": True}
  922. r1 = GroupSeen.objects.filter(group=group1, user_id=self.user.id)
  923. assert r1.exists()
  924. r2 = GroupSeen.objects.filter(group=group2, user_id=self.user.id)
  925. assert r2.exists()
  926. r3 = GroupSeen.objects.filter(group=group3, user_id=self.user.id)
  927. assert not r3.exists()
  928. r4 = GroupSeen.objects.filter(group=group4, user_id=self.user.id)
  929. assert not r4.exists()
  930. def test_inbox_fields(self):
  931. group1 = self.create_group(status=GroupStatus.RESOLVED)
  932. add_group_to_inbox(group1, GroupInboxReason.NEW)
  933. self.login_as(user=self.user)
  934. url = f"{self.path}?id={group1.id}"
  935. response = self.client.put(url, data={"status": "resolved"}, format="json")
  936. assert "inbox" in response.data
  937. assert response.data["inbox"] is None
  938. @patch("sentry.api.helpers.group_index.update.uuid4")
  939. @patch("sentry.api.helpers.group_index.update.merge_groups")
  940. @patch("sentry.api.helpers.group_index.update.eventstream")
  941. def test_merge(self, mock_eventstream, merge_groups, mock_uuid4):
  942. eventstream_state = object()
  943. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  944. mock_uuid4.return_value = self.get_mock_uuid()
  945. group1 = self.create_group(times_seen=1)
  946. group2 = self.create_group(times_seen=50)
  947. group3 = self.create_group(times_seen=2)
  948. self.create_group()
  949. self.login_as(user=self.user)
  950. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  951. response = self.client.put(url, data={"merge": "1"}, format="json")
  952. assert response.status_code == 200
  953. assert response.data["merge"]["parent"] == str(group2.id)
  954. assert sorted(response.data["merge"]["children"]) == sorted(
  955. [str(group1.id), str(group3.id)]
  956. )
  957. mock_eventstream.start_merge.assert_called_once_with(
  958. group1.project_id, [group3.id, group1.id], group2.id
  959. )
  960. assert len(merge_groups.mock_calls) == 1
  961. merge_groups.delay.assert_any_call(
  962. from_object_ids=[group3.id, group1.id],
  963. to_object_id=group2.id,
  964. transaction_id="abc123",
  965. eventstream_state=eventstream_state,
  966. )
  967. @patch("sentry.api.helpers.group_index.update.uuid4")
  968. @patch("sentry.api.helpers.group_index.update.merge_groups")
  969. @patch("sentry.api.helpers.group_index.update.eventstream")
  970. def test_merge_performance_issues(self, mock_eventstream, merge_groups, mock_uuid4):
  971. eventstream_state = object()
  972. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  973. mock_uuid4.return_value = self.get_mock_uuid()
  974. group1 = self.create_group(times_seen=1, type=PerformanceSlowDBQueryGroupType.type_id)
  975. group2 = self.create_group(times_seen=50, type=PerformanceSlowDBQueryGroupType.type_id)
  976. group3 = self.create_group(times_seen=2, type=PerformanceSlowDBQueryGroupType.type_id)
  977. self.create_group()
  978. self.login_as(user=self.user)
  979. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  980. response = self.client.put(url, data={"merge": "1"}, format="json")
  981. assert response.status_code == 400, response.content
  982. def test_assign(self):
  983. group1 = self.create_group(is_public=True)
  984. group2 = self.create_group(is_public=True)
  985. user = self.user
  986. self.login_as(user=user)
  987. url = f"{self.path}?id={group1.id}"
  988. response = self.client.put(url, data={"assignedTo": user.username})
  989. assert response.status_code == 200
  990. assert response.data["assignedTo"]["id"] == str(user.id)
  991. assert response.data["assignedTo"]["type"] == "user"
  992. assert GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
  993. assert not GroupAssignee.objects.filter(group=group2, user_id=user.id).exists()
  994. assert (
  995. Activity.objects.filter(
  996. group=group1, user=user, type=ActivityType.ASSIGNED.value
  997. ).count()
  998. == 1
  999. )
  1000. assert GroupSubscription.objects.filter(
  1001. user_id=user.id, group=group1, is_active=True
  1002. ).exists()
  1003. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1004. assert response.status_code == 200, response.content
  1005. assert response.data["assignedTo"] is None
  1006. assert not GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
  1007. def test_assign_non_member(self):
  1008. group = self.create_group(is_public=True)
  1009. member = self.user
  1010. non_member = self.create_user("bar@example.com")
  1011. self.login_as(user=member)
  1012. url = f"{self.path}?id={group.id}"
  1013. response = self.client.put(url, data={"assignedTo": non_member.username}, format="json")
  1014. assert response.status_code == 400, response.content
  1015. def test_assign_team(self):
  1016. self.login_as(user=self.user)
  1017. group = self.create_group()
  1018. other_member = self.create_user("bar@example.com")
  1019. team = self.create_team(
  1020. organization=group.project.organization, members=[self.user, other_member]
  1021. )
  1022. group.project.add_team(team)
  1023. url = f"{self.path}?id={group.id}"
  1024. response = self.client.put(url, data={"assignedTo": f"team:{team.id}"})
  1025. assert response.status_code == 200
  1026. assert response.data["assignedTo"]["id"] == str(team.id)
  1027. assert response.data["assignedTo"]["type"] == "team"
  1028. assert GroupAssignee.objects.filter(group=group, team=team).exists()
  1029. assert Activity.objects.filter(group=group, type=ActivityType.ASSIGNED.value).count() == 1
  1030. assert GroupSubscription.objects.filter(group=group, is_active=True).count() == 2
  1031. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1032. assert response.status_code == 200, response.content
  1033. assert response.data["assignedTo"] is None
  1034. def test_discard(self):
  1035. group1 = self.create_group(is_public=True)
  1036. group2 = self.create_group(is_public=True)
  1037. group_hash = GroupHash.objects.create(hash="x" * 32, project=group1.project, group=group1)
  1038. user = self.user
  1039. self.login_as(user=user)
  1040. url = f"{self.path}?id={group1.id}"
  1041. with self.tasks():
  1042. with self.feature("projects:discard-groups"):
  1043. response = self.client.put(url, data={"discard": True})
  1044. assert response.status_code == 204
  1045. assert not Group.objects.filter(id=group1.id).exists()
  1046. assert Group.objects.filter(id=group2.id).exists()
  1047. assert GroupHash.objects.filter(id=group_hash.id).exists()
  1048. tombstone = GroupTombstone.objects.get(
  1049. id=GroupHash.objects.get(id=group_hash.id).group_tombstone_id
  1050. )
  1051. assert tombstone.message == group1.message
  1052. assert tombstone.culprit == group1.culprit
  1053. assert tombstone.project == group1.project
  1054. assert tombstone.data == group1.data
  1055. @patch(
  1056. "sentry.models.OrganizationMember.get_scopes",
  1057. return_value=frozenset(s for s in settings.SENTRY_SCOPES if s != "event:admin"),
  1058. )
  1059. def test_discard_requires_events_admin(self, mock_get_scopes):
  1060. group1 = self.create_group(is_public=True)
  1061. user = self.user
  1062. self.login_as(user=user)
  1063. url = f"{self.path}?id={group1.id}"
  1064. with self.tasks(), self.feature("projects:discard-groups"):
  1065. response = self.client.put(url, data={"discard": True})
  1066. assert response.status_code == 400
  1067. assert Group.objects.filter(id=group1.id).exists()
  1068. @region_silo_test
  1069. class GroupDeleteTest(APITestCase, SnubaTestCase):
  1070. @cached_property
  1071. def path(self):
  1072. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  1073. @patch("sentry.api.helpers.group_index.delete.eventstream")
  1074. @patch("sentry.eventstream")
  1075. def test_delete_by_id(self, mock_eventstream_task, mock_eventstream_api):
  1076. eventstream_state = {"event_stream_state": uuid4()}
  1077. mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
  1078. group1 = self.create_group(status=GroupStatus.RESOLVED)
  1079. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  1080. group3 = self.create_group(status=GroupStatus.IGNORED)
  1081. group4 = self.create_group(
  1082. project=self.create_project(slug="foo"),
  1083. status=GroupStatus.UNRESOLVED,
  1084. )
  1085. hashes = []
  1086. for g in group1, group2, group3, group4:
  1087. hash = uuid4().hex
  1088. hashes.append(hash)
  1089. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1090. self.login_as(user=self.user)
  1091. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  1092. response = self.client.delete(url, format="json")
  1093. mock_eventstream_api.start_delete_groups.assert_called_once_with(
  1094. group1.project_id, [group1.id, group2.id]
  1095. )
  1096. assert response.status_code == 204
  1097. assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
  1098. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1099. assert Group.objects.get(id=group2.id).status == GroupStatus.PENDING_DELETION
  1100. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1101. assert Group.objects.get(id=group3.id).status != GroupStatus.PENDING_DELETION
  1102. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1103. assert Group.objects.get(id=group4.id).status != GroupStatus.PENDING_DELETION
  1104. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1105. Group.objects.filter(id__in=(group1.id, group2.id)).update(status=GroupStatus.UNRESOLVED)
  1106. with self.tasks():
  1107. response = self.client.delete(url, format="json")
  1108. mock_eventstream_task.end_delete_groups.assert_called_once_with(eventstream_state)
  1109. assert response.status_code == 204
  1110. assert not Group.objects.filter(id=group1.id).exists()
  1111. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1112. assert not Group.objects.filter(id=group2.id).exists()
  1113. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1114. assert Group.objects.filter(id=group3.id).exists()
  1115. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1116. assert Group.objects.filter(id=group4.id).exists()
  1117. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1118. @patch("sentry.api.helpers.group_index.delete.eventstream")
  1119. @patch("sentry.eventstream")
  1120. def test_delete_performance_issue_by_id(self, mock_eventstream_task, mock_eventstream_api):
  1121. eventstream_state = {"event_stream_state": uuid4()}
  1122. mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
  1123. group1 = self.create_group(
  1124. status=GroupStatus.RESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
  1125. )
  1126. group2 = self.create_group(
  1127. status=GroupStatus.UNRESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
  1128. )
  1129. hashes = []
  1130. for g in group1, group2:
  1131. hash = uuid4().hex
  1132. hashes.append(hash)
  1133. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1134. self.login_as(user=self.user)
  1135. url = f"{self.path}?id={group1.id}&id={group2.id}"
  1136. response = self.client.delete(url, format="json")
  1137. assert response.status_code == 400
  1138. assert Group.objects.filter(id=group1.id).exists()
  1139. assert GroupHash.objects.filter(group_id=group1.id).exists()
  1140. assert Group.objects.filter(id=group2.id).exists()
  1141. assert GroupHash.objects.filter(group_id=group2.id).exists()
  1142. def test_bulk_delete(self):
  1143. groups = []
  1144. for i in range(10, 41):
  1145. groups.append(
  1146. self.create_group(
  1147. project=self.project,
  1148. status=GroupStatus.RESOLVED,
  1149. )
  1150. )
  1151. hashes = []
  1152. for group in groups:
  1153. hash = uuid4().hex
  1154. hashes.append(hash)
  1155. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1156. self.login_as(user=self.user)
  1157. # if query is '' it defaults to is:unresolved
  1158. url = self.path + "?query="
  1159. response = self.client.delete(url, format="json")
  1160. assert response.status_code == 204
  1161. for group in groups:
  1162. assert Group.objects.get(id=group.id).status == GroupStatus.PENDING_DELETION
  1163. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1164. Group.objects.filter(id__in=[group.id for group in groups]).update(
  1165. status=GroupStatus.UNRESOLVED
  1166. )
  1167. with self.tasks():
  1168. response = self.client.delete(url, format="json")
  1169. assert response.status_code == 204
  1170. for group in groups:
  1171. assert not Group.objects.filter(id=group.id).exists()
  1172. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1173. def test_bulk_delete_performance_issues(self):
  1174. groups = []
  1175. for i in range(10, 41):
  1176. groups.append(
  1177. self.create_group(
  1178. project=self.project,
  1179. status=GroupStatus.RESOLVED,
  1180. type=PerformanceSlowDBQueryGroupType.type_id,
  1181. )
  1182. )
  1183. hashes = []
  1184. for group in groups:
  1185. hash = uuid4().hex
  1186. hashes.append(hash)
  1187. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1188. self.login_as(user=self.user)
  1189. # if query is '' it defaults to is:unresolved
  1190. url = self.path + "?query="
  1191. response = self.client.delete(url, format="json")
  1192. assert response.status_code == 400
  1193. for group in groups:
  1194. assert Group.objects.filter(id=group.id).exists()
  1195. assert GroupHash.objects.filter(group_id=group.id).exists()