test_project_group_index.py 60 KB

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