test_project_group_index.py 59 KB

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