test_project_group_index.py 56 KB

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