test_project_group_index.py 59 KB

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