test_project_group_index.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513
  1. import time
  2. from datetime import timedelta
  3. from unittest.mock import Mock, patch
  4. from urllib.parse import quote
  5. from uuid import uuid4
  6. from django.conf import settings
  7. from django.utils import timezone
  8. from exam import fixture
  9. from sentry.models import (
  10. Activity,
  11. ApiToken,
  12. ExternalIssue,
  13. Group,
  14. GroupAssignee,
  15. GroupBookmark,
  16. GroupHash,
  17. GroupLink,
  18. GroupResolution,
  19. GroupSeen,
  20. GroupShare,
  21. GroupSnooze,
  22. GroupStatus,
  23. GroupSubscription,
  24. GroupTombstone,
  25. Integration,
  26. OrganizationIntegration,
  27. Release,
  28. UserOption,
  29. )
  30. from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox
  31. from sentry.testutils import APITestCase, SnubaTestCase
  32. from sentry.testutils.helpers import parse_link_header
  33. from sentry.testutils.helpers.datetime import before_now, iso_format
  34. from sentry.testutils.silo import region_silo_test
  35. from sentry.types.activity import ActivityType
  36. from sentry.types.issues import GroupType
  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. @fixture
  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. @fixture
  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=self.user, 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=self.user, 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=self.user, 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=self.user, 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=self.user, 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=user).exists()
  463. assert GroupSubscription.objects.filter(user=user, group=group, is_active=True).exists()
  464. uo1.delete()
  465. def test_self_assign_issue_next_release(self):
  466. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  467. release.add_project(self.project)
  468. group = self.create_group(status=GroupStatus.UNRESOLVED)
  469. uo1 = UserOption.objects.create(
  470. key="self_assign_issue", value="1", project=None, user=self.user
  471. )
  472. self.login_as(user=self.user)
  473. url = f"{self.path}?id={group.id}"
  474. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  475. assert response.status_code == 200
  476. assert response.data["status"] == "resolved"
  477. assert response.data["statusDetails"]["inNextRelease"]
  478. assert response.data["assignedTo"]["id"] == str(self.user.id)
  479. assert response.data["assignedTo"]["type"] == "user"
  480. group = Group.objects.get(id=group.id)
  481. assert group.status == GroupStatus.RESOLVED
  482. assert GroupResolution.objects.filter(group=group, release=release).exists()
  483. assert GroupSubscription.objects.filter(
  484. user=self.user, group=group, is_active=True
  485. ).exists()
  486. activity = Activity.objects.get(
  487. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  488. )
  489. assert activity.data["version"] == ""
  490. uo1.delete()
  491. def test_selective_status_update(self):
  492. group1 = self.create_group(status=GroupStatus.RESOLVED)
  493. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  494. group3 = self.create_group(status=GroupStatus.IGNORED)
  495. group4 = self.create_group(
  496. project=self.create_project(slug="foo"),
  497. status=GroupStatus.UNRESOLVED,
  498. )
  499. self.login_as(user=self.user)
  500. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  501. response = self.client.put(url, data={"status": "resolved"}, format="json")
  502. assert response.status_code == 200
  503. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  504. new_group1 = Group.objects.get(id=group1.id)
  505. assert new_group1.resolved_at is not None
  506. assert new_group1.status == GroupStatus.RESOLVED
  507. new_group2 = Group.objects.get(id=group2.id)
  508. assert new_group2.resolved_at is not None
  509. assert new_group2.status == GroupStatus.RESOLVED
  510. assert GroupSubscription.objects.filter(
  511. user=self.user, group=new_group2, is_active=True
  512. ).exists()
  513. new_group3 = Group.objects.get(id=group3.id)
  514. assert new_group3.resolved_at is None
  515. assert new_group3.status == GroupStatus.IGNORED
  516. new_group4 = Group.objects.get(id=group4.id)
  517. assert new_group4.resolved_at is None
  518. assert new_group4.status == GroupStatus.UNRESOLVED
  519. def test_set_resolved_in_current_release(self):
  520. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  521. release.add_project(self.project)
  522. group = self.create_group(status=GroupStatus.UNRESOLVED)
  523. self.login_as(user=self.user)
  524. url = f"{self.path}?id={group.id}"
  525. response = self.client.put(
  526. url,
  527. data={"status": "resolved", "statusDetails": {"inRelease": "latest"}},
  528. format="json",
  529. )
  530. assert response.status_code == 200
  531. assert response.data["status"] == "resolved"
  532. assert response.data["statusDetails"]["inRelease"] == release.version
  533. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  534. assert "activity" in response.data
  535. group = Group.objects.get(id=group.id)
  536. assert group.status == GroupStatus.RESOLVED
  537. resolution = GroupResolution.objects.get(group=group)
  538. assert resolution.release == release
  539. assert resolution.type == GroupResolution.Type.in_release
  540. assert resolution.status == GroupResolution.Status.resolved
  541. assert resolution.actor_id == self.user.id
  542. assert GroupSubscription.objects.filter(
  543. user=self.user, group=group, is_active=True
  544. ).exists()
  545. activity = Activity.objects.get(
  546. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  547. )
  548. assert activity.data["version"] == release.version
  549. def test_set_resolved_in_explicit_release(self):
  550. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  551. release.add_project(self.project)
  552. release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
  553. release2.add_project(self.project)
  554. group = self.create_group(status=GroupStatus.UNRESOLVED)
  555. self.login_as(user=self.user)
  556. url = f"{self.path}?id={group.id}"
  557. response = self.client.put(
  558. url,
  559. data={"status": "resolved", "statusDetails": {"inRelease": release.version}},
  560. format="json",
  561. )
  562. assert response.status_code == 200
  563. assert response.data["status"] == "resolved"
  564. assert response.data["statusDetails"]["inRelease"] == release.version
  565. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  566. assert "activity" in response.data
  567. group = Group.objects.get(id=group.id)
  568. assert group.status == GroupStatus.RESOLVED
  569. resolution = GroupResolution.objects.get(group=group)
  570. assert resolution.release == release
  571. assert resolution.type == GroupResolution.Type.in_release
  572. assert resolution.status == GroupResolution.Status.resolved
  573. assert resolution.actor_id == self.user.id
  574. assert GroupSubscription.objects.filter(
  575. user=self.user, group=group, is_active=True
  576. ).exists()
  577. activity = Activity.objects.get(
  578. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  579. )
  580. assert activity.data["version"] == release.version
  581. def test_set_resolved_in_next_release(self):
  582. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  583. release.add_project(self.project)
  584. group = self.create_group(status=GroupStatus.UNRESOLVED)
  585. self.login_as(user=self.user)
  586. url = f"{self.path}?id={group.id}"
  587. response = self.client.put(
  588. url,
  589. data={"status": "resolved", "statusDetails": {"inNextRelease": True}},
  590. format="json",
  591. )
  592. assert response.status_code == 200
  593. assert response.data["status"] == "resolved"
  594. assert response.data["statusDetails"]["inNextRelease"]
  595. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  596. assert "activity" in response.data
  597. group = Group.objects.get(id=group.id)
  598. assert group.status == GroupStatus.RESOLVED
  599. resolution = GroupResolution.objects.get(group=group)
  600. assert resolution.release == release
  601. assert resolution.type == GroupResolution.Type.in_next_release
  602. assert resolution.status == GroupResolution.Status.pending
  603. assert resolution.actor_id == self.user.id
  604. assert GroupSubscription.objects.filter(
  605. user=self.user, group=group, is_active=True
  606. ).exists()
  607. activity = Activity.objects.get(
  608. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  609. )
  610. assert activity.data["version"] == ""
  611. def test_set_resolved_in_next_release_legacy(self):
  612. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  613. release.add_project(self.project)
  614. group = self.create_group(status=GroupStatus.UNRESOLVED)
  615. self.login_as(user=self.user)
  616. url = f"{self.path}?id={group.id}"
  617. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  618. assert response.status_code == 200
  619. assert response.data["status"] == "resolved"
  620. assert response.data["statusDetails"]["inNextRelease"]
  621. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  622. group = Group.objects.get(id=group.id)
  623. assert group.status == GroupStatus.RESOLVED
  624. resolution = GroupResolution.objects.get(group=group)
  625. assert resolution.release == release
  626. assert resolution.type == GroupResolution.Type.in_next_release
  627. assert resolution.status == GroupResolution.Status.pending
  628. assert resolution.actor_id == self.user.id
  629. assert GroupSubscription.objects.filter(
  630. user=self.user, group=group, is_active=True
  631. ).exists()
  632. activity = Activity.objects.get(
  633. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  634. )
  635. assert activity.data["version"] == ""
  636. def test_set_resolved_in_explicit_commit_unreleased(self):
  637. repo = self.create_repo(project=self.project, name=self.project.name)
  638. commit = self.create_commit(project=self.project, repo=repo)
  639. group = self.create_group(status=GroupStatus.UNRESOLVED)
  640. self.login_as(user=self.user)
  641. url = f"{self.path}?id={group.id}"
  642. response = self.client.put(
  643. url,
  644. data={
  645. "status": "resolved",
  646. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  647. },
  648. format="json",
  649. )
  650. assert response.status_code == 200
  651. assert response.data["status"] == "resolved"
  652. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  653. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  654. group = Group.objects.get(id=group.id)
  655. assert group.status == GroupStatus.RESOLVED
  656. link = GroupLink.objects.get(group_id=group.id)
  657. assert link.linked_type == GroupLink.LinkedType.commit
  658. assert link.relationship == GroupLink.Relationship.resolves
  659. assert link.linked_id == commit.id
  660. assert GroupSubscription.objects.filter(
  661. user=self.user, group=group, is_active=True
  662. ).exists()
  663. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  664. assert activity.data["commit"] == commit.id
  665. def test_set_resolved_in_explicit_commit_released(self):
  666. release = self.create_release(project=self.project)
  667. repo = self.create_repo(project=self.project, name=self.project.name)
  668. commit = self.create_commit(project=self.project, repo=repo, release=release)
  669. group = self.create_group(status=GroupStatus.UNRESOLVED)
  670. self.login_as(user=self.user)
  671. url = f"{self.path}?id={group.id}"
  672. response = self.client.put(
  673. url,
  674. data={
  675. "status": "resolved",
  676. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  677. },
  678. format="json",
  679. )
  680. assert response.status_code == 200
  681. assert response.data["status"] == "resolved"
  682. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  683. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  684. group = Group.objects.get(id=group.id)
  685. assert group.status == GroupStatus.RESOLVED
  686. link = GroupLink.objects.get(group_id=group.id)
  687. assert link.project_id == self.project.id
  688. assert link.linked_type == GroupLink.LinkedType.commit
  689. assert link.relationship == GroupLink.Relationship.resolves
  690. assert link.linked_id == commit.id
  691. assert GroupSubscription.objects.filter(
  692. user=self.user, group=group, is_active=True
  693. ).exists()
  694. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  695. assert activity.data["commit"] == commit.id
  696. resolution = GroupResolution.objects.get(group=group)
  697. assert resolution.type == GroupResolution.Type.in_release
  698. assert resolution.status == GroupResolution.Status.resolved
  699. def test_set_resolved_in_explicit_commit_missing(self):
  700. repo = self.create_repo(project=self.project, name=self.project.name)
  701. group = self.create_group(status=GroupStatus.UNRESOLVED)
  702. self.login_as(user=self.user)
  703. url = f"{self.path}?id={group.id}"
  704. response = self.client.put(
  705. url,
  706. data={
  707. "status": "resolved",
  708. "statusDetails": {"inCommit": {"commit": "a" * 40, "repository": repo.name}},
  709. },
  710. format="json",
  711. )
  712. assert response.status_code == 400
  713. assert (
  714. response.data["statusDetails"]["inCommit"]["commit"][0]
  715. == "Unable to find the given commit."
  716. )
  717. def test_set_unresolved(self):
  718. release = self.create_release(project=self.project, version="abc")
  719. group = self.create_group(status=GroupStatus.RESOLVED)
  720. GroupResolution.objects.create(group=group, release=release)
  721. self.login_as(user=self.user)
  722. url = f"{self.path}?id={group.id}"
  723. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  724. assert response.status_code == 200
  725. assert response.data == {"status": "unresolved", "statusDetails": {}}
  726. group = Group.objects.get(id=group.id)
  727. assert group.status == GroupStatus.UNRESOLVED
  728. self.assertNoResolution(group)
  729. assert GroupSubscription.objects.filter(
  730. user=self.user, group=group, is_active=True
  731. ).exists()
  732. def test_set_unresolved_on_snooze(self):
  733. group = self.create_group(status=GroupStatus.IGNORED)
  734. GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(days=1))
  735. self.login_as(user=self.user)
  736. url = f"{self.path}?id={group.id}"
  737. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  738. assert response.status_code == 200
  739. assert response.data == {"status": "unresolved", "statusDetails": {}}
  740. group = Group.objects.get(id=group.id)
  741. assert group.status == GroupStatus.UNRESOLVED
  742. def test_basic_ignore(self):
  743. group = self.create_group(status=GroupStatus.RESOLVED)
  744. snooze = GroupSnooze.objects.create(group=group, until=timezone.now())
  745. self.login_as(user=self.user)
  746. url = f"{self.path}?id={group.id}"
  747. response = self.client.put(url, data={"status": "ignored"}, format="json")
  748. assert response.status_code == 200
  749. # existing snooze objects should be cleaned up
  750. assert not GroupSnooze.objects.filter(id=snooze.id).exists()
  751. group = Group.objects.get(id=group.id)
  752. assert group.status == GroupStatus.IGNORED
  753. assert response.data == {"status": "ignored", "statusDetails": {}, "inbox": None}
  754. def test_snooze_duration(self):
  755. group = self.create_group(status=GroupStatus.RESOLVED)
  756. self.login_as(user=self.user)
  757. url = f"{self.path}?id={group.id}"
  758. response = self.client.put(
  759. url, data={"status": "ignored", "ignoreDuration": 30}, format="json"
  760. )
  761. assert response.status_code == 200
  762. snooze = GroupSnooze.objects.get(group=group)
  763. now = timezone.now()
  764. assert snooze.count is None
  765. assert snooze.until > now + timedelta(minutes=29)
  766. assert snooze.until < now + timedelta(minutes=31)
  767. assert snooze.user_count is None
  768. assert snooze.user_window is None
  769. assert snooze.window is None
  770. assert response.data["status"] == "ignored"
  771. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  772. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  773. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  774. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  775. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  776. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  777. def test_snooze_count(self):
  778. group = self.create_group(status=GroupStatus.RESOLVED, times_seen=1)
  779. self.login_as(user=self.user)
  780. url = f"{self.path}?id={group.id}"
  781. response = self.client.put(
  782. url, data={"status": "ignored", "ignoreCount": 100}, format="json"
  783. )
  784. assert response.status_code == 200
  785. snooze = GroupSnooze.objects.get(group=group)
  786. assert snooze.count == 100
  787. assert snooze.until is None
  788. assert snooze.user_count is None
  789. assert snooze.user_window is None
  790. assert snooze.window is None
  791. assert snooze.state["times_seen"] == 1
  792. assert response.data["status"] == "ignored"
  793. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  794. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  795. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  796. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  797. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  798. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  799. def test_snooze_user_count(self):
  800. event = {}
  801. for i in range(10):
  802. event = self.store_event(
  803. data={
  804. "fingerprint": ["put-me-in-group-1"],
  805. "user": {"id": str(i)},
  806. "timestamp": iso_format(self.min_ago + timedelta(seconds=i)),
  807. },
  808. project_id=self.project.id,
  809. )
  810. group = Group.objects.get(id=event.group.id)
  811. group.status = GroupStatus.RESOLVED
  812. group.save()
  813. self.login_as(user=self.user)
  814. url = f"{self.path}?id={group.id}"
  815. response = self.client.put(
  816. url, data={"status": "ignored", "ignoreUserCount": 10}, format="json"
  817. )
  818. assert response.status_code == 200
  819. snooze = GroupSnooze.objects.get(group=group)
  820. assert snooze.count is None
  821. assert snooze.until is None
  822. assert snooze.user_count == 10
  823. assert snooze.user_window is None
  824. assert snooze.window is None
  825. assert snooze.state["users_seen"] == 10
  826. assert response.data["status"] == "ignored"
  827. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  828. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  829. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  830. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  831. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  832. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  833. def test_set_bookmarked(self):
  834. group1 = self.create_group(status=GroupStatus.RESOLVED)
  835. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  836. group3 = self.create_group(status=GroupStatus.IGNORED)
  837. group4 = self.create_group(
  838. project=self.create_project(slug="foo"),
  839. status=GroupStatus.UNRESOLVED,
  840. )
  841. self.login_as(user=self.user)
  842. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  843. response = self.client.put(url, data={"isBookmarked": "true"}, format="json")
  844. assert response.status_code == 200
  845. assert response.data == {"isBookmarked": True}
  846. bookmark1 = GroupBookmark.objects.filter(group=group1, user=self.user)
  847. assert bookmark1.exists()
  848. assert GroupSubscription.objects.filter(
  849. user=self.user, group=group1, is_active=True
  850. ).exists()
  851. bookmark2 = GroupBookmark.objects.filter(group=group2, user=self.user)
  852. assert bookmark2.exists()
  853. assert GroupSubscription.objects.filter(
  854. user=self.user, group=group2, is_active=True
  855. ).exists()
  856. bookmark3 = GroupBookmark.objects.filter(group=group3, user=self.user)
  857. assert not bookmark3.exists()
  858. bookmark4 = GroupBookmark.objects.filter(group=group4, user=self.user)
  859. assert not bookmark4.exists()
  860. def test_subscription(self):
  861. group1 = self.create_group()
  862. group2 = self.create_group()
  863. group3 = self.create_group()
  864. group4 = self.create_group(project=self.create_project(slug="foo"))
  865. self.login_as(user=self.user)
  866. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  867. response = self.client.put(url, data={"isSubscribed": "true"}, format="json")
  868. assert response.status_code == 200
  869. assert response.data == {"isSubscribed": True, "subscriptionDetails": {"reason": "unknown"}}
  870. assert GroupSubscription.objects.filter(
  871. group=group1, user=self.user, is_active=True
  872. ).exists()
  873. assert GroupSubscription.objects.filter(
  874. group=group2, user=self.user, is_active=True
  875. ).exists()
  876. assert not GroupSubscription.objects.filter(group=group3, user=self.user).exists()
  877. assert not GroupSubscription.objects.filter(group=group4, user=self.user).exists()
  878. def test_set_public(self):
  879. group1 = self.create_group()
  880. group2 = self.create_group()
  881. self.login_as(user=self.user)
  882. url = f"{self.path}?id={group1.id}&id={group2.id}"
  883. response = self.client.put(url, data={"isPublic": "true"}, format="json")
  884. assert response.status_code == 200
  885. assert response.data["isPublic"] is True
  886. assert "shareId" in response.data
  887. new_group1 = Group.objects.get(id=group1.id)
  888. assert bool(new_group1.get_share_id())
  889. new_group2 = Group.objects.get(id=group2.id)
  890. assert bool(new_group2.get_share_id())
  891. def test_set_private(self):
  892. group1 = self.create_group()
  893. group2 = self.create_group()
  894. # Manually mark them as shared
  895. for g in group1, group2:
  896. GroupShare.objects.create(project_id=g.project_id, group=g)
  897. assert bool(g.get_share_id())
  898. self.login_as(user=self.user)
  899. url = f"{self.path}?id={group1.id}&id={group2.id}"
  900. response = self.client.put(url, data={"isPublic": "false"}, format="json")
  901. assert response.status_code == 200
  902. assert response.data == {"isPublic": False, "shareId": None}
  903. new_group1 = Group.objects.get(id=group1.id)
  904. assert not bool(new_group1.get_share_id())
  905. new_group2 = Group.objects.get(id=group2.id)
  906. assert not bool(new_group2.get_share_id())
  907. def test_set_has_seen(self):
  908. group1 = self.create_group(status=GroupStatus.RESOLVED)
  909. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  910. group3 = self.create_group(status=GroupStatus.IGNORED)
  911. group4 = self.create_group(
  912. project=self.create_project(slug="foo"),
  913. status=GroupStatus.UNRESOLVED,
  914. )
  915. self.login_as(user=self.user)
  916. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  917. response = self.client.put(url, data={"hasSeen": "true"}, format="json")
  918. assert response.status_code == 200
  919. assert response.data == {"hasSeen": True}
  920. r1 = GroupSeen.objects.filter(group=group1, user=self.user)
  921. assert r1.exists()
  922. r2 = GroupSeen.objects.filter(group=group2, user=self.user)
  923. assert r2.exists()
  924. r3 = GroupSeen.objects.filter(group=group3, user=self.user)
  925. assert not r3.exists()
  926. r4 = GroupSeen.objects.filter(group=group4, user=self.user)
  927. assert not r4.exists()
  928. def test_inbox_fields(self):
  929. group1 = self.create_group(status=GroupStatus.RESOLVED)
  930. add_group_to_inbox(group1, GroupInboxReason.NEW)
  931. self.login_as(user=self.user)
  932. url = f"{self.path}?id={group1.id}"
  933. response = self.client.put(url, data={"status": "resolved"}, format="json")
  934. assert "inbox" in response.data
  935. assert response.data["inbox"] is None
  936. @patch("sentry.api.helpers.group_index.update.uuid4")
  937. @patch("sentry.api.helpers.group_index.update.merge_groups")
  938. @patch("sentry.api.helpers.group_index.update.eventstream")
  939. def test_merge(self, mock_eventstream, merge_groups, mock_uuid4):
  940. eventstream_state = object()
  941. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  942. mock_uuid4.return_value = self.get_mock_uuid()
  943. group1 = self.create_group(times_seen=1)
  944. group2 = self.create_group(times_seen=50)
  945. group3 = self.create_group(times_seen=2)
  946. self.create_group()
  947. self.login_as(user=self.user)
  948. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  949. response = self.client.put(url, data={"merge": "1"}, format="json")
  950. assert response.status_code == 200
  951. assert response.data["merge"]["parent"] == str(group2.id)
  952. assert sorted(response.data["merge"]["children"]) == sorted(
  953. [str(group1.id), str(group3.id)]
  954. )
  955. mock_eventstream.start_merge.assert_called_once_with(
  956. group1.project_id, [group3.id, group1.id], group2.id
  957. )
  958. assert len(merge_groups.mock_calls) == 1
  959. merge_groups.delay.assert_any_call(
  960. from_object_ids=[group3.id, group1.id],
  961. to_object_id=group2.id,
  962. transaction_id="abc123",
  963. eventstream_state=eventstream_state,
  964. )
  965. @patch("sentry.api.helpers.group_index.update.uuid4")
  966. @patch("sentry.api.helpers.group_index.update.merge_groups")
  967. @patch("sentry.api.helpers.group_index.update.eventstream")
  968. def test_merge_performance_issues(self, mock_eventstream, merge_groups, mock_uuid4):
  969. eventstream_state = object()
  970. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  971. mock_uuid4.return_value = self.get_mock_uuid()
  972. group1 = self.create_group(times_seen=1, type=GroupType.PERFORMANCE_SLOW_SPAN.value)
  973. group2 = self.create_group(times_seen=50, type=GroupType.PERFORMANCE_SLOW_SPAN.value)
  974. group3 = self.create_group(times_seen=2, type=GroupType.PERFORMANCE_SLOW_SPAN.value)
  975. self.create_group()
  976. self.login_as(user=self.user)
  977. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  978. response = self.client.put(url, data={"merge": "1"}, format="json")
  979. assert response.status_code == 400, response.content
  980. def test_assign(self):
  981. group1 = self.create_group(is_public=True)
  982. group2 = self.create_group(is_public=True)
  983. user = self.user
  984. self.login_as(user=user)
  985. url = f"{self.path}?id={group1.id}"
  986. response = self.client.put(url, data={"assignedTo": user.username})
  987. assert response.status_code == 200
  988. assert response.data["assignedTo"]["id"] == str(user.id)
  989. assert response.data["assignedTo"]["type"] == "user"
  990. assert GroupAssignee.objects.filter(group=group1, user=user).exists()
  991. assert not GroupAssignee.objects.filter(group=group2, user=user).exists()
  992. assert (
  993. Activity.objects.filter(
  994. group=group1, user=user, type=ActivityType.ASSIGNED.value
  995. ).count()
  996. == 1
  997. )
  998. assert GroupSubscription.objects.filter(user=user, group=group1, is_active=True).exists()
  999. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1000. assert response.status_code == 200, response.content
  1001. assert response.data["assignedTo"] is None
  1002. assert not GroupAssignee.objects.filter(group=group1, user=user).exists()
  1003. def test_assign_non_member(self):
  1004. group = self.create_group(is_public=True)
  1005. member = self.user
  1006. non_member = self.create_user("bar@example.com")
  1007. self.login_as(user=member)
  1008. url = f"{self.path}?id={group.id}"
  1009. response = self.client.put(url, data={"assignedTo": non_member.username}, format="json")
  1010. assert response.status_code == 400, response.content
  1011. def test_assign_team(self):
  1012. self.login_as(user=self.user)
  1013. group = self.create_group()
  1014. other_member = self.create_user("bar@example.com")
  1015. team = self.create_team(
  1016. organization=group.project.organization, members=[self.user, other_member]
  1017. )
  1018. group.project.add_team(team)
  1019. url = f"{self.path}?id={group.id}"
  1020. response = self.client.put(url, data={"assignedTo": f"team:{team.id}"})
  1021. assert response.status_code == 200
  1022. assert response.data["assignedTo"]["id"] == str(team.id)
  1023. assert response.data["assignedTo"]["type"] == "team"
  1024. assert GroupAssignee.objects.filter(group=group, team=team).exists()
  1025. assert Activity.objects.filter(group=group, type=ActivityType.ASSIGNED.value).count() == 1
  1026. assert GroupSubscription.objects.filter(group=group, is_active=True).count() == 2
  1027. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1028. assert response.status_code == 200, response.content
  1029. assert response.data["assignedTo"] is None
  1030. def test_discard(self):
  1031. group1 = self.create_group(is_public=True)
  1032. group2 = self.create_group(is_public=True)
  1033. group_hash = GroupHash.objects.create(hash="x" * 32, project=group1.project, group=group1)
  1034. user = self.user
  1035. self.login_as(user=user)
  1036. url = f"{self.path}?id={group1.id}"
  1037. with self.tasks():
  1038. with self.feature("projects:discard-groups"):
  1039. response = self.client.put(url, data={"discard": True})
  1040. assert response.status_code == 204
  1041. assert not Group.objects.filter(id=group1.id).exists()
  1042. assert Group.objects.filter(id=group2.id).exists()
  1043. assert GroupHash.objects.filter(id=group_hash.id).exists()
  1044. tombstone = GroupTombstone.objects.get(
  1045. id=GroupHash.objects.get(id=group_hash.id).group_tombstone_id
  1046. )
  1047. assert tombstone.message == group1.message
  1048. assert tombstone.culprit == group1.culprit
  1049. assert tombstone.project == group1.project
  1050. assert tombstone.data == group1.data
  1051. @patch(
  1052. "sentry.models.OrganizationMember.get_scopes",
  1053. return_value=frozenset(s for s in settings.SENTRY_SCOPES if s != "event:admin"),
  1054. )
  1055. def test_discard_requires_events_admin(self, mock_get_scopes):
  1056. group1 = self.create_group(is_public=True)
  1057. user = self.user
  1058. self.login_as(user=user)
  1059. url = f"{self.path}?id={group1.id}"
  1060. with self.tasks(), self.feature("projects:discard-groups"):
  1061. response = self.client.put(url, data={"discard": True})
  1062. assert response.status_code == 400
  1063. assert Group.objects.filter(id=group1.id).exists()
  1064. @region_silo_test
  1065. class GroupDeleteTest(APITestCase, SnubaTestCase):
  1066. @fixture
  1067. def path(self):
  1068. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  1069. @patch("sentry.api.helpers.group_index.delete.eventstream")
  1070. @patch("sentry.eventstream")
  1071. def test_delete_by_id(self, mock_eventstream_task, mock_eventstream_api):
  1072. eventstream_state = {"event_stream_state": uuid4()}
  1073. mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
  1074. group1 = self.create_group(status=GroupStatus.RESOLVED)
  1075. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  1076. group3 = self.create_group(status=GroupStatus.IGNORED)
  1077. group4 = self.create_group(
  1078. project=self.create_project(slug="foo"),
  1079. status=GroupStatus.UNRESOLVED,
  1080. )
  1081. hashes = []
  1082. for g in group1, group2, group3, group4:
  1083. hash = uuid4().hex
  1084. hashes.append(hash)
  1085. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1086. self.login_as(user=self.user)
  1087. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  1088. response = self.client.delete(url, format="json")
  1089. mock_eventstream_api.start_delete_groups.assert_called_once_with(
  1090. group1.project_id, [group1.id, group2.id]
  1091. )
  1092. assert response.status_code == 204
  1093. assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
  1094. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1095. assert Group.objects.get(id=group2.id).status == GroupStatus.PENDING_DELETION
  1096. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1097. assert Group.objects.get(id=group3.id).status != GroupStatus.PENDING_DELETION
  1098. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1099. assert Group.objects.get(id=group4.id).status != GroupStatus.PENDING_DELETION
  1100. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1101. Group.objects.filter(id__in=(group1.id, group2.id)).update(status=GroupStatus.UNRESOLVED)
  1102. with self.tasks():
  1103. response = self.client.delete(url, format="json")
  1104. mock_eventstream_task.end_delete_groups.assert_called_once_with(eventstream_state)
  1105. assert response.status_code == 204
  1106. assert not Group.objects.filter(id=group1.id).exists()
  1107. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1108. assert not Group.objects.filter(id=group2.id).exists()
  1109. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1110. assert Group.objects.filter(id=group3.id).exists()
  1111. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1112. assert Group.objects.filter(id=group4.id).exists()
  1113. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1114. @patch("sentry.api.helpers.group_index.delete.eventstream")
  1115. @patch("sentry.eventstream")
  1116. def test_delete_performance_issue_by_id(self, mock_eventstream_task, mock_eventstream_api):
  1117. eventstream_state = {"event_stream_state": uuid4()}
  1118. mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
  1119. group1 = self.create_group(
  1120. status=GroupStatus.RESOLVED, type=GroupType.PERFORMANCE_SLOW_SPAN.value
  1121. )
  1122. group2 = self.create_group(
  1123. status=GroupStatus.UNRESOLVED, type=GroupType.PERFORMANCE_SLOW_SPAN.value
  1124. )
  1125. hashes = []
  1126. for g in group1, group2:
  1127. hash = uuid4().hex
  1128. hashes.append(hash)
  1129. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1130. self.login_as(user=self.user)
  1131. url = f"{self.path}?id={group1.id}&id={group2.id}"
  1132. response = self.client.delete(url, format="json")
  1133. assert response.status_code == 400
  1134. assert Group.objects.filter(id=group1.id).exists()
  1135. assert GroupHash.objects.filter(group_id=group1.id).exists()
  1136. assert Group.objects.filter(id=group2.id).exists()
  1137. assert GroupHash.objects.filter(group_id=group2.id).exists()
  1138. def test_bulk_delete(self):
  1139. groups = []
  1140. for i in range(10, 41):
  1141. groups.append(
  1142. self.create_group(
  1143. project=self.project,
  1144. status=GroupStatus.RESOLVED,
  1145. )
  1146. )
  1147. hashes = []
  1148. for group in groups:
  1149. hash = uuid4().hex
  1150. hashes.append(hash)
  1151. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1152. self.login_as(user=self.user)
  1153. # if query is '' it defaults to is:unresolved
  1154. url = self.path + "?query="
  1155. response = self.client.delete(url, format="json")
  1156. assert response.status_code == 204
  1157. for group in groups:
  1158. assert Group.objects.get(id=group.id).status == GroupStatus.PENDING_DELETION
  1159. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1160. Group.objects.filter(id__in=[group.id for group in groups]).update(
  1161. status=GroupStatus.UNRESOLVED
  1162. )
  1163. with self.tasks():
  1164. response = self.client.delete(url, format="json")
  1165. assert response.status_code == 204
  1166. for group in groups:
  1167. assert not Group.objects.filter(id=group.id).exists()
  1168. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1169. def test_bulk_delete_performance_issues(self):
  1170. groups = []
  1171. for i in range(10, 41):
  1172. groups.append(
  1173. self.create_group(
  1174. project=self.project,
  1175. status=GroupStatus.RESOLVED,
  1176. type=GroupType.PERFORMANCE_SLOW_SPAN.value,
  1177. )
  1178. )
  1179. hashes = []
  1180. for group in groups:
  1181. hash = uuid4().hex
  1182. hashes.append(hash)
  1183. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1184. self.login_as(user=self.user)
  1185. # if query is '' it defaults to is:unresolved
  1186. url = self.path + "?query="
  1187. with self.feature("organizations:performance-issues"):
  1188. response = self.client.delete(url, format="json")
  1189. assert response.status_code == 400
  1190. for group in groups:
  1191. assert Group.objects.filter(id=group.id).exists()
  1192. assert GroupHash.objects.filter(group_id=group.id).exists()