123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608 |
- import time
- from datetime import timedelta
- from functools import cached_property
- from unittest.mock import Mock, call, 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.activity import Activity
- from sentry.models.apitoken import ApiToken
- from sentry.models.group import Group, GroupStatus
- from sentry.models.groupassignee import GroupAssignee
- from sentry.models.groupbookmark import GroupBookmark
- from sentry.models.grouphash import GroupHash
- from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox
- from sentry.models.grouplink import GroupLink
- from sentry.models.groupresolution import GroupResolution
- from sentry.models.groupseen import GroupSeen
- from sentry.models.groupshare import GroupShare
- from sentry.models.groupsnooze import GroupSnooze
- from sentry.models.groupsubscription import GroupSubscription
- from sentry.models.grouptombstone import GroupTombstone
- from sentry.models.integrations.external_issue import ExternalIssue
- from sentry.models.integrations.organization_integration import OrganizationIntegration
- from sentry.models.options.user_option import UserOption
- from sentry.models.release import Release
- from sentry.silo.base import SiloMode
- from sentry.testutils.cases import APITestCase, SnubaTestCase
- from sentry.testutils.helpers import parse_link_header, with_feature
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.testutils.silo import assume_test_silo_mode
- from sentry.types.activity import ActivityType
- from sentry.utils import json
- 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 + "?query=is:unresolved", 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 + "?query=is:unresolved", 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):
- with assume_test_silo_mode(SiloMode.CONTROL):
- 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, substatus=None)
- 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]
- 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&query=is: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&query=is: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
- with assume_test_silo_mode(SiloMode.CONTROL):
- integration = self.create_provider_integration(provider="example", name="Example")
- integration.add_organization(org, self.user)
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- with assume_test_silo_mode(SiloMode.CONTROL):
- 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&query=is: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
- with assume_test_silo_mode(SiloMode.CONTROL):
- integration = self.create_provider_integration(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
- with assume_test_silo_mode(SiloMode.CONTROL):
- uo1 = UserOption.objects.create(
- key="self_assign_issue", value="1", project_id=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()
- with assume_test_silo_mode(SiloMode.CONTROL):
- 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)
- with assume_test_silo_mode(SiloMode.CONTROL):
- uo1 = UserOption.objects.create(
- key="self_assign_issue", value="1", project_id=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"] == ""
- with assume_test_silo_mode(SiloMode.CONTROL):
- 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"] == ""
- @with_feature("organizations:resolve-in-upcoming-release")
- def test_set_resolved_in_upcoming_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": {"inUpcomingRelease": True}},
- format="json",
- )
- assert response.status_code == 200
- assert response.data["status"] == "resolved"
- assert response.data["statusDetails"]["inUpcomingRelease"]
- 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_upcoming_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_upcoming_release_flag_validation(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": {"inUpcomingRelease": True}},
- format="json",
- )
- assert response.status_code == 400
- assert (
- response.data["statusDetails"]["inUpcomingRelease"][0]
- == "Your organization does not have access to this feature."
- )
- @with_feature("organizations:resolve-in-upcoming-release")
- def test_upcoming_release_release_validation(self):
- 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": {"inUpcomingRelease": True}},
- format="json",
- )
- assert response.status_code == 400
- assert (
- response.data["statusDetails"]["inUpcomingRelease"][0]
- == "No release data present in the system."
- )
- 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 is not 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 is not 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):
- 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.substatus = None
- 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 is not 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.issues.merge.uuid4")
- @patch("sentry.issues.merge.merge_groups")
- @patch("sentry.eventstream.backend")
- 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.issues.merge.uuid4")
- @patch("sentry.issues.merge.merge_groups")
- @patch("sentry.eventstream.backend")
- 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_id=user.id, 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()
- class GroupDeleteTest(APITestCase, SnubaTestCase):
- @cached_property
- def path(self):
- return f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/issues/"
- @patch("sentry.eventstream.backend")
- def test_delete_by_id(self, mock_eventstream):
- eventstream_state = {"event_stream_state": uuid4()}
- mock_eventstream.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.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")
- # XXX(markus): Something is sending duplicated replacements to snuba --
- # once from within tasks.deletions.groups and another time from
- # sentry.deletions.defaults.groups
- assert mock_eventstream.end_delete_groups.call_args_list == [
- call(eventstream_state),
- call(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.eventstream.backend")
- def test_delete_performance_issue_by_id(self, mock_eventstream):
- eventstream_state = {"event_stream_state": uuid4()}
- mock_eventstream.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()
|