test_project_group_index.py 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608
  1. import time
  2. from datetime import timedelta
  3. from functools import cached_property
  4. from unittest.mock import Mock, call, patch
  5. from urllib.parse import quote
  6. from uuid import uuid4
  7. from django.conf import settings
  8. from django.utils import timezone
  9. from sentry.issues.grouptype import PerformanceSlowDBQueryGroupType
  10. from sentry.models.activity import Activity
  11. from sentry.models.apitoken import ApiToken
  12. from sentry.models.group import Group, GroupStatus
  13. from sentry.models.groupassignee import GroupAssignee
  14. from sentry.models.groupbookmark import GroupBookmark
  15. from sentry.models.grouphash import GroupHash
  16. from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox
  17. from sentry.models.grouplink import GroupLink
  18. from sentry.models.groupresolution import GroupResolution
  19. from sentry.models.groupseen import GroupSeen
  20. from sentry.models.groupshare import GroupShare
  21. from sentry.models.groupsnooze import GroupSnooze
  22. from sentry.models.groupsubscription import GroupSubscription
  23. from sentry.models.grouptombstone import GroupTombstone
  24. from sentry.models.integrations.external_issue import ExternalIssue
  25. from sentry.models.integrations.organization_integration import OrganizationIntegration
  26. from sentry.models.options.user_option import UserOption
  27. from sentry.models.release import Release
  28. from sentry.silo.base import SiloMode
  29. from sentry.testutils.cases import APITestCase, SnubaTestCase
  30. from sentry.testutils.helpers import parse_link_header, with_feature
  31. from sentry.testutils.helpers.datetime import before_now, iso_format
  32. from sentry.testutils.silo import assume_test_silo_mode
  33. from sentry.types.activity import ActivityType
  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. @cached_property
  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(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(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(last_seen=before_now(seconds=1))
  97. self.create_group(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(last_seen=before_now(days=1))
  136. group2 = self.create_group(last_seen=timezone.now())
  137. self.login_as(user=self.user)
  138. response = self.client.get(self.path + "?query=is:unresolved", 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()
  191. self.create_group()
  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(status=GroupStatus.PENDING_DELETION)
  268. group = self.create_group()
  269. self.create_group(status=GroupStatus.DELETION_IN_PROGRESS)
  270. self.create_group(status=GroupStatus.PENDING_MERGE)
  271. self.login_as(user=self.user)
  272. response = self.client.get(self.path + "?query=is:unresolved", 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. with assume_test_silo_mode(SiloMode.CONTROL):
  284. token = ApiToken.objects.create(user=self.user, scopes=256)
  285. response = self.client.get(
  286. self.path, format="json", HTTP_AUTHORIZATION=f"Bearer {token.token}"
  287. )
  288. assert response.status_code == 200, response.content
  289. def test_filter_not_unresolved(self):
  290. event = self.store_event(
  291. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  292. project_id=self.project.id,
  293. )
  294. event.group.update(status=GroupStatus.RESOLVED, substatus=None)
  295. self.login_as(user=self.user)
  296. response = self.client.get(f"{self.path}?query=!is:unresolved", format="json")
  297. assert response.status_code == 200
  298. assert [int(r["id"]) for r in response.data] == [event.group.id]
  299. class GroupUpdateTest(APITestCase, SnubaTestCase):
  300. def setUp(self):
  301. super().setUp()
  302. self.min_ago = timezone.now() - timedelta(minutes=1)
  303. @cached_property
  304. def path(self):
  305. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  306. def assertNoResolution(self, group):
  307. assert not GroupResolution.objects.filter(group=group).exists()
  308. def test_global_resolve(self):
  309. group1 = self.create_group(status=GroupStatus.RESOLVED)
  310. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  311. group3 = self.create_group(status=GroupStatus.IGNORED)
  312. group4 = self.create_group(
  313. project=self.create_project(slug="foo"),
  314. status=GroupStatus.UNRESOLVED,
  315. )
  316. self.login_as(user=self.user)
  317. response = self.client.put(
  318. f"{self.path}?status=unresolved&query=is:unresolved",
  319. data={"status": "resolved"},
  320. format="json",
  321. )
  322. assert response.status_code == 200, response.data
  323. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  324. # the previously resolved entry should not be included
  325. new_group1 = Group.objects.get(id=group1.id)
  326. assert new_group1.status == GroupStatus.RESOLVED
  327. assert new_group1.resolved_at is None
  328. # this wont exist because it wasn't affected
  329. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group1).exists()
  330. new_group2 = Group.objects.get(id=group2.id)
  331. assert new_group2.status == GroupStatus.RESOLVED
  332. assert new_group2.resolved_at is not None
  333. assert GroupSubscription.objects.filter(
  334. user_id=self.user.id, group=new_group2, is_active=True
  335. ).exists()
  336. # the ignored entry should not be included
  337. new_group3 = Group.objects.get(id=group3.id)
  338. assert new_group3.status == GroupStatus.IGNORED
  339. assert new_group3.resolved_at is None
  340. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group3)
  341. new_group4 = Group.objects.get(id=group4.id)
  342. assert new_group4.status == GroupStatus.UNRESOLVED
  343. assert new_group4.resolved_at is None
  344. assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group4)
  345. def test_bulk_resolve(self):
  346. self.login_as(user=self.user)
  347. for i in range(200):
  348. self.create_group(status=GroupStatus.UNRESOLVED)
  349. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  350. assert len(response.data) == 100
  351. response = self.client.put(
  352. f"{self.path}?status=unresolved&query=is:unresolved",
  353. data={"status": "resolved"},
  354. format="json",
  355. )
  356. assert response.status_code == 200, response.data
  357. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  358. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  359. assert len(response.data) == 0
  360. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  361. def test_resolve_with_integration(self, mock_sync_status_outbound):
  362. self.login_as(user=self.user)
  363. org = self.organization
  364. with assume_test_silo_mode(SiloMode.CONTROL):
  365. integration = self.create_provider_integration(provider="example", name="Example")
  366. integration.add_organization(org, self.user)
  367. group = self.create_group(status=GroupStatus.UNRESOLVED)
  368. with assume_test_silo_mode(SiloMode.CONTROL):
  369. OrganizationIntegration.objects.filter(
  370. integration_id=integration.id, organization_id=group.organization.id
  371. ).update(
  372. config={
  373. "sync_comments": True,
  374. "sync_status_outbound": True,
  375. "sync_status_inbound": True,
  376. "sync_assignee_outbound": True,
  377. "sync_assignee_inbound": True,
  378. }
  379. )
  380. external_issue = ExternalIssue.objects.get_or_create(
  381. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  382. )[0]
  383. GroupLink.objects.get_or_create(
  384. group_id=group.id,
  385. project_id=group.project_id,
  386. linked_type=GroupLink.LinkedType.issue,
  387. linked_id=external_issue.id,
  388. relationship=GroupLink.Relationship.references,
  389. )[0]
  390. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  391. assert len(response.data) == 1
  392. with self.tasks():
  393. with self.feature({"organizations:integrations-issue-sync": True}):
  394. response = self.client.put(
  395. f"{self.path}?status=unresolved&query=is:unresolved",
  396. data={"status": "resolved"},
  397. format="json",
  398. )
  399. assert response.status_code == 200, response.data
  400. group = Group.objects.get(id=group.id)
  401. assert group.status == GroupStatus.RESOLVED
  402. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  403. mock_sync_status_outbound.assert_called_once_with(
  404. external_issue, True, group.project_id
  405. )
  406. response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
  407. assert len(response.data) == 0
  408. @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
  409. def test_set_unresolved_with_integration(self, mock_sync_status_outbound):
  410. release = self.create_release(project=self.project, version="abc")
  411. group = self.create_group(status=GroupStatus.RESOLVED)
  412. org = self.organization
  413. with assume_test_silo_mode(SiloMode.CONTROL):
  414. integration = self.create_provider_integration(provider="example", name="Example")
  415. integration.add_organization(org, self.user)
  416. OrganizationIntegration.objects.filter(
  417. integration_id=integration.id, organization_id=group.organization.id
  418. ).update(
  419. config={
  420. "sync_comments": True,
  421. "sync_status_outbound": True,
  422. "sync_status_inbound": True,
  423. "sync_assignee_outbound": True,
  424. "sync_assignee_inbound": True,
  425. }
  426. )
  427. GroupResolution.objects.create(group=group, release=release)
  428. external_issue = ExternalIssue.objects.get_or_create(
  429. organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
  430. )[0]
  431. GroupLink.objects.get_or_create(
  432. group_id=group.id,
  433. project_id=group.project_id,
  434. linked_type=GroupLink.LinkedType.issue,
  435. linked_id=external_issue.id,
  436. relationship=GroupLink.Relationship.references,
  437. )[0]
  438. self.login_as(user=self.user)
  439. url = f"{self.path}?id={group.id}"
  440. with self.tasks():
  441. with self.feature({"organizations:integrations-issue-sync": True}):
  442. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  443. assert response.status_code == 200
  444. assert response.data == {"status": "unresolved", "statusDetails": {}}
  445. group = Group.objects.get(id=group.id)
  446. assert group.status == GroupStatus.UNRESOLVED
  447. self.assertNoResolution(group)
  448. assert GroupSubscription.objects.filter(
  449. user_id=self.user.id, group=group, is_active=True
  450. ).exists()
  451. mock_sync_status_outbound.assert_called_once_with(
  452. external_issue, False, group.project_id
  453. )
  454. def test_self_assign_issue(self):
  455. group = self.create_group(status=GroupStatus.UNRESOLVED)
  456. user = self.user
  457. with assume_test_silo_mode(SiloMode.CONTROL):
  458. uo1 = UserOption.objects.create(
  459. key="self_assign_issue", value="1", project_id=None, user=user
  460. )
  461. self.login_as(user=user)
  462. url = f"{self.path}?id={group.id}"
  463. response = self.client.put(url, data={"status": "resolved"}, format="json")
  464. assert response.status_code == 200, response.data
  465. assert response.data["assignedTo"]["id"] == str(user.id)
  466. assert response.data["assignedTo"]["type"] == "user"
  467. assert response.data["status"] == "resolved"
  468. assert GroupAssignee.objects.filter(group=group, user_id=user.id).exists()
  469. assert GroupSubscription.objects.filter(
  470. user_id=user.id, group=group, is_active=True
  471. ).exists()
  472. with assume_test_silo_mode(SiloMode.CONTROL):
  473. uo1.delete()
  474. def test_self_assign_issue_next_release(self):
  475. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  476. release.add_project(self.project)
  477. group = self.create_group(status=GroupStatus.UNRESOLVED)
  478. with assume_test_silo_mode(SiloMode.CONTROL):
  479. uo1 = UserOption.objects.create(
  480. key="self_assign_issue", value="1", project_id=None, user=self.user
  481. )
  482. self.login_as(user=self.user)
  483. url = f"{self.path}?id={group.id}"
  484. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  485. assert response.status_code == 200
  486. assert response.data["status"] == "resolved"
  487. assert response.data["statusDetails"]["inNextRelease"]
  488. assert response.data["assignedTo"]["id"] == str(self.user.id)
  489. assert response.data["assignedTo"]["type"] == "user"
  490. group = Group.objects.get(id=group.id)
  491. assert group.status == GroupStatus.RESOLVED
  492. assert GroupResolution.objects.filter(group=group, release=release).exists()
  493. assert GroupSubscription.objects.filter(
  494. user_id=self.user.id, group=group, is_active=True
  495. ).exists()
  496. activity = Activity.objects.get(
  497. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  498. )
  499. assert activity.data["version"] == ""
  500. with assume_test_silo_mode(SiloMode.CONTROL):
  501. uo1.delete()
  502. def test_selective_status_update(self):
  503. group1 = self.create_group(status=GroupStatus.RESOLVED)
  504. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  505. group3 = self.create_group(status=GroupStatus.IGNORED)
  506. group4 = self.create_group(
  507. project=self.create_project(slug="foo"),
  508. status=GroupStatus.UNRESOLVED,
  509. )
  510. self.login_as(user=self.user)
  511. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  512. response = self.client.put(url, data={"status": "resolved"}, format="json")
  513. assert response.status_code == 200
  514. assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
  515. new_group1 = Group.objects.get(id=group1.id)
  516. assert new_group1.resolved_at is not None
  517. assert new_group1.status == GroupStatus.RESOLVED
  518. new_group2 = Group.objects.get(id=group2.id)
  519. assert new_group2.resolved_at is not None
  520. assert new_group2.status == GroupStatus.RESOLVED
  521. assert GroupSubscription.objects.filter(
  522. user_id=self.user.id, group=new_group2, is_active=True
  523. ).exists()
  524. new_group3 = Group.objects.get(id=group3.id)
  525. assert new_group3.resolved_at is None
  526. assert new_group3.status == GroupStatus.IGNORED
  527. new_group4 = Group.objects.get(id=group4.id)
  528. assert new_group4.resolved_at is None
  529. assert new_group4.status == GroupStatus.UNRESOLVED
  530. def test_set_resolved_in_current_release(self):
  531. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  532. release.add_project(self.project)
  533. group = self.create_group(status=GroupStatus.UNRESOLVED)
  534. self.login_as(user=self.user)
  535. url = f"{self.path}?id={group.id}"
  536. response = self.client.put(
  537. url,
  538. data={"status": "resolved", "statusDetails": {"inRelease": "latest"}},
  539. format="json",
  540. )
  541. assert response.status_code == 200
  542. assert response.data["status"] == "resolved"
  543. assert response.data["statusDetails"]["inRelease"] == release.version
  544. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  545. assert "activity" in response.data
  546. group = Group.objects.get(id=group.id)
  547. assert group.status == GroupStatus.RESOLVED
  548. resolution = GroupResolution.objects.get(group=group)
  549. assert resolution.release == release
  550. assert resolution.type == GroupResolution.Type.in_release
  551. assert resolution.status == GroupResolution.Status.resolved
  552. assert resolution.actor_id == self.user.id
  553. assert GroupSubscription.objects.filter(
  554. user_id=self.user.id, group=group, is_active=True
  555. ).exists()
  556. activity = Activity.objects.get(
  557. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  558. )
  559. assert activity.data["version"] == release.version
  560. def test_set_resolved_in_explicit_release(self):
  561. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  562. release.add_project(self.project)
  563. release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
  564. release2.add_project(self.project)
  565. group = self.create_group(status=GroupStatus.UNRESOLVED)
  566. self.login_as(user=self.user)
  567. url = f"{self.path}?id={group.id}"
  568. response = self.client.put(
  569. url,
  570. data={"status": "resolved", "statusDetails": {"inRelease": release.version}},
  571. format="json",
  572. )
  573. assert response.status_code == 200
  574. assert response.data["status"] == "resolved"
  575. assert response.data["statusDetails"]["inRelease"] == release.version
  576. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  577. assert "activity" in response.data
  578. group = Group.objects.get(id=group.id)
  579. assert group.status == GroupStatus.RESOLVED
  580. resolution = GroupResolution.objects.get(group=group)
  581. assert resolution.release == release
  582. assert resolution.type == GroupResolution.Type.in_release
  583. assert resolution.status == GroupResolution.Status.resolved
  584. assert resolution.actor_id == self.user.id
  585. assert GroupSubscription.objects.filter(
  586. user_id=self.user.id, group=group, is_active=True
  587. ).exists()
  588. activity = Activity.objects.get(
  589. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  590. )
  591. assert activity.data["version"] == release.version
  592. def test_set_resolved_in_next_release(self):
  593. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  594. release.add_project(self.project)
  595. group = self.create_group(status=GroupStatus.UNRESOLVED)
  596. self.login_as(user=self.user)
  597. url = f"{self.path}?id={group.id}"
  598. response = self.client.put(
  599. url,
  600. data={"status": "resolved", "statusDetails": {"inNextRelease": True}},
  601. format="json",
  602. )
  603. assert response.status_code == 200
  604. assert response.data["status"] == "resolved"
  605. assert response.data["statusDetails"]["inNextRelease"]
  606. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  607. assert "activity" in response.data
  608. group = Group.objects.get(id=group.id)
  609. assert group.status == GroupStatus.RESOLVED
  610. resolution = GroupResolution.objects.get(group=group)
  611. assert resolution.release == release
  612. assert resolution.type == GroupResolution.Type.in_next_release
  613. assert resolution.status == GroupResolution.Status.pending
  614. assert resolution.actor_id == self.user.id
  615. assert GroupSubscription.objects.filter(
  616. user_id=self.user.id, group=group, is_active=True
  617. ).exists()
  618. activity = Activity.objects.get(
  619. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  620. )
  621. assert activity.data["version"] == ""
  622. def test_set_resolved_in_next_release_legacy(self):
  623. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  624. release.add_project(self.project)
  625. group = self.create_group(status=GroupStatus.UNRESOLVED)
  626. self.login_as(user=self.user)
  627. url = f"{self.path}?id={group.id}"
  628. response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
  629. assert response.status_code == 200
  630. assert response.data["status"] == "resolved"
  631. assert response.data["statusDetails"]["inNextRelease"]
  632. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  633. group = Group.objects.get(id=group.id)
  634. assert group.status == GroupStatus.RESOLVED
  635. resolution = GroupResolution.objects.get(group=group)
  636. assert resolution.release == release
  637. assert resolution.type == GroupResolution.Type.in_next_release
  638. assert resolution.status == GroupResolution.Status.pending
  639. assert resolution.actor_id == self.user.id
  640. assert GroupSubscription.objects.filter(
  641. user_id=self.user.id, group=group, is_active=True
  642. ).exists()
  643. activity = Activity.objects.get(
  644. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  645. )
  646. assert activity.data["version"] == ""
  647. @with_feature("organizations:resolve-in-upcoming-release")
  648. def test_set_resolved_in_upcoming_release(self):
  649. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  650. release.add_project(self.project)
  651. group = self.create_group(status=GroupStatus.UNRESOLVED)
  652. self.login_as(user=self.user)
  653. url = f"{self.path}?id={group.id}"
  654. response = self.client.put(
  655. url,
  656. data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
  657. format="json",
  658. )
  659. assert response.status_code == 200
  660. assert response.data["status"] == "resolved"
  661. assert response.data["statusDetails"]["inUpcomingRelease"]
  662. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  663. assert "activity" in response.data
  664. group = Group.objects.get(id=group.id)
  665. assert group.status == GroupStatus.RESOLVED
  666. resolution = GroupResolution.objects.get(group=group)
  667. assert resolution.release == release
  668. assert resolution.type == GroupResolution.Type.in_upcoming_release
  669. assert resolution.status == GroupResolution.Status.pending
  670. assert resolution.actor_id == self.user.id
  671. assert GroupSubscription.objects.filter(
  672. user_id=self.user.id, group=group, is_active=True
  673. ).exists()
  674. activity = Activity.objects.get(
  675. group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
  676. )
  677. assert activity.data["version"] == ""
  678. def test_upcoming_release_flag_validation(self):
  679. release = Release.objects.create(organization_id=self.project.organization_id, version="a")
  680. release.add_project(self.project)
  681. group = self.create_group(status=GroupStatus.UNRESOLVED)
  682. self.login_as(user=self.user)
  683. url = f"{self.path}?id={group.id}"
  684. response = self.client.put(
  685. url,
  686. data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
  687. format="json",
  688. )
  689. assert response.status_code == 400
  690. assert (
  691. response.data["statusDetails"]["inUpcomingRelease"][0]
  692. == "Your organization does not have access to this feature."
  693. )
  694. @with_feature("organizations:resolve-in-upcoming-release")
  695. def test_upcoming_release_release_validation(self):
  696. group = self.create_group(status=GroupStatus.UNRESOLVED)
  697. self.login_as(user=self.user)
  698. url = f"{self.path}?id={group.id}"
  699. response = self.client.put(
  700. url,
  701. data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
  702. format="json",
  703. )
  704. assert response.status_code == 400
  705. assert (
  706. response.data["statusDetails"]["inUpcomingRelease"][0]
  707. == "No release data present in the system."
  708. )
  709. def test_set_resolved_in_explicit_commit_unreleased(self):
  710. repo = self.create_repo(project=self.project, name=self.project.name)
  711. commit = self.create_commit(project=self.project, repo=repo)
  712. group = self.create_group(status=GroupStatus.UNRESOLVED)
  713. self.login_as(user=self.user)
  714. url = f"{self.path}?id={group.id}"
  715. response = self.client.put(
  716. url,
  717. data={
  718. "status": "resolved",
  719. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  720. },
  721. format="json",
  722. )
  723. assert response.status_code == 200
  724. assert response.data["status"] == "resolved"
  725. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  726. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  727. group = Group.objects.get(id=group.id)
  728. assert group.status == GroupStatus.RESOLVED
  729. link = GroupLink.objects.get(group_id=group.id)
  730. assert link.linked_type == GroupLink.LinkedType.commit
  731. assert link.relationship == GroupLink.Relationship.resolves
  732. assert link.linked_id == commit.id
  733. assert GroupSubscription.objects.filter(
  734. user_id=self.user.id, group=group, is_active=True
  735. ).exists()
  736. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  737. assert activity.data["commit"] == commit.id
  738. def test_set_resolved_in_explicit_commit_released(self):
  739. release = self.create_release(project=self.project)
  740. repo = self.create_repo(project=self.project, name=self.project.name)
  741. commit = self.create_commit(project=self.project, repo=repo, release=release)
  742. group = self.create_group(status=GroupStatus.UNRESOLVED)
  743. self.login_as(user=self.user)
  744. url = f"{self.path}?id={group.id}"
  745. response = self.client.put(
  746. url,
  747. data={
  748. "status": "resolved",
  749. "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
  750. },
  751. format="json",
  752. )
  753. assert response.status_code == 200
  754. assert response.data["status"] == "resolved"
  755. assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
  756. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  757. group = Group.objects.get(id=group.id)
  758. assert group.status == GroupStatus.RESOLVED
  759. link = GroupLink.objects.get(group_id=group.id)
  760. assert link.project_id == self.project.id
  761. assert link.linked_type == GroupLink.LinkedType.commit
  762. assert link.relationship == GroupLink.Relationship.resolves
  763. assert link.linked_id == commit.id
  764. assert GroupSubscription.objects.filter(
  765. user_id=self.user.id, group=group, is_active=True
  766. ).exists()
  767. activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
  768. assert activity.data["commit"] == commit.id
  769. resolution = GroupResolution.objects.get(group=group)
  770. assert resolution.type == GroupResolution.Type.in_release
  771. assert resolution.status == GroupResolution.Status.resolved
  772. def test_set_resolved_in_explicit_commit_missing(self):
  773. repo = self.create_repo(project=self.project, name=self.project.name)
  774. group = self.create_group(status=GroupStatus.UNRESOLVED)
  775. self.login_as(user=self.user)
  776. url = f"{self.path}?id={group.id}"
  777. response = self.client.put(
  778. url,
  779. data={
  780. "status": "resolved",
  781. "statusDetails": {"inCommit": {"commit": "a" * 40, "repository": repo.name}},
  782. },
  783. format="json",
  784. )
  785. assert response.status_code == 400
  786. assert (
  787. response.data["statusDetails"]["inCommit"]["commit"][0]
  788. == "Unable to find the given commit."
  789. )
  790. def test_set_unresolved(self):
  791. release = self.create_release(project=self.project, version="abc")
  792. group = self.create_group(status=GroupStatus.RESOLVED)
  793. GroupResolution.objects.create(group=group, release=release)
  794. self.login_as(user=self.user)
  795. url = f"{self.path}?id={group.id}"
  796. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  797. assert response.status_code == 200
  798. assert response.data == {"status": "unresolved", "statusDetails": {}}
  799. group = Group.objects.get(id=group.id)
  800. assert group.status == GroupStatus.UNRESOLVED
  801. self.assertNoResolution(group)
  802. assert GroupSubscription.objects.filter(
  803. user_id=self.user.id, group=group, is_active=True
  804. ).exists()
  805. def test_set_unresolved_on_snooze(self):
  806. group = self.create_group(status=GroupStatus.IGNORED)
  807. GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(days=1))
  808. self.login_as(user=self.user)
  809. url = f"{self.path}?id={group.id}"
  810. response = self.client.put(url, data={"status": "unresolved"}, format="json")
  811. assert response.status_code == 200
  812. assert response.data == {"status": "unresolved", "statusDetails": {}}
  813. group = Group.objects.get(id=group.id)
  814. assert group.status == GroupStatus.UNRESOLVED
  815. def test_basic_ignore(self):
  816. group = self.create_group(status=GroupStatus.RESOLVED)
  817. snooze = GroupSnooze.objects.create(group=group, until=timezone.now())
  818. self.login_as(user=self.user)
  819. url = f"{self.path}?id={group.id}"
  820. response = self.client.put(url, data={"status": "ignored"}, format="json")
  821. assert response.status_code == 200
  822. # existing snooze objects should be cleaned up
  823. assert not GroupSnooze.objects.filter(id=snooze.id).exists()
  824. group = Group.objects.get(id=group.id)
  825. assert group.status == GroupStatus.IGNORED
  826. assert response.data == {"status": "ignored", "statusDetails": {}, "inbox": None}
  827. def test_snooze_duration(self):
  828. group = self.create_group(status=GroupStatus.RESOLVED)
  829. self.login_as(user=self.user)
  830. url = f"{self.path}?id={group.id}"
  831. response = self.client.put(
  832. url, data={"status": "ignored", "ignoreDuration": 30}, format="json"
  833. )
  834. assert response.status_code == 200
  835. snooze = GroupSnooze.objects.get(group=group)
  836. now = timezone.now()
  837. assert snooze.count is None
  838. assert snooze.until is not None
  839. assert snooze.until > now + timedelta(minutes=29)
  840. assert snooze.until < now + timedelta(minutes=31)
  841. assert snooze.user_count is None
  842. assert snooze.user_window is None
  843. assert snooze.window is None
  844. assert response.data["status"] == "ignored"
  845. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  846. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  847. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  848. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  849. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  850. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  851. def test_snooze_count(self):
  852. group = self.create_group(status=GroupStatus.RESOLVED, times_seen=1)
  853. self.login_as(user=self.user)
  854. url = f"{self.path}?id={group.id}"
  855. response = self.client.put(
  856. url, data={"status": "ignored", "ignoreCount": 100}, format="json"
  857. )
  858. assert response.status_code == 200
  859. snooze = GroupSnooze.objects.get(group=group)
  860. assert snooze.count == 100
  861. assert snooze.until is None
  862. assert snooze.user_count is None
  863. assert snooze.user_window is None
  864. assert snooze.window is None
  865. assert snooze.state is not None
  866. assert snooze.state["times_seen"] == 1
  867. assert response.data["status"] == "ignored"
  868. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  869. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  870. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  871. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  872. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  873. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  874. def test_snooze_user_count(self):
  875. for i in range(10):
  876. event = self.store_event(
  877. data={
  878. "fingerprint": ["put-me-in-group-1"],
  879. "user": {"id": str(i)},
  880. "timestamp": iso_format(self.min_ago + timedelta(seconds=i)),
  881. },
  882. project_id=self.project.id,
  883. )
  884. group = Group.objects.get(id=event.group.id)
  885. group.status = GroupStatus.RESOLVED
  886. group.substatus = None
  887. group.save()
  888. self.login_as(user=self.user)
  889. url = f"{self.path}?id={group.id}"
  890. response = self.client.put(
  891. url, data={"status": "ignored", "ignoreUserCount": 10}, format="json"
  892. )
  893. assert response.status_code == 200
  894. snooze = GroupSnooze.objects.get(group=group)
  895. assert snooze.count is None
  896. assert snooze.until is None
  897. assert snooze.user_count == 10
  898. assert snooze.user_window is None
  899. assert snooze.window is None
  900. assert snooze.state is not None
  901. assert snooze.state["users_seen"] == 10
  902. assert response.data["status"] == "ignored"
  903. assert response.data["statusDetails"]["ignoreCount"] == snooze.count
  904. assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
  905. assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
  906. assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
  907. assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
  908. assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
  909. def test_set_bookmarked(self):
  910. group1 = self.create_group(status=GroupStatus.RESOLVED)
  911. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  912. group3 = self.create_group(status=GroupStatus.IGNORED)
  913. group4 = self.create_group(
  914. project=self.create_project(slug="foo"),
  915. status=GroupStatus.UNRESOLVED,
  916. )
  917. self.login_as(user=self.user)
  918. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  919. response = self.client.put(url, data={"isBookmarked": "true"}, format="json")
  920. assert response.status_code == 200
  921. assert response.data == {"isBookmarked": True}
  922. bookmark1 = GroupBookmark.objects.filter(group=group1, user_id=self.user.id)
  923. assert bookmark1.exists()
  924. assert GroupSubscription.objects.filter(
  925. user_id=self.user.id, group=group1, is_active=True
  926. ).exists()
  927. bookmark2 = GroupBookmark.objects.filter(group=group2, user_id=self.user.id)
  928. assert bookmark2.exists()
  929. assert GroupSubscription.objects.filter(
  930. user_id=self.user.id, group=group2, is_active=True
  931. ).exists()
  932. bookmark3 = GroupBookmark.objects.filter(group=group3, user_id=self.user.id)
  933. assert not bookmark3.exists()
  934. bookmark4 = GroupBookmark.objects.filter(group=group4, user_id=self.user.id)
  935. assert not bookmark4.exists()
  936. def test_subscription(self):
  937. group1 = self.create_group()
  938. group2 = self.create_group()
  939. group3 = self.create_group()
  940. group4 = self.create_group(project=self.create_project(slug="foo"))
  941. self.login_as(user=self.user)
  942. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  943. response = self.client.put(url, data={"isSubscribed": "true"}, format="json")
  944. assert response.status_code == 200
  945. assert response.data == {"isSubscribed": True, "subscriptionDetails": {"reason": "unknown"}}
  946. assert GroupSubscription.objects.filter(
  947. group=group1, user_id=self.user.id, is_active=True
  948. ).exists()
  949. assert GroupSubscription.objects.filter(
  950. group=group2, user_id=self.user.id, is_active=True
  951. ).exists()
  952. assert not GroupSubscription.objects.filter(group=group3, user_id=self.user.id).exists()
  953. assert not GroupSubscription.objects.filter(group=group4, user_id=self.user.id).exists()
  954. def test_set_public(self):
  955. group1 = self.create_group()
  956. group2 = self.create_group()
  957. self.login_as(user=self.user)
  958. url = f"{self.path}?id={group1.id}&id={group2.id}"
  959. response = self.client.put(url, data={"isPublic": "true"}, format="json")
  960. assert response.status_code == 200
  961. assert response.data["isPublic"] is True
  962. assert "shareId" in response.data
  963. new_group1 = Group.objects.get(id=group1.id)
  964. assert bool(new_group1.get_share_id())
  965. new_group2 = Group.objects.get(id=group2.id)
  966. assert bool(new_group2.get_share_id())
  967. def test_set_private(self):
  968. group1 = self.create_group()
  969. group2 = self.create_group()
  970. # Manually mark them as shared
  971. for g in group1, group2:
  972. GroupShare.objects.create(project_id=g.project_id, group=g)
  973. assert bool(g.get_share_id())
  974. self.login_as(user=self.user)
  975. url = f"{self.path}?id={group1.id}&id={group2.id}"
  976. response = self.client.put(url, data={"isPublic": "false"}, format="json")
  977. assert response.status_code == 200
  978. assert response.data == {"isPublic": False, "shareId": None}
  979. new_group1 = Group.objects.get(id=group1.id)
  980. assert not bool(new_group1.get_share_id())
  981. new_group2 = Group.objects.get(id=group2.id)
  982. assert not bool(new_group2.get_share_id())
  983. def test_set_has_seen(self):
  984. group1 = self.create_group(status=GroupStatus.RESOLVED)
  985. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  986. group3 = self.create_group(status=GroupStatus.IGNORED)
  987. group4 = self.create_group(
  988. project=self.create_project(slug="foo"),
  989. status=GroupStatus.UNRESOLVED,
  990. )
  991. self.login_as(user=self.user)
  992. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  993. response = self.client.put(url, data={"hasSeen": "true"}, format="json")
  994. assert response.status_code == 200
  995. assert response.data == {"hasSeen": True}
  996. r1 = GroupSeen.objects.filter(group=group1, user_id=self.user.id)
  997. assert r1.exists()
  998. r2 = GroupSeen.objects.filter(group=group2, user_id=self.user.id)
  999. assert r2.exists()
  1000. r3 = GroupSeen.objects.filter(group=group3, user_id=self.user.id)
  1001. assert not r3.exists()
  1002. r4 = GroupSeen.objects.filter(group=group4, user_id=self.user.id)
  1003. assert not r4.exists()
  1004. def test_inbox_fields(self):
  1005. group1 = self.create_group(status=GroupStatus.RESOLVED)
  1006. add_group_to_inbox(group1, GroupInboxReason.NEW)
  1007. self.login_as(user=self.user)
  1008. url = f"{self.path}?id={group1.id}"
  1009. response = self.client.put(url, data={"status": "resolved"}, format="json")
  1010. assert "inbox" in response.data
  1011. assert response.data["inbox"] is None
  1012. @patch("sentry.issues.merge.uuid4")
  1013. @patch("sentry.issues.merge.merge_groups")
  1014. @patch("sentry.eventstream.backend")
  1015. def test_merge(self, mock_eventstream, merge_groups, mock_uuid4):
  1016. eventstream_state = object()
  1017. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  1018. mock_uuid4.return_value = self.get_mock_uuid()
  1019. group1 = self.create_group(times_seen=1)
  1020. group2 = self.create_group(times_seen=50)
  1021. group3 = self.create_group(times_seen=2)
  1022. self.create_group()
  1023. self.login_as(user=self.user)
  1024. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  1025. response = self.client.put(url, data={"merge": "1"}, format="json")
  1026. assert response.status_code == 200
  1027. assert response.data["merge"]["parent"] == str(group2.id)
  1028. assert sorted(response.data["merge"]["children"]) == sorted(
  1029. [str(group1.id), str(group3.id)]
  1030. )
  1031. mock_eventstream.start_merge.assert_called_once_with(
  1032. group1.project_id, [group3.id, group1.id], group2.id
  1033. )
  1034. assert len(merge_groups.mock_calls) == 1
  1035. merge_groups.delay.assert_any_call(
  1036. from_object_ids=[group3.id, group1.id],
  1037. to_object_id=group2.id,
  1038. transaction_id="abc123",
  1039. eventstream_state=eventstream_state,
  1040. )
  1041. @patch("sentry.issues.merge.uuid4")
  1042. @patch("sentry.issues.merge.merge_groups")
  1043. @patch("sentry.eventstream.backend")
  1044. def test_merge_performance_issues(self, mock_eventstream, merge_groups, mock_uuid4):
  1045. eventstream_state = object()
  1046. mock_eventstream.start_merge = Mock(return_value=eventstream_state)
  1047. mock_uuid4.return_value = self.get_mock_uuid()
  1048. group1 = self.create_group(times_seen=1, type=PerformanceSlowDBQueryGroupType.type_id)
  1049. group2 = self.create_group(times_seen=50, type=PerformanceSlowDBQueryGroupType.type_id)
  1050. group3 = self.create_group(times_seen=2, type=PerformanceSlowDBQueryGroupType.type_id)
  1051. self.create_group()
  1052. self.login_as(user=self.user)
  1053. url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
  1054. response = self.client.put(url, data={"merge": "1"}, format="json")
  1055. assert response.status_code == 400, response.content
  1056. def test_assign(self):
  1057. group1 = self.create_group(is_public=True)
  1058. group2 = self.create_group(is_public=True)
  1059. user = self.user
  1060. self.login_as(user=user)
  1061. url = f"{self.path}?id={group1.id}"
  1062. response = self.client.put(url, data={"assignedTo": user.username})
  1063. assert response.status_code == 200
  1064. assert response.data["assignedTo"]["id"] == str(user.id)
  1065. assert response.data["assignedTo"]["type"] == "user"
  1066. assert GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
  1067. assert not GroupAssignee.objects.filter(group=group2, user_id=user.id).exists()
  1068. assert (
  1069. Activity.objects.filter(
  1070. group=group1, user_id=user.id, type=ActivityType.ASSIGNED.value
  1071. ).count()
  1072. == 1
  1073. )
  1074. assert GroupSubscription.objects.filter(
  1075. user_id=user.id, group=group1, is_active=True
  1076. ).exists()
  1077. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1078. assert response.status_code == 200, response.content
  1079. assert response.data["assignedTo"] is None
  1080. assert not GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
  1081. def test_assign_non_member(self):
  1082. group = self.create_group(is_public=True)
  1083. member = self.user
  1084. non_member = self.create_user("bar@example.com")
  1085. self.login_as(user=member)
  1086. url = f"{self.path}?id={group.id}"
  1087. response = self.client.put(url, data={"assignedTo": non_member.username}, format="json")
  1088. assert response.status_code == 400, response.content
  1089. def test_assign_team(self):
  1090. self.login_as(user=self.user)
  1091. group = self.create_group()
  1092. other_member = self.create_user("bar@example.com")
  1093. team = self.create_team(
  1094. organization=group.project.organization, members=[self.user, other_member]
  1095. )
  1096. group.project.add_team(team)
  1097. url = f"{self.path}?id={group.id}"
  1098. response = self.client.put(url, data={"assignedTo": f"team:{team.id}"})
  1099. assert response.status_code == 200
  1100. assert response.data["assignedTo"]["id"] == str(team.id)
  1101. assert response.data["assignedTo"]["type"] == "team"
  1102. assert GroupAssignee.objects.filter(group=group, team=team).exists()
  1103. assert Activity.objects.filter(group=group, type=ActivityType.ASSIGNED.value).count() == 1
  1104. assert GroupSubscription.objects.filter(group=group, is_active=True).count() == 2
  1105. response = self.client.put(url, data={"assignedTo": ""}, format="json")
  1106. assert response.status_code == 200, response.content
  1107. assert response.data["assignedTo"] is None
  1108. def test_discard(self):
  1109. group1 = self.create_group(is_public=True)
  1110. group2 = self.create_group(is_public=True)
  1111. group_hash = GroupHash.objects.create(hash="x" * 32, project=group1.project, group=group1)
  1112. user = self.user
  1113. self.login_as(user=user)
  1114. url = f"{self.path}?id={group1.id}"
  1115. with self.tasks():
  1116. with self.feature("projects:discard-groups"):
  1117. response = self.client.put(url, data={"discard": True})
  1118. assert response.status_code == 204
  1119. assert not Group.objects.filter(id=group1.id).exists()
  1120. assert Group.objects.filter(id=group2.id).exists()
  1121. assert GroupHash.objects.filter(id=group_hash.id).exists()
  1122. tombstone = GroupTombstone.objects.get(
  1123. id=GroupHash.objects.get(id=group_hash.id).group_tombstone_id
  1124. )
  1125. assert tombstone.message == group1.message
  1126. assert tombstone.culprit == group1.culprit
  1127. assert tombstone.project == group1.project
  1128. assert tombstone.data == group1.data
  1129. @patch(
  1130. "sentry.models.OrganizationMember.get_scopes",
  1131. return_value=frozenset(s for s in settings.SENTRY_SCOPES if s != "event:admin"),
  1132. )
  1133. def test_discard_requires_events_admin(self, mock_get_scopes):
  1134. group1 = self.create_group(is_public=True)
  1135. user = self.user
  1136. self.login_as(user=user)
  1137. url = f"{self.path}?id={group1.id}"
  1138. with self.tasks(), self.feature("projects:discard-groups"):
  1139. response = self.client.put(url, data={"discard": True})
  1140. assert response.status_code == 400
  1141. assert Group.objects.filter(id=group1.id).exists()
  1142. class GroupDeleteTest(APITestCase, SnubaTestCase):
  1143. @cached_property
  1144. def path(self):
  1145. return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
  1146. @patch("sentry.eventstream.backend")
  1147. def test_delete_by_id(self, mock_eventstream):
  1148. eventstream_state = {"event_stream_state": uuid4()}
  1149. mock_eventstream.start_delete_groups = Mock(return_value=eventstream_state)
  1150. group1 = self.create_group(status=GroupStatus.RESOLVED)
  1151. group2 = self.create_group(status=GroupStatus.UNRESOLVED)
  1152. group3 = self.create_group(status=GroupStatus.IGNORED)
  1153. group4 = self.create_group(
  1154. project=self.create_project(slug="foo"),
  1155. status=GroupStatus.UNRESOLVED,
  1156. )
  1157. hashes = []
  1158. for g in group1, group2, group3, group4:
  1159. hash = uuid4().hex
  1160. hashes.append(hash)
  1161. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1162. self.login_as(user=self.user)
  1163. url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
  1164. response = self.client.delete(url, format="json")
  1165. mock_eventstream.start_delete_groups.assert_called_once_with(
  1166. group1.project_id, [group1.id, group2.id]
  1167. )
  1168. assert response.status_code == 204
  1169. assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
  1170. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1171. assert Group.objects.get(id=group2.id).status == GroupStatus.PENDING_DELETION
  1172. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1173. assert Group.objects.get(id=group3.id).status != GroupStatus.PENDING_DELETION
  1174. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1175. assert Group.objects.get(id=group4.id).status != GroupStatus.PENDING_DELETION
  1176. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1177. Group.objects.filter(id__in=(group1.id, group2.id)).update(status=GroupStatus.UNRESOLVED)
  1178. with self.tasks():
  1179. response = self.client.delete(url, format="json")
  1180. # XXX(markus): Something is sending duplicated replacements to snuba --
  1181. # once from within tasks.deletions.groups and another time from
  1182. # sentry.deletions.defaults.groups
  1183. assert mock_eventstream.end_delete_groups.call_args_list == [
  1184. call(eventstream_state),
  1185. call(eventstream_state),
  1186. ]
  1187. assert response.status_code == 204
  1188. assert not Group.objects.filter(id=group1.id).exists()
  1189. assert not GroupHash.objects.filter(group_id=group1.id).exists()
  1190. assert not Group.objects.filter(id=group2.id).exists()
  1191. assert not GroupHash.objects.filter(group_id=group2.id).exists()
  1192. assert Group.objects.filter(id=group3.id).exists()
  1193. assert GroupHash.objects.filter(group_id=group3.id).exists()
  1194. assert Group.objects.filter(id=group4.id).exists()
  1195. assert GroupHash.objects.filter(group_id=group4.id).exists()
  1196. @patch("sentry.eventstream.backend")
  1197. def test_delete_performance_issue_by_id(self, mock_eventstream):
  1198. eventstream_state = {"event_stream_state": uuid4()}
  1199. mock_eventstream.start_delete_groups = Mock(return_value=eventstream_state)
  1200. group1 = self.create_group(
  1201. status=GroupStatus.RESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
  1202. )
  1203. group2 = self.create_group(
  1204. status=GroupStatus.UNRESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
  1205. )
  1206. hashes = []
  1207. for g in group1, group2:
  1208. hash = uuid4().hex
  1209. hashes.append(hash)
  1210. GroupHash.objects.create(project=g.project, hash=hash, group=g)
  1211. self.login_as(user=self.user)
  1212. url = f"{self.path}?id={group1.id}&id={group2.id}"
  1213. response = self.client.delete(url, format="json")
  1214. assert response.status_code == 400
  1215. assert Group.objects.filter(id=group1.id).exists()
  1216. assert GroupHash.objects.filter(group_id=group1.id).exists()
  1217. assert Group.objects.filter(id=group2.id).exists()
  1218. assert GroupHash.objects.filter(group_id=group2.id).exists()
  1219. def test_bulk_delete(self):
  1220. groups = []
  1221. for i in range(10, 41):
  1222. groups.append(
  1223. self.create_group(
  1224. project=self.project,
  1225. status=GroupStatus.RESOLVED,
  1226. )
  1227. )
  1228. hashes = []
  1229. for group in groups:
  1230. hash = uuid4().hex
  1231. hashes.append(hash)
  1232. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1233. self.login_as(user=self.user)
  1234. # if query is '' it defaults to is:unresolved
  1235. url = self.path + "?query="
  1236. response = self.client.delete(url, format="json")
  1237. assert response.status_code == 204
  1238. for group in groups:
  1239. assert Group.objects.get(id=group.id).status == GroupStatus.PENDING_DELETION
  1240. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1241. Group.objects.filter(id__in=[group.id for group in groups]).update(
  1242. status=GroupStatus.UNRESOLVED
  1243. )
  1244. with self.tasks():
  1245. response = self.client.delete(url, format="json")
  1246. assert response.status_code == 204
  1247. for group in groups:
  1248. assert not Group.objects.filter(id=group.id).exists()
  1249. assert not GroupHash.objects.filter(group_id=group.id).exists()
  1250. def test_bulk_delete_performance_issues(self):
  1251. groups = []
  1252. for i in range(10, 41):
  1253. groups.append(
  1254. self.create_group(
  1255. project=self.project,
  1256. status=GroupStatus.RESOLVED,
  1257. type=PerformanceSlowDBQueryGroupType.type_id,
  1258. )
  1259. )
  1260. hashes = []
  1261. for group in groups:
  1262. hash = uuid4().hex
  1263. hashes.append(hash)
  1264. GroupHash.objects.create(project=group.project, hash=hash, group=group)
  1265. self.login_as(user=self.user)
  1266. # if query is '' it defaults to is:unresolved
  1267. url = self.path + "?query="
  1268. response = self.client.delete(url, format="json")
  1269. assert response.status_code == 400
  1270. for group in groups:
  1271. assert Group.objects.filter(id=group.id).exists()
  1272. assert GroupHash.objects.filter(group_id=group.id).exists()