diff --git a/tests/sentry/deletions/test_group.py b/tests/sentry/deletions/test_group.py index de230707d9474a..69c317765955b2 100644 --- a/tests/sentry/deletions/test_group.py +++ b/tests/sentry/deletions/test_group.py @@ -2,13 +2,13 @@ import random from datetime import datetime, timedelta from time import time +from typing import Any from unittest import mock from uuid import uuid4 from snuba_sdk import Column, Condition, Entity, Function, Op, Query, Request from sentry import deletions, nodestore -from sentry.deletions.defaults.group import ErrorEventsDeletionTask from sentry.deletions.tasks.groups import delete_groups_for_project from sentry.issues.grouptype import FeedbackGroup, GroupCategory from sentry.issues.issue_occurrence import IssueOccurrence @@ -31,93 +31,102 @@ class DeleteGroupTest(TestCase, SnubaTestCase): - def setUp(self) -> None: - super().setUp() - one_minute = before_now(minutes=1).isoformat() - group1_data = {"timestamp": one_minute, "fingerprint": ["group1"]} - group2_data = {"timestamp": one_minute, "fingerprint": ["group2"]} - - # Group 1 events - self.event = self.store_event( - data=group1_data | {"tags": {"foo": "bar"}}, project_id=self.project.id - ) - self.event_id = self.event.event_id - self.node_id = Event.generate_node_id(self.project.id, self.event_id) - group = self.event.group - self.event2 = self.store_event(data=group1_data, project_id=self.project.id) - self.node_id2 = Event.generate_node_id(self.project.id, self.event2.event_id) - # Group 2 event - self.keep_event = self.store_event(data=group2_data, project_id=self.project.id) - self.keep_node_id = Event.generate_node_id(self.project.id, self.keep_event.event_id) + def _generate_data(self, fingerprint: str | None = None) -> dict[str, Any]: + return { + "fingerprint": [fingerprint or uuid4().hex], + "timestamp": before_now(minutes=1).isoformat(), + } + + def _get_node_id(self, event: Event) -> str: + return Event.generate_node_id(event.project_id, event.event_id) + def _create_event_with_many_group_children(self) -> Event: + event = self.store_event( + data=self._generate_data(fingerprint="group1"), + project_id=self.project.id, + ) UserReport.objects.create( - group_id=group.id, project_id=self.event.project_id, name="With group id" + group_id=event.group.id, project_id=event.project_id, name="With group id" ) UserReport.objects.create( - event_id=self.event.event_id, project_id=self.event.project_id, name="With event id" + event_id=event.event_id, project_id=event.project_id, name="With event id" ) EventAttachment.objects.create( - event_id=self.event.event_id, - project_id=self.event.project_id, + event_id=event.event_id, + project_id=event.project_id, name="hello.png", content_type="image/png", ) - GroupAssignee.objects.create(group=group, project=self.project, user_id=self.user.id) - GroupHash.objects.create(project=self.project, group=group, hash=uuid4().hex) - GroupMeta.objects.create(group=group, key="foo", value="bar") - GroupRedirect.objects.create(group_id=group.id, previous_group_id=1) + GroupAssignee.objects.create(group=event.group, project=self.project, user_id=self.user.id) + GroupHash.objects.create(project=self.project, group=event.group, hash=uuid4().hex) + GroupMeta.objects.create(group=event.group, key="foo", value="bar") + GroupRedirect.objects.create(group_id=event.group.id, previous_group_id=1) - def test_simple(self) -> None: - ErrorEventsDeletionTask.DEFAULT_CHUNK_SIZE = 1 # test chunking logic - group = self.event.group - assert nodestore.backend.get(self.node_id) - assert nodestore.backend.get(self.node_id2) - assert nodestore.backend.get(self.keep_node_id) + return event + + def test_delete_group_with_many_related_children(self) -> None: + event = self._create_event_with_many_group_children() + assert event.group is not None + + event2 = self.store_event( + data=self._generate_data(fingerprint="group1"), project_id=self.project.id + ) + assert event2.group is not None + + assert nodestore.backend.get(self._get_node_id(event)) + assert nodestore.backend.get(self._get_node_id(event2)) with self.tasks(): delete_groups_for_project( - object_ids=[group.id], transaction_id=uuid4().hex, project_id=self.project.id + object_ids=[event.group.id], transaction_id=uuid4().hex, project_id=self.project.id ) - assert not UserReport.objects.filter(group_id=group.id).exists() - assert not UserReport.objects.filter(event_id=self.event.event_id).exists() - assert not EventAttachment.objects.filter(event_id=self.event.event_id).exists() + assert not UserReport.objects.filter(group_id=event.group.id).exists() + assert not UserReport.objects.filter(event_id=event.event_id).exists() + assert not EventAttachment.objects.filter(event_id=event.event_id).exists() - assert not GroupRedirect.objects.filter(group_id=group.id).exists() - assert not GroupHash.objects.filter(group_id=group.id).exists() - assert not Group.objects.filter(id=group.id).exists() - assert not nodestore.backend.get(self.node_id) - assert not nodestore.backend.get(self.node_id2) - assert nodestore.backend.get(self.keep_node_id), "Does not remove from second group" - assert Group.objects.filter(id=self.keep_event.group_id).exists() + assert not GroupRedirect.objects.filter(group_id=event.group.id).exists() + assert not GroupHash.objects.filter(group_id=event.group.id).exists() + assert not Group.objects.filter(id=event.group.id).exists() + assert not nodestore.backend.get(self._get_node_id(event)) + assert not nodestore.backend.get(self._get_node_id(event2)) - def test_simple_multiple_groups(self) -> None: + def test_multiple_groups(self) -> None: + event = self.store_event( + data=self._generate_data(fingerprint="group1"), + project_id=self.project.id, + ) + assert event.group is not None + keep_event = self.store_event( + data=self._generate_data(fingerprint="group2"), + project_id=self.project.id, + ) + assert keep_event.group is not None other_event = self.store_event( - data={"timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group3"]}, + data=self._generate_data(fingerprint="group3"), project_id=self.project.id, ) - other_node_id = Event.generate_node_id(self.project.id, other_event.event_id) + assert other_event.group is not None - group = self.event.group with self.tasks(): delete_groups_for_project( - object_ids=[group.id, other_event.group_id], + object_ids=[event.group.id, other_event.group.id], transaction_id=uuid4().hex, project_id=self.project.id, ) - assert not Group.objects.filter(id=group.id).exists() + assert not Group.objects.filter(id=event.group.id).exists() assert not Group.objects.filter(id=other_event.group_id).exists() - assert not nodestore.backend.get(self.node_id) - assert not nodestore.backend.get(other_node_id) + assert not nodestore.backend.get(self._get_node_id(event)) + assert not nodestore.backend.get(self._get_node_id(other_event)) - assert Group.objects.filter(id=self.keep_event.group_id).exists() - assert nodestore.backend.get(self.keep_node_id) + assert Group.objects.filter(id=keep_event.group.id).exists() + assert nodestore.backend.get(self._get_node_id(keep_event)) def test_grouphistory_relation(self) -> None: other_event = self.store_event( - data={"timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group3"]}, + data=self._generate_data(fingerprint="other_group"), project_id=self.project.id, ) other_group = other_event.group @@ -172,39 +181,45 @@ def test_delete_groups_delete_grouping_records_by_hash( self, mock_delete_seer_grouping_records_by_hash_apply_async: mock.Mock ) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) + event = self.store_event( + data=self._generate_data(fingerprint="group1"), + project_id=self.project.id, + ) + assert event.group + keep_event = self.store_event( + data=self._generate_data(fingerprint="group2"), + project_id=self.project.id, + ) + assert keep_event.group other_event = self.store_event( - data={ - "timestamp": before_now(minutes=1).isoformat(), - "fingerprint": ["group3"], - }, + data=self._generate_data(fingerprint="group3"), project_id=self.project.id, ) - other_node_id = Event.generate_node_id(self.project.id, other_event.event_id) hashes = [ grouphash.hash for grouphash in GroupHash.objects.filter( - project_id=self.project.id, group_id__in=[self.event.group.id, other_event.group_id] + project_id=self.project.id, group_id__in=[event.group.id, other_event.group_id] ) ] - group = self.event.group + with self.tasks(): delete_groups_for_project( - object_ids=[group.id, other_event.group_id], + object_ids=[event.group.id, other_event.group_id], transaction_id=uuid4().hex, project_id=self.project.id, ) - assert not Group.objects.filter(id=group.id).exists() + assert not Group.objects.filter(id=event.group.id).exists() assert not Group.objects.filter(id=other_event.group_id).exists() - assert not nodestore.backend.get(self.node_id) - assert not nodestore.backend.get(other_node_id) + assert not nodestore.backend.get(self._get_node_id(event)) + assert not nodestore.backend.get(self._get_node_id(other_event)) - assert Group.objects.filter(id=self.keep_event.group_id).exists() - assert nodestore.backend.get(self.keep_node_id) + assert Group.objects.filter(id=keep_event.group_id).exists() + assert nodestore.backend.get(self._get_node_id(keep_event)) assert mock_delete_seer_grouping_records_by_hash_apply_async.call_args[1] == { - "args": [group.project.id, hashes, 0] + "args": [event.project.id, hashes, 0] } @mock.patch( @@ -291,13 +306,14 @@ def test_delete_grouphashes_and_metadata(self) -> None: project_id=self.project.id, ) grouphash_b = GroupHash.objects.get(hash=event_b.get_primary_hash()) - # assert grouphash_b.metadata is not None assert grouphash_b.metadata is not None metadata_b_id = grouphash_b.metadata.id # Verify that seer matched event_b to event_a's hash assert event_a.group_id == event_b.group_id + # Make sure it has not changed + grouphash_a.refresh_from_db() assert grouphash_a.metadata is not None assert grouphash_a.metadata.seer_matched_grouphash is None assert grouphash_b.metadata is not None