test_project_group_index.py 55 KB

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