test_project_group_index.py 56 KB

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