1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516 |
- import time
- from datetime import timedelta
- from functools import cached_property
- from unittest.mock import Mock, patch
- from urllib.parse import quote
- from uuid import uuid4
- from django.conf import settings
- from django.utils import timezone
- from sentry.issues.grouptype import PerformanceSlowDBQueryGroupType
- from sentry.models import (
- Activity,
- ApiToken,
- ExternalIssue,
- Group,
- GroupAssignee,
- GroupBookmark,
- GroupHash,
- GroupLink,
- GroupResolution,
- GroupSeen,
- GroupShare,
- GroupSnooze,
- GroupStatus,
- GroupSubscription,
- GroupTombstone,
- Integration,
- OrganizationIntegration,
- Release,
- UserOption,
- )
- from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox
- from sentry.testutils import APITestCase, SnubaTestCase
- from sentry.testutils.helpers import parse_link_header
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.testutils.silo import region_silo_test
- from sentry.types.activity import ActivityType
- from sentry.utils import json
- @region_silo_test
- class GroupListTest(APITestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.min_ago = before_now(minutes=1)
- def _parse_links(self, header):
- # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
- links = {}
- for url, attrs in parse_link_header(header).items():
- links[attrs["rel"]] = attrs
- attrs["href"] = url
- return links
- @cached_property
- def path(self):
- return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
- def test_sort_by_date_with_tag(self):
- # XXX(dcramer): this tests a case where an ambiguous column name existed
- group1 = self.create_group(last_seen=before_now(seconds=1))
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(group1.id)
- def test_invalid_query(self):
- self.create_group(last_seen=before_now(seconds=1))
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?sort_by=date&query=timesSeen:>1t", format="json")
- assert response.status_code == 400
- assert "Error parsing search query" in response.data["detail"]
- def test_simple_pagination(self):
- event1 = self.store_event(
- data={
- "fingerprint": ["put-me-in-group-1"],
- "timestamp": iso_format(self.min_ago - timedelta(seconds=2)),
- },
- project_id=self.project.id,
- )
- event2 = self.store_event(
- data={
- "fingerprint": ["put-me-in-group-2"],
- "timestamp": iso_format(self.min_ago - timedelta(seconds=1)),
- },
- project_id=self.project.id,
- )
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?sort_by=date&limit=1", format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(event2.group.id)
- links = self._parse_links(response["Link"])
- assert links["previous"]["results"] == "false"
- assert links["next"]["results"] == "true"
- response = self.client.get(links["next"]["href"], format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(event1.group.id)
- links = self._parse_links(response["Link"])
- assert links["previous"]["results"] == "true"
- assert links["next"]["results"] == "false"
- def test_stats_period(self):
- # TODO(dcramer): this test really only checks if validation happens
- # on statsPeriod
- self.create_group(last_seen=before_now(seconds=1))
- self.create_group(last_seen=timezone.now())
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?statsPeriod=24h", format="json")
- assert response.status_code == 200
- response = self.client.get(f"{self.path}?statsPeriod=14d", format="json")
- assert response.status_code == 200
- response = self.client.get(f"{self.path}?statsPeriod=", format="json")
- assert response.status_code == 200
- time.sleep(1)
- response = self.client.get(f"{self.path}?statsPeriod=48h", format="json")
- assert response.status_code == 400
- def test_environment(self):
- self.store_event(
- data={
- "fingerprint": ["put-me-in-group1"],
- "timestamp": iso_format(self.min_ago),
- "environment": "production",
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "fingerprint": ["put-me-in-group2"],
- "timestamp": iso_format(self.min_ago),
- "environment": "staging",
- },
- project_id=self.project.id,
- )
- self.login_as(user=self.user)
- response = self.client.get(self.path + "?environment=production", format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- response = self.client.get(self.path + "?environment=garbage", format="json")
- assert response.status_code == 200
- assert len(response.data) == 0
- def test_auto_resolved(self):
- project = self.project
- project.update_option("sentry:resolve_age", 1)
- self.create_group(last_seen=before_now(days=1))
- group2 = self.create_group(last_seen=timezone.now())
- self.login_as(user=self.user)
- response = self.client.get(self.path, format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(group2.id)
- def test_lookup_by_event_id(self):
- project = self.project
- project.update_option("sentry:resolve_age", 1)
- event_id = "c" * 32
- event = self.store_event(
- data={"event_id": event_id, "timestamp": iso_format(self.min_ago)},
- project_id=self.project.id,
- )
- self.login_as(user=self.user)
- response = self.client.get("{}?query={}".format(self.path, "c" * 32), format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(event.group.id)
- assert response.data[0]["matchingEventId"] == event.event_id
- def test_lookup_by_event_with_matching_environment(self):
- project = self.project
- project.update_option("sentry:resolve_age", 1)
- self.create_environment(name="test", project=project)
- event = self.store_event(
- data={"environment": "test", "timestamp": iso_format(self.min_ago)},
- project_id=self.project.id,
- )
- self.login_as(user=self.user)
- response = self.client.get(
- f"{self.path}?query={event.event_id}&environment=test", format="json"
- )
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(event.group.id)
- assert response.data[0]["matchingEventId"] == event.event_id
- assert response.data[0]["matchingEventEnvironment"] == "test"
- def test_lookup_by_event_id_with_whitespace(self):
- project = self.project
- project.update_option("sentry:resolve_age", 1)
- event = self.store_event(
- data={"event_id": "c" * 32, "timestamp": iso_format(self.min_ago)},
- project_id=self.project.id,
- )
- self.login_as(user=self.user)
- response = self.client.get(
- "{}?query=%20%20{}%20%20".format(self.path, "c" * 32), format="json"
- )
- assert response.status_code == 200
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(event.group.id)
- def test_lookup_by_unknown_event_id(self):
- project = self.project
- project.update_option("sentry:resolve_age", 1)
- self.create_group()
- self.create_group()
- self.login_as(user=self.user)
- response = self.client.get("{}?query={}".format(self.path, "c" * 32), format="json")
- assert response.status_code == 200
- assert len(response.data) == 0
- def test_lookup_by_short_id(self):
- group = self.group
- short_id = group.qualified_short_id
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?query={short_id}&shortIdLookup=1", format="json")
- assert response.status_code == 200
- assert len(response.data) == 1
- def test_lookup_by_short_id_no_perms(self):
- organization = self.create_organization()
- project = self.create_project(organization=organization)
- project2 = self.create_project(organization=organization)
- team = self.create_team(organization=organization)
- project2.add_team(team)
- group = self.create_group(project=project)
- user = self.create_user()
- self.create_member(organization=organization, user=user, teams=[team])
- short_id = group.qualified_short_id
- self.login_as(user=user)
- path = f"/api/0/projects/{organization.slug}/{project2.slug}/issues/"
- response = self.client.get(f"{path}?query={short_id}&shortIdLookup=1", format="json")
- assert response.status_code == 200
- assert len(response.data) == 0
- def test_lookup_by_first_release(self):
- self.login_as(self.user)
- project = self.project
- project2 = self.create_project(name="baz", organization=project.organization)
- release = Release.objects.create(organization=project.organization, version="12345")
- release.add_project(project)
- release.add_project(project2)
- group = self.store_event(
- data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
- project_id=project.id,
- ).group
- self.store_event(
- data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))},
- project_id=project2.id,
- )
- url = "{}?query={}".format(self.path, 'first-release:"%s"' % release.version)
- response = self.client.get(url, format="json")
- issues = json.loads(response.content)
- assert response.status_code == 200
- assert len(issues) == 1
- assert int(issues[0]["id"]) == group.id
- def test_lookup_by_release(self):
- self.login_as(self.user)
- version = "12345"
- event = self.store_event(
- data={"tags": {"sentry:release": version}}, project_id=self.project.id
- )
- group = event.group
- url = "{}?query={}".format(self.path, quote('release:"%s"' % version))
- response = self.client.get(url, format="json")
- issues = json.loads(response.content)
- assert response.status_code == 200
- assert len(issues) == 1
- assert int(issues[0]["id"]) == group.id
- def test_lookup_by_release_wildcard(self):
- self.login_as(self.user)
- version = "12345"
- event = self.store_event(
- data={"tags": {"sentry:release": version}}, project_id=self.project.id
- )
- group = event.group
- release_wildcard = version[:3] + "*"
- url = "{}?query={}".format(self.path, quote('release:"%s"' % release_wildcard))
- response = self.client.get(url, format="json")
- issues = json.loads(response.content)
- assert response.status_code == 200
- assert len(issues) == 1
- assert int(issues[0]["id"]) == group.id
- def test_pending_delete_pending_merge_excluded(self):
- self.create_group(status=GroupStatus.PENDING_DELETION)
- group = self.create_group()
- self.create_group(status=GroupStatus.DELETION_IN_PROGRESS)
- self.create_group(status=GroupStatus.PENDING_MERGE)
- self.login_as(user=self.user)
- response = self.client.get(self.path, format="json")
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(group.id)
- def test_filters_based_on_retention(self):
- self.login_as(user=self.user)
- self.create_group(last_seen=timezone.now() - timedelta(days=2))
- with self.options({"system.event-retention-days": 1}):
- response = self.client.get(self.path)
- assert response.status_code == 200, response.content
- assert len(response.data) == 0
- def test_token_auth(self):
- token = ApiToken.objects.create(user=self.user, scopes=256)
- response = self.client.get(
- self.path, format="json", HTTP_AUTHORIZATION=f"Bearer {token.token}"
- )
- assert response.status_code == 200, response.content
- def test_filter_not_unresolved(self):
- event = self.store_event(
- data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
- project_id=self.project.id,
- )
- event.group.update(status=GroupStatus.RESOLVED)
- self.login_as(user=self.user)
- response = self.client.get(f"{self.path}?query=!is:unresolved", format="json")
- assert response.status_code == 200
- assert [int(r["id"]) for r in response.data] == [event.group.id]
- @region_silo_test
- class GroupUpdateTest(APITestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.min_ago = timezone.now() - timedelta(minutes=1)
- @cached_property
- def path(self):
- return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
- def assertNoResolution(self, group):
- assert not GroupResolution.objects.filter(group=group).exists()
- def test_global_resolve(self):
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- group2 = self.create_group(status=GroupStatus.UNRESOLVED)
- group3 = self.create_group(status=GroupStatus.IGNORED)
- group4 = self.create_group(
- project=self.create_project(slug="foo"),
- status=GroupStatus.UNRESOLVED,
- )
- self.login_as(user=self.user)
- response = self.client.put(
- f"{self.path}?status=unresolved", data={"status": "resolved"}, format="json"
- )
- assert response.status_code == 200, response.data
- assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
- # the previously resolved entry should not be included
- new_group1 = Group.objects.get(id=group1.id)
- assert new_group1.status == GroupStatus.RESOLVED
- assert new_group1.resolved_at is None
- # this wont exist because it wasn't affected
- assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group1).exists()
- new_group2 = Group.objects.get(id=group2.id)
- assert new_group2.status == GroupStatus.RESOLVED
- assert new_group2.resolved_at is not None
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=new_group2, is_active=True
- ).exists()
- # the ignored entry should not be included
- new_group3 = Group.objects.get(id=group3.id)
- assert new_group3.status == GroupStatus.IGNORED
- assert new_group3.resolved_at is None
- assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group3)
- new_group4 = Group.objects.get(id=group4.id)
- assert new_group4.status == GroupStatus.UNRESOLVED
- assert new_group4.resolved_at is None
- assert not GroupSubscription.objects.filter(user_id=self.user.id, group=new_group4)
- def test_bulk_resolve(self):
- self.login_as(user=self.user)
- for i in range(200):
- self.create_group(status=GroupStatus.UNRESOLVED)
- response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
- assert len(response.data) == 100
- response = self.client.put(
- f"{self.path}?status=unresolved", data={"status": "resolved"}, format="json"
- )
- assert response.status_code == 200, response.data
- assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
- response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
- assert len(response.data) == 0
- @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
- def test_resolve_with_integration(self, mock_sync_status_outbound):
- self.login_as(user=self.user)
- org = self.organization
- integration = Integration.objects.create(provider="example", name="Example")
- integration.add_organization(org, self.user)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- OrganizationIntegration.objects.filter(
- integration_id=integration.id, organization_id=group.organization.id
- ).update(
- config={
- "sync_comments": True,
- "sync_status_outbound": True,
- "sync_status_inbound": True,
- "sync_assignee_outbound": True,
- "sync_assignee_inbound": True,
- }
- )
- external_issue = ExternalIssue.objects.get_or_create(
- organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
- )[0]
- GroupLink.objects.get_or_create(
- group_id=group.id,
- project_id=group.project_id,
- linked_type=GroupLink.LinkedType.issue,
- linked_id=external_issue.id,
- relationship=GroupLink.Relationship.references,
- )[0]
- response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
- assert len(response.data) == 1
- with self.tasks():
- with self.feature({"organizations:integrations-issue-sync": True}):
- response = self.client.put(
- f"{self.path}?status=unresolved",
- data={"status": "resolved"},
- format="json",
- )
- assert response.status_code == 200, response.data
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
- mock_sync_status_outbound.assert_called_once_with(
- external_issue, True, group.project_id
- )
- response = self.client.get(f"{self.path}?sort_by=date&query=is:unresolved", format="json")
- assert len(response.data) == 0
- @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
- def test_set_unresolved_with_integration(self, mock_sync_status_outbound):
- release = self.create_release(project=self.project, version="abc")
- group = self.create_group(status=GroupStatus.RESOLVED)
- org = self.organization
- integration = Integration.objects.create(provider="example", name="Example")
- integration.add_organization(org, self.user)
- OrganizationIntegration.objects.filter(
- integration_id=integration.id, organization_id=group.organization.id
- ).update(
- config={
- "sync_comments": True,
- "sync_status_outbound": True,
- "sync_status_inbound": True,
- "sync_assignee_outbound": True,
- "sync_assignee_inbound": True,
- }
- )
- GroupResolution.objects.create(group=group, release=release)
- external_issue = ExternalIssue.objects.get_or_create(
- organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
- )[0]
- GroupLink.objects.get_or_create(
- group_id=group.id,
- project_id=group.project_id,
- linked_type=GroupLink.LinkedType.issue,
- linked_id=external_issue.id,
- relationship=GroupLink.Relationship.references,
- )[0]
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- with self.tasks():
- with self.feature({"organizations:integrations-issue-sync": True}):
- response = self.client.put(url, data={"status": "unresolved"}, format="json")
- assert response.status_code == 200
- assert response.data == {"status": "unresolved", "statusDetails": {}}
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.UNRESOLVED
- self.assertNoResolution(group)
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- mock_sync_status_outbound.assert_called_once_with(
- external_issue, False, group.project_id
- )
- def test_self_assign_issue(self):
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- user = self.user
- uo1 = UserOption.objects.create(key="self_assign_issue", value="1", project=None, user=user)
- self.login_as(user=user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "resolved"}, format="json")
- assert response.status_code == 200, response.data
- assert response.data["assignedTo"]["id"] == str(user.id)
- assert response.data["assignedTo"]["type"] == "user"
- assert response.data["status"] == "resolved"
- assert GroupAssignee.objects.filter(group=group, user_id=user.id).exists()
- assert GroupSubscription.objects.filter(
- user_id=user.id, group=group, is_active=True
- ).exists()
- uo1.delete()
- def test_self_assign_issue_next_release(self):
- release = Release.objects.create(organization_id=self.project.organization_id, version="a")
- release.add_project(self.project)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- uo1 = UserOption.objects.create(
- key="self_assign_issue", value="1", project=None, user=self.user
- )
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inNextRelease"]
- assert response.data["assignedTo"]["id"] == str(self.user.id)
- assert response.data["assignedTo"]["type"] == "user"
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- assert GroupResolution.objects.filter(group=group, release=release).exists()
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(
- group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
- )
- assert activity.data["version"] == ""
- uo1.delete()
- def test_selective_status_update(self):
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- group2 = self.create_group(status=GroupStatus.UNRESOLVED)
- group3 = self.create_group(status=GroupStatus.IGNORED)
- group4 = self.create_group(
- project=self.create_project(slug="foo"),
- status=GroupStatus.UNRESOLVED,
- )
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
- response = self.client.put(url, data={"status": "resolved"}, format="json")
- assert response.status_code == 200
- assert response.data == {"status": "resolved", "statusDetails": {}, "inbox": None}
- new_group1 = Group.objects.get(id=group1.id)
- assert new_group1.resolved_at is not None
- assert new_group1.status == GroupStatus.RESOLVED
- new_group2 = Group.objects.get(id=group2.id)
- assert new_group2.resolved_at is not None
- assert new_group2.status == GroupStatus.RESOLVED
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=new_group2, is_active=True
- ).exists()
- new_group3 = Group.objects.get(id=group3.id)
- assert new_group3.resolved_at is None
- assert new_group3.status == GroupStatus.IGNORED
- new_group4 = Group.objects.get(id=group4.id)
- assert new_group4.resolved_at is None
- assert new_group4.status == GroupStatus.UNRESOLVED
- def test_set_resolved_in_current_release(self):
- release = Release.objects.create(organization_id=self.project.organization_id, version="a")
- release.add_project(self.project)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={"status": "resolved", "statusDetails": {"inRelease": "latest"}},
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inRelease"] == release.version
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- assert "activity" in response.data
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- resolution = GroupResolution.objects.get(group=group)
- assert resolution.release == release
- assert resolution.type == GroupResolution.Type.in_release
- assert resolution.status == GroupResolution.Status.resolved
- assert resolution.actor_id == self.user.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(
- group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
- )
- assert activity.data["version"] == release.version
- def test_set_resolved_in_explicit_release(self):
- release = Release.objects.create(organization_id=self.project.organization_id, version="a")
- release.add_project(self.project)
- release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
- release2.add_project(self.project)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={"status": "resolved", "statusDetails": {"inRelease": release.version}},
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inRelease"] == release.version
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- assert "activity" in response.data
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- resolution = GroupResolution.objects.get(group=group)
- assert resolution.release == release
- assert resolution.type == GroupResolution.Type.in_release
- assert resolution.status == GroupResolution.Status.resolved
- assert resolution.actor_id == self.user.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(
- group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
- )
- assert activity.data["version"] == release.version
- def test_set_resolved_in_next_release(self):
- release = Release.objects.create(organization_id=self.project.organization_id, version="a")
- release.add_project(self.project)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={"status": "resolved", "statusDetails": {"inNextRelease": True}},
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inNextRelease"]
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- assert "activity" in response.data
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- resolution = GroupResolution.objects.get(group=group)
- assert resolution.release == release
- assert resolution.type == GroupResolution.Type.in_next_release
- assert resolution.status == GroupResolution.Status.pending
- assert resolution.actor_id == self.user.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(
- group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
- )
- assert activity.data["version"] == ""
- def test_set_resolved_in_next_release_legacy(self):
- release = Release.objects.create(organization_id=self.project.organization_id, version="a")
- release.add_project(self.project)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "resolvedInNextRelease"}, format="json")
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inNextRelease"]
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- resolution = GroupResolution.objects.get(group=group)
- assert resolution.release == release
- assert resolution.type == GroupResolution.Type.in_next_release
- assert resolution.status == GroupResolution.Status.pending
- assert resolution.actor_id == self.user.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(
- group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
- )
- assert activity.data["version"] == ""
- def test_set_resolved_in_explicit_commit_unreleased(self):
- repo = self.create_repo(project=self.project, name=self.project.name)
- commit = self.create_commit(project=self.project, repo=repo)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={
- "status": "resolved",
- "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
- },
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- link = GroupLink.objects.get(group_id=group.id)
- assert link.linked_type == GroupLink.LinkedType.commit
- assert link.relationship == GroupLink.Relationship.resolves
- assert link.linked_id == commit.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
- assert activity.data["commit"] == commit.id
- def test_set_resolved_in_explicit_commit_released(self):
- release = self.create_release(project=self.project)
- repo = self.create_repo(project=self.project, name=self.project.name)
- commit = self.create_commit(project=self.project, repo=repo, release=release)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={
- "status": "resolved",
- "statusDetails": {"inCommit": {"commit": commit.key, "repository": repo.name}},
- },
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inCommit"]["id"] == commit.key
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.RESOLVED
- link = GroupLink.objects.get(group_id=group.id)
- assert link.project_id == self.project.id
- assert link.linked_type == GroupLink.LinkedType.commit
- assert link.relationship == GroupLink.Relationship.resolves
- assert link.linked_id == commit.id
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- activity = Activity.objects.get(group=group, type=ActivityType.SET_RESOLVED_IN_COMMIT.value)
- assert activity.data["commit"] == commit.id
- resolution = GroupResolution.objects.get(group=group)
- assert resolution.type == GroupResolution.Type.in_release
- assert resolution.status == GroupResolution.Status.resolved
- def test_set_resolved_in_explicit_commit_missing(self):
- repo = self.create_repo(project=self.project, name=self.project.name)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url,
- data={
- "status": "resolved",
- "statusDetails": {"inCommit": {"commit": "a" * 40, "repository": repo.name}},
- },
- format="json",
- )
- assert response.status_code == 400
- assert (
- response.data["statusDetails"]["inCommit"]["commit"][0]
- == "Unable to find the given commit."
- )
- def test_set_unresolved(self):
- release = self.create_release(project=self.project, version="abc")
- group = self.create_group(status=GroupStatus.RESOLVED)
- GroupResolution.objects.create(group=group, release=release)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "unresolved"}, format="json")
- assert response.status_code == 200
- assert response.data == {"status": "unresolved", "statusDetails": {}}
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.UNRESOLVED
- self.assertNoResolution(group)
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group, is_active=True
- ).exists()
- def test_set_unresolved_on_snooze(self):
- group = self.create_group(status=GroupStatus.IGNORED)
- GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(days=1))
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "unresolved"}, format="json")
- assert response.status_code == 200
- assert response.data == {"status": "unresolved", "statusDetails": {}}
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.UNRESOLVED
- def test_basic_ignore(self):
- group = self.create_group(status=GroupStatus.RESOLVED)
- snooze = GroupSnooze.objects.create(group=group, until=timezone.now())
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"status": "ignored"}, format="json")
- assert response.status_code == 200
- # existing snooze objects should be cleaned up
- assert not GroupSnooze.objects.filter(id=snooze.id).exists()
- group = Group.objects.get(id=group.id)
- assert group.status == GroupStatus.IGNORED
- assert response.data == {"status": "ignored", "statusDetails": {}, "inbox": None}
- def test_snooze_duration(self):
- group = self.create_group(status=GroupStatus.RESOLVED)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url, data={"status": "ignored", "ignoreDuration": 30}, format="json"
- )
- assert response.status_code == 200
- snooze = GroupSnooze.objects.get(group=group)
- now = timezone.now()
- assert snooze.count is None
- assert snooze.until > now + timedelta(minutes=29)
- assert snooze.until < now + timedelta(minutes=31)
- assert snooze.user_count is None
- assert snooze.user_window is None
- assert snooze.window is None
- assert response.data["status"] == "ignored"
- assert response.data["statusDetails"]["ignoreCount"] == snooze.count
- assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
- assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
- assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
- assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- def test_snooze_count(self):
- group = self.create_group(status=GroupStatus.RESOLVED, times_seen=1)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url, data={"status": "ignored", "ignoreCount": 100}, format="json"
- )
- assert response.status_code == 200
- snooze = GroupSnooze.objects.get(group=group)
- assert snooze.count == 100
- assert snooze.until is None
- assert snooze.user_count is None
- assert snooze.user_window is None
- assert snooze.window is None
- assert snooze.state["times_seen"] == 1
- assert response.data["status"] == "ignored"
- assert response.data["statusDetails"]["ignoreCount"] == snooze.count
- assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
- assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
- assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
- assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- def test_snooze_user_count(self):
- event = {}
- for i in range(10):
- event = self.store_event(
- data={
- "fingerprint": ["put-me-in-group-1"],
- "user": {"id": str(i)},
- "timestamp": iso_format(self.min_ago + timedelta(seconds=i)),
- },
- project_id=self.project.id,
- )
- group = Group.objects.get(id=event.group.id)
- group.status = GroupStatus.RESOLVED
- group.save()
- self.login_as(user=self.user)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(
- url, data={"status": "ignored", "ignoreUserCount": 10}, format="json"
- )
- assert response.status_code == 200
- snooze = GroupSnooze.objects.get(group=group)
- assert snooze.count is None
- assert snooze.until is None
- assert snooze.user_count == 10
- assert snooze.user_window is None
- assert snooze.window is None
- assert snooze.state["users_seen"] == 10
- assert response.data["status"] == "ignored"
- assert response.data["statusDetails"]["ignoreCount"] == snooze.count
- assert response.data["statusDetails"]["ignoreWindow"] == snooze.window
- assert response.data["statusDetails"]["ignoreUserCount"] == snooze.user_count
- assert response.data["statusDetails"]["ignoreUserWindow"] == snooze.user_window
- assert response.data["statusDetails"]["ignoreUntil"] == snooze.until
- assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
- def test_set_bookmarked(self):
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- group2 = self.create_group(status=GroupStatus.UNRESOLVED)
- group3 = self.create_group(status=GroupStatus.IGNORED)
- group4 = self.create_group(
- project=self.create_project(slug="foo"),
- status=GroupStatus.UNRESOLVED,
- )
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
- response = self.client.put(url, data={"isBookmarked": "true"}, format="json")
- assert response.status_code == 200
- assert response.data == {"isBookmarked": True}
- bookmark1 = GroupBookmark.objects.filter(group=group1, user_id=self.user.id)
- assert bookmark1.exists()
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group1, is_active=True
- ).exists()
- bookmark2 = GroupBookmark.objects.filter(group=group2, user_id=self.user.id)
- assert bookmark2.exists()
- assert GroupSubscription.objects.filter(
- user_id=self.user.id, group=group2, is_active=True
- ).exists()
- bookmark3 = GroupBookmark.objects.filter(group=group3, user_id=self.user.id)
- assert not bookmark3.exists()
- bookmark4 = GroupBookmark.objects.filter(group=group4, user_id=self.user.id)
- assert not bookmark4.exists()
- def test_subscription(self):
- group1 = self.create_group()
- group2 = self.create_group()
- group3 = self.create_group()
- group4 = self.create_group(project=self.create_project(slug="foo"))
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
- response = self.client.put(url, data={"isSubscribed": "true"}, format="json")
- assert response.status_code == 200
- assert response.data == {"isSubscribed": True, "subscriptionDetails": {"reason": "unknown"}}
- assert GroupSubscription.objects.filter(
- group=group1, user_id=self.user.id, is_active=True
- ).exists()
- assert GroupSubscription.objects.filter(
- group=group2, user_id=self.user.id, is_active=True
- ).exists()
- assert not GroupSubscription.objects.filter(group=group3, user_id=self.user.id).exists()
- assert not GroupSubscription.objects.filter(group=group4, user_id=self.user.id).exists()
- def test_set_public(self):
- group1 = self.create_group()
- group2 = self.create_group()
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}"
- response = self.client.put(url, data={"isPublic": "true"}, format="json")
- assert response.status_code == 200
- assert response.data["isPublic"] is True
- assert "shareId" in response.data
- new_group1 = Group.objects.get(id=group1.id)
- assert bool(new_group1.get_share_id())
- new_group2 = Group.objects.get(id=group2.id)
- assert bool(new_group2.get_share_id())
- def test_set_private(self):
- group1 = self.create_group()
- group2 = self.create_group()
- # Manually mark them as shared
- for g in group1, group2:
- GroupShare.objects.create(project_id=g.project_id, group=g)
- assert bool(g.get_share_id())
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}"
- response = self.client.put(url, data={"isPublic": "false"}, format="json")
- assert response.status_code == 200
- assert response.data == {"isPublic": False, "shareId": None}
- new_group1 = Group.objects.get(id=group1.id)
- assert not bool(new_group1.get_share_id())
- new_group2 = Group.objects.get(id=group2.id)
- assert not bool(new_group2.get_share_id())
- def test_set_has_seen(self):
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- group2 = self.create_group(status=GroupStatus.UNRESOLVED)
- group3 = self.create_group(status=GroupStatus.IGNORED)
- group4 = self.create_group(
- project=self.create_project(slug="foo"),
- status=GroupStatus.UNRESOLVED,
- )
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
- response = self.client.put(url, data={"hasSeen": "true"}, format="json")
- assert response.status_code == 200
- assert response.data == {"hasSeen": True}
- r1 = GroupSeen.objects.filter(group=group1, user_id=self.user.id)
- assert r1.exists()
- r2 = GroupSeen.objects.filter(group=group2, user_id=self.user.id)
- assert r2.exists()
- r3 = GroupSeen.objects.filter(group=group3, user_id=self.user.id)
- assert not r3.exists()
- r4 = GroupSeen.objects.filter(group=group4, user_id=self.user.id)
- assert not r4.exists()
- def test_inbox_fields(self):
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- add_group_to_inbox(group1, GroupInboxReason.NEW)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}"
- response = self.client.put(url, data={"status": "resolved"}, format="json")
- assert "inbox" in response.data
- assert response.data["inbox"] is None
- @patch("sentry.api.helpers.group_index.update.uuid4")
- @patch("sentry.api.helpers.group_index.update.merge_groups")
- @patch("sentry.api.helpers.group_index.update.eventstream")
- def test_merge(self, mock_eventstream, merge_groups, mock_uuid4):
- eventstream_state = object()
- mock_eventstream.start_merge = Mock(return_value=eventstream_state)
- mock_uuid4.return_value = self.get_mock_uuid()
- group1 = self.create_group(times_seen=1)
- group2 = self.create_group(times_seen=50)
- group3 = self.create_group(times_seen=2)
- self.create_group()
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
- response = self.client.put(url, data={"merge": "1"}, format="json")
- assert response.status_code == 200
- assert response.data["merge"]["parent"] == str(group2.id)
- assert sorted(response.data["merge"]["children"]) == sorted(
- [str(group1.id), str(group3.id)]
- )
- mock_eventstream.start_merge.assert_called_once_with(
- group1.project_id, [group3.id, group1.id], group2.id
- )
- assert len(merge_groups.mock_calls) == 1
- merge_groups.delay.assert_any_call(
- from_object_ids=[group3.id, group1.id],
- to_object_id=group2.id,
- transaction_id="abc123",
- eventstream_state=eventstream_state,
- )
- @patch("sentry.api.helpers.group_index.update.uuid4")
- @patch("sentry.api.helpers.group_index.update.merge_groups")
- @patch("sentry.api.helpers.group_index.update.eventstream")
- def test_merge_performance_issues(self, mock_eventstream, merge_groups, mock_uuid4):
- eventstream_state = object()
- mock_eventstream.start_merge = Mock(return_value=eventstream_state)
- mock_uuid4.return_value = self.get_mock_uuid()
- group1 = self.create_group(times_seen=1, type=PerformanceSlowDBQueryGroupType.type_id)
- group2 = self.create_group(times_seen=50, type=PerformanceSlowDBQueryGroupType.type_id)
- group3 = self.create_group(times_seen=2, type=PerformanceSlowDBQueryGroupType.type_id)
- self.create_group()
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&id={group3.id}"
- response = self.client.put(url, data={"merge": "1"}, format="json")
- assert response.status_code == 400, response.content
- def test_assign(self):
- group1 = self.create_group(is_public=True)
- group2 = self.create_group(is_public=True)
- user = self.user
- self.login_as(user=user)
- url = f"{self.path}?id={group1.id}"
- response = self.client.put(url, data={"assignedTo": user.username})
- assert response.status_code == 200
- assert response.data["assignedTo"]["id"] == str(user.id)
- assert response.data["assignedTo"]["type"] == "user"
- assert GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
- assert not GroupAssignee.objects.filter(group=group2, user_id=user.id).exists()
- assert (
- Activity.objects.filter(
- group=group1, user=user, type=ActivityType.ASSIGNED.value
- ).count()
- == 1
- )
- assert GroupSubscription.objects.filter(
- user_id=user.id, group=group1, is_active=True
- ).exists()
- response = self.client.put(url, data={"assignedTo": ""}, format="json")
- assert response.status_code == 200, response.content
- assert response.data["assignedTo"] is None
- assert not GroupAssignee.objects.filter(group=group1, user_id=user.id).exists()
- def test_assign_non_member(self):
- group = self.create_group(is_public=True)
- member = self.user
- non_member = self.create_user("bar@example.com")
- self.login_as(user=member)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"assignedTo": non_member.username}, format="json")
- assert response.status_code == 400, response.content
- def test_assign_team(self):
- self.login_as(user=self.user)
- group = self.create_group()
- other_member = self.create_user("bar@example.com")
- team = self.create_team(
- organization=group.project.organization, members=[self.user, other_member]
- )
- group.project.add_team(team)
- url = f"{self.path}?id={group.id}"
- response = self.client.put(url, data={"assignedTo": f"team:{team.id}"})
- assert response.status_code == 200
- assert response.data["assignedTo"]["id"] == str(team.id)
- assert response.data["assignedTo"]["type"] == "team"
- assert GroupAssignee.objects.filter(group=group, team=team).exists()
- assert Activity.objects.filter(group=group, type=ActivityType.ASSIGNED.value).count() == 1
- assert GroupSubscription.objects.filter(group=group, is_active=True).count() == 2
- response = self.client.put(url, data={"assignedTo": ""}, format="json")
- assert response.status_code == 200, response.content
- assert response.data["assignedTo"] is None
- def test_discard(self):
- group1 = self.create_group(is_public=True)
- group2 = self.create_group(is_public=True)
- group_hash = GroupHash.objects.create(hash="x" * 32, project=group1.project, group=group1)
- user = self.user
- self.login_as(user=user)
- url = f"{self.path}?id={group1.id}"
- with self.tasks():
- with self.feature("projects:discard-groups"):
- response = self.client.put(url, data={"discard": True})
- assert response.status_code == 204
- assert not Group.objects.filter(id=group1.id).exists()
- assert Group.objects.filter(id=group2.id).exists()
- assert GroupHash.objects.filter(id=group_hash.id).exists()
- tombstone = GroupTombstone.objects.get(
- id=GroupHash.objects.get(id=group_hash.id).group_tombstone_id
- )
- assert tombstone.message == group1.message
- assert tombstone.culprit == group1.culprit
- assert tombstone.project == group1.project
- assert tombstone.data == group1.data
- @patch(
- "sentry.models.OrganizationMember.get_scopes",
- return_value=frozenset(s for s in settings.SENTRY_SCOPES if s != "event:admin"),
- )
- def test_discard_requires_events_admin(self, mock_get_scopes):
- group1 = self.create_group(is_public=True)
- user = self.user
- self.login_as(user=user)
- url = f"{self.path}?id={group1.id}"
- with self.tasks(), self.feature("projects:discard-groups"):
- response = self.client.put(url, data={"discard": True})
- assert response.status_code == 400
- assert Group.objects.filter(id=group1.id).exists()
- @region_silo_test
- class GroupDeleteTest(APITestCase, SnubaTestCase):
- @cached_property
- def path(self):
- return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
- @patch("sentry.api.helpers.group_index.delete.eventstream")
- @patch("sentry.eventstream")
- def test_delete_by_id(self, mock_eventstream_task, mock_eventstream_api):
- eventstream_state = {"event_stream_state": uuid4()}
- mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
- group1 = self.create_group(status=GroupStatus.RESOLVED)
- group2 = self.create_group(status=GroupStatus.UNRESOLVED)
- group3 = self.create_group(status=GroupStatus.IGNORED)
- group4 = self.create_group(
- project=self.create_project(slug="foo"),
- status=GroupStatus.UNRESOLVED,
- )
- hashes = []
- for g in group1, group2, group3, group4:
- hash = uuid4().hex
- hashes.append(hash)
- GroupHash.objects.create(project=g.project, hash=hash, group=g)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}&group4={group4.id}"
- response = self.client.delete(url, format="json")
- mock_eventstream_api.start_delete_groups.assert_called_once_with(
- group1.project_id, [group1.id, group2.id]
- )
- assert response.status_code == 204
- assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
- assert not GroupHash.objects.filter(group_id=group1.id).exists()
- assert Group.objects.get(id=group2.id).status == GroupStatus.PENDING_DELETION
- assert not GroupHash.objects.filter(group_id=group2.id).exists()
- assert Group.objects.get(id=group3.id).status != GroupStatus.PENDING_DELETION
- assert GroupHash.objects.filter(group_id=group3.id).exists()
- assert Group.objects.get(id=group4.id).status != GroupStatus.PENDING_DELETION
- assert GroupHash.objects.filter(group_id=group4.id).exists()
- Group.objects.filter(id__in=(group1.id, group2.id)).update(status=GroupStatus.UNRESOLVED)
- with self.tasks():
- response = self.client.delete(url, format="json")
- mock_eventstream_task.end_delete_groups.assert_called_once_with(eventstream_state)
- assert response.status_code == 204
- assert not Group.objects.filter(id=group1.id).exists()
- assert not GroupHash.objects.filter(group_id=group1.id).exists()
- assert not Group.objects.filter(id=group2.id).exists()
- assert not GroupHash.objects.filter(group_id=group2.id).exists()
- assert Group.objects.filter(id=group3.id).exists()
- assert GroupHash.objects.filter(group_id=group3.id).exists()
- assert Group.objects.filter(id=group4.id).exists()
- assert GroupHash.objects.filter(group_id=group4.id).exists()
- @patch("sentry.api.helpers.group_index.delete.eventstream")
- @patch("sentry.eventstream")
- def test_delete_performance_issue_by_id(self, mock_eventstream_task, mock_eventstream_api):
- eventstream_state = {"event_stream_state": uuid4()}
- mock_eventstream_api.start_delete_groups = Mock(return_value=eventstream_state)
- group1 = self.create_group(
- status=GroupStatus.RESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
- )
- group2 = self.create_group(
- status=GroupStatus.UNRESOLVED, type=PerformanceSlowDBQueryGroupType.type_id
- )
- hashes = []
- for g in group1, group2:
- hash = uuid4().hex
- hashes.append(hash)
- GroupHash.objects.create(project=g.project, hash=hash, group=g)
- self.login_as(user=self.user)
- url = f"{self.path}?id={group1.id}&id={group2.id}"
- response = self.client.delete(url, format="json")
- assert response.status_code == 400
- assert Group.objects.filter(id=group1.id).exists()
- assert GroupHash.objects.filter(group_id=group1.id).exists()
- assert Group.objects.filter(id=group2.id).exists()
- assert GroupHash.objects.filter(group_id=group2.id).exists()
- def test_bulk_delete(self):
- groups = []
- for i in range(10, 41):
- groups.append(
- self.create_group(
- project=self.project,
- status=GroupStatus.RESOLVED,
- )
- )
- hashes = []
- for group in groups:
- hash = uuid4().hex
- hashes.append(hash)
- GroupHash.objects.create(project=group.project, hash=hash, group=group)
- self.login_as(user=self.user)
- # if query is '' it defaults to is:unresolved
- url = self.path + "?query="
- response = self.client.delete(url, format="json")
- assert response.status_code == 204
- for group in groups:
- assert Group.objects.get(id=group.id).status == GroupStatus.PENDING_DELETION
- assert not GroupHash.objects.filter(group_id=group.id).exists()
- Group.objects.filter(id__in=[group.id for group in groups]).update(
- status=GroupStatus.UNRESOLVED
- )
- with self.tasks():
- response = self.client.delete(url, format="json")
- assert response.status_code == 204
- for group in groups:
- assert not Group.objects.filter(id=group.id).exists()
- assert not GroupHash.objects.filter(group_id=group.id).exists()
- def test_bulk_delete_performance_issues(self):
- groups = []
- for i in range(10, 41):
- groups.append(
- self.create_group(
- project=self.project,
- status=GroupStatus.RESOLVED,
- type=PerformanceSlowDBQueryGroupType.type_id,
- )
- )
- hashes = []
- for group in groups:
- hash = uuid4().hex
- hashes.append(hash)
- GroupHash.objects.create(project=group.project, hash=hash, group=group)
- self.login_as(user=self.user)
- # if query is '' it defaults to is:unresolved
- url = self.path + "?query="
- response = self.client.delete(url, format="json")
- assert response.status_code == 400
- for group in groups:
- assert Group.objects.filter(id=group.id).exists()
- assert GroupHash.objects.filter(group_id=group.id).exists()
|