test_project_group_index.py 60 KB

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