mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
1529 lines
60 KiB
Python
1529 lines
60 KiB
Python
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 import SiloMode
|
|
from sentry.testutils.cases 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 assume_test_silo_mode, 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):
|
|
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]
|
|
|
|
|
|
@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
|
|
|
|
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",
|
|
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"] == ""
|
|
|
|
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["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["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("[email protected]")
|
|
|
|
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("[email protected]")
|
|
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.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()
|