From 9092e62017a70007f5c1d74d6eb2d978411ab83a Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Wed, 19 Nov 2025 16:53:01 -0800 Subject: [PATCH 1/2] WIP: fix(aci): Try our best in Project.tranfer_to --- src/sentry/models/project.py | 84 +++++++- tests/sentry/models/test_project.py | 292 +++++++++++++++++++++++++++- 2 files changed, 373 insertions(+), 3 deletions(-) diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index 093e3badc3d790..b76f28e65749ab 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -9,7 +9,7 @@ import sentry_sdk from django.conf import settings from django.db import IntegrityError, models, router, transaction -from django.db.models import Q, QuerySet, Subquery +from django.db.models import Count, Q, QuerySet, Subquery from django.db.models.signals import pre_delete from django.utils import timezone from django.utils.http import urlencode @@ -55,6 +55,7 @@ if TYPE_CHECKING: from sentry.models.options.project_option import ProjectOptionManager from sentry.models.options.project_template_option import ProjectTemplateOptionManager + from sentry.models.organization import Organization from sentry.users.models.user import User # NOTE: @@ -499,7 +500,7 @@ def get_audit_log_data(self): def get_full_name(self): return self.slug - def transfer_to(self, organization): + def transfer_to(self, organization: Organization) -> None: from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion from sentry.incidents.models.alert_rule import AlertRule from sentry.integrations.models.external_issue import ExternalIssue @@ -514,6 +515,7 @@ def transfer_to(self, organization): from sentry.models.rule import Rule from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorStatus from sentry.snuba.models import SnubaQuery + from sentry.workflow_engine.models import DataConditionGroup, DataSource, Detector, Workflow old_org_id = self.organization_id org_changed = old_org_id != organization.id @@ -637,6 +639,84 @@ def transfer_to(self, organization): AlertRule.objects.fetch_for_project(self).update(organization=organization) + # Transfer DataSource, Workflow, and DataConditionGroup objects for Detectors attached to this project. + # * DataSources link detectors to their data sources (QuerySubscriptions, Monitors, etc.). + # * Workflows are connected to detectors and define what actions to take. + # * DataConditionGroups are connected to workflows (unique 1:1 via WorkflowDataConditionGroup). + # Since Detectors are project-scoped and their DataSources are project-specific, + # we need to update all related organization-scoped workflow_engine models. + # + # IMPORTANT: Workflows and DataConditionGroups can be shared across multiple projects + # in the same organization. We only transfer them if they're exclusively used by + # detectors in this project. Shared workflows remain in the original organization. + # There are certainly more correct ways to do this, but this should cover most cases. + + detector_ids = Detector.objects.filter(project_id=self.id).values_list("id", flat=True) + if detector_ids: + # Update DataSources + # DataSources are 1:1 with their source (e.g., QuerySubscription) so they always transfer + data_source_ids = ( + DataSource.objects.filter(detectors__id__in=detector_ids) + .distinct() + .values_list("id", flat=True) + ) + DataSource.objects.filter(id__in=data_source_ids).update( + organization_id=organization.id + ) + + # Update Workflows connected to these detectors + # Only transfer workflows that are exclusively used by detectors in this project + all_workflow_ids = ( + Workflow.objects.filter(detectorworkflow__detector_id__in=detector_ids) + .distinct() + .values_list("id", flat=True) + ) + + # Find workflows that are ONLY connected to detectors in this project + exclusive_workflow_ids = ( + Workflow.objects.filter(id__in=all_workflow_ids) + .annotate( + detector_count=Count("detectorworkflow__detector"), + project_detector_count=Count( + "detectorworkflow__detector", + filter=Q(detectorworkflow__detector_id__in=detector_ids), + ), + ) + .filter(detector_count=models.F("project_detector_count")) + .values_list("id", flat=True) + ) + + Workflow.objects.filter(id__in=exclusive_workflow_ids).update( + organization_id=organization.id + ) + + # Update DataConditionGroups connected to the transferred workflows + # These are linked via WorkflowDataConditionGroup with a unique constraint on condition_group + workflow_condition_group_ids = ( + DataConditionGroup.objects.filter( + workflowdataconditiongroup__workflow_id__in=exclusive_workflow_ids + ) + .distinct() + .values_list("id", flat=True) + ) + DataConditionGroup.objects.filter(id__in=workflow_condition_group_ids).update( + organization_id=organization.id + ) + + # Update DataConditionGroups that are directly owned by detectors + # These are linked via Detector.workflow_condition_group (unique FK) + # and are exclusively owned by the detector, so they always transfer + detector_condition_group_ids = ( + Detector.objects.filter( + id__in=detector_ids, workflow_condition_group_id__isnull=False + ) + .values_list("workflow_condition_group_id", flat=True) + .distinct() + ) + DataConditionGroup.objects.filter(id__in=detector_condition_group_ids).update( + organization_id=organization.id + ) + # Manually move over external issues to the new org linked_groups = GroupLink.objects.filter(project_id=self.id).values_list( "linked_id", flat=True diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index 771401f98ba468..77a8fa6832f9ea 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -36,7 +36,7 @@ from sentry.types.actor import Actor from sentry.users.models.user import User from sentry.users.models.user_option import UserOption -from sentry.workflow_engine.models import Detector +from sentry.workflow_engine.models import Detector, DetectorWorkflow from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType @@ -482,6 +482,296 @@ def test_project_detectors(self) -> None: assert Detector.objects.filter(project=project, type=ErrorGroupType.slug).count() == 1 assert Detector.objects.filter(project=project, type=IssueStreamGroupType.slug).count() == 1 + def test_transfer_to_organization_with_metric_issue_detector_and_workflow(self) -> None: + """ + Test that transferring a project properly handles MetricIssue Detectors and their Workflows. + + This test verifies the complex chain of related objects is correctly transferred: + - A MetricIssue Detector (attached to the project) + - A QuerySubscription (data source for the detector) + - A DataSource (links the detector to the query subscription) + - A Workflow (connected to the detector via DetectorWorkflow) + + When the project is transferred to a new organization, the following must happen: + 1. The project's organization_id is updated + 2. The detector remains attached to the project + 3. The DataSource's organization_id is updated to the new organization + 4. The QuerySubscription's project_id remains valid + 5. The Workflow's organization_id is updated to the new organization + 6. The DetectorWorkflow relationship is preserved + """ + from sentry.incidents.grouptype import MetricIssue + from sentry.incidents.models.alert_rule import AlertRuleDetectionType + from sentry.incidents.utils.constants import INCIDENTS_SNUBA_SUBSCRIPTION_TYPE + from sentry.incidents.utils.types import DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION + from sentry.snuba.models import QuerySubscription, SnubaQuery + from sentry.workflow_engine.models import DataSource, Detector, DetectorWorkflow, Workflow + + from_org = self.create_organization() + team = self.create_team(organization=from_org) + to_org = self.create_organization() + + project = self.create_project(teams=[team]) + + # Create a SnubaQuery for the detector + snuba_query = SnubaQuery.objects.create( + type=SnubaQuery.Type.ERROR.value, + dataset="events", + aggregate="count()", + time_window=60, + resolution=60, + ) + + # Create a QuerySubscription + query_subscription = QuerySubscription.objects.create( + project=project, + snuba_query=snuba_query, + type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + ) + + # Create a MetricIssue Detector with the QuerySubscription as data source + detector = Detector.objects.create( + project=project, + name="Test Metric Issue Detector", + type=MetricIssue.slug, + config={"detection_type": AlertRuleDetectionType.STATIC}, + ) + + # Create a DataSource linking the detector to the query subscription + data_source = DataSource.objects.create( + organization=from_org, + source_id=str(query_subscription.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + data_source.detectors.add(detector) + + # Create a Workflow connected to the detector + workflow = Workflow.objects.create( + name="Test Workflow", + organization=from_org, + config={}, + ) + DetectorWorkflow.objects.create( + detector=detector, + workflow=workflow, + ) + + # Verify initial state + assert detector.project_id == project.id + assert data_source.organization_id == from_org.id + assert workflow.organization_id == from_org.id + assert query_subscription.project_id == project.id + + # Transfer the project + project.transfer_to(organization=to_org) + + # Refresh objects + project.refresh_from_db() + detector.refresh_from_db() + data_source.refresh_from_db() + workflow.refresh_from_db() + query_subscription.refresh_from_db() + + # Expected behavior after transfer: + # 1. Project should be in the new organization + assert project.organization_id == to_org.id + + # 2. Detector should still be attached to the project + assert detector.project_id == project.id + + # 3. DataSource organization should be updated to the new organization + assert data_source.organization_id == to_org.id + + # 4. QuerySubscription project should be updated to reflect the project's new org + assert query_subscription.project_id == project.id + + # 5. Workflow should be updated to the new organization + assert workflow.organization_id == to_org.id + + # 6. DetectorWorkflow relationship should still exist + assert DetectorWorkflow.objects.filter( + detector=detector, + workflow=workflow, + ).exists() + + def test_transfer_to_organization_with_workflow_data_condition_groups(self) -> None: + from_org = self.create_organization() + team = self.create_team(organization=from_org) + to_org = self.create_organization() + + project = self.create_project(teams=[team]) + + # Create a detector for this project + detector = self.create_detector(project=project) + + # Create a workflow and connect it to the detector + workflow = self.create_workflow(organization=from_org) + self.create_detector_workflow(detector=detector, workflow=workflow) + + # Create a DataConditionGroup and connect it to the workflow + condition_group = self.create_data_condition_group(organization=from_org) + self.create_workflow_data_condition_group( + workflow=workflow, condition_group=condition_group + ) + + # Verify initial state + assert detector.project_id == project.id + assert workflow.organization_id == from_org.id + assert condition_group.organization_id == from_org.id + + # Transfer the project + project.transfer_to(organization=to_org) + + # Refresh objects + project.refresh_from_db() + detector.refresh_from_db() + workflow.refresh_from_db() + condition_group.refresh_from_db() + + # Expected behavior after transfer: + # 1. Project should be in the new organization + assert project.organization_id == to_org.id + + # 2. Detector should still be attached to the project + assert detector.project_id == project.id + + # 3. Workflow should be updated to the new organization + assert workflow.organization_id == to_org.id + + # 4. DataConditionGroup should be updated to the new organization + assert condition_group.organization_id == to_org.id + + # 5. WorkflowDataConditionGroup relationship should be preserved + wdcg = condition_group.workflowdataconditiongroup_set.first() + assert wdcg is not None + assert wdcg.workflow_id == workflow.id + + def test_transfer_to_organization_does_not_transfer_shared_workflows(self) -> None: + from_org = self.create_organization() + team = self.create_team(organization=from_org) + to_org = self.create_organization() + + # Create two projects in the same organization + project_a = self.create_project(teams=[team], name="Project A") + project_b = self.create_project(teams=[team], organization=from_org, name="Project B") + + # Create detectors for both projects + detector_a = self.create_detector(project=project_a) + detector_b = self.create_detector(project=project_b) + + # Create a SHARED workflow connected to both detectors + shared_workflow = self.create_workflow(organization=from_org, name="Shared Workflow") + self.create_detector_workflow(detector=detector_a, workflow=shared_workflow) + self.create_detector_workflow(detector=detector_b, workflow=shared_workflow) + + # Create an EXCLUSIVE workflow only for project_a + exclusive_workflow = self.create_workflow(organization=from_org, name="Exclusive Workflow") + self.create_detector_workflow(detector=detector_a, workflow=exclusive_workflow) + + # Create DataConditionGroups for both workflows + shared_dcg = self.create_data_condition_group(organization=from_org) + self.create_workflow_data_condition_group( + workflow=shared_workflow, condition_group=shared_dcg + ) + + exclusive_dcg = self.create_data_condition_group(organization=from_org) + self.create_workflow_data_condition_group( + workflow=exclusive_workflow, condition_group=exclusive_dcg + ) + + # Verify initial state + assert shared_workflow.organization_id == from_org.id + assert exclusive_workflow.organization_id == from_org.id + assert shared_dcg.organization_id == from_org.id + assert exclusive_dcg.organization_id == from_org.id + + # Transfer project_a to the new organization + project_a.transfer_to(organization=to_org) + + # Refresh all objects + project_a.refresh_from_db() + project_b.refresh_from_db() + detector_a.refresh_from_db() + detector_b.refresh_from_db() + shared_workflow.refresh_from_db() + exclusive_workflow.refresh_from_db() + shared_dcg.refresh_from_db() + exclusive_dcg.refresh_from_db() + + # Expected behavior after transfer: + + # 1. Both projects' basic attributes are correct + assert project_a.organization_id == to_org.id + assert project_b.organization_id == from_org.id + + # 2. Detectors follow their projects + assert detector_a.project_id == project_a.id + assert detector_b.project_id == project_b.id + + # 3. SHARED workflow should NOT transfer (still in old org) + # because it's used by detector_b which remains in from_org + assert shared_workflow.organization_id == from_org.id + + # 4. EXCLUSIVE workflow SHOULD transfer (moved to new org) + # because it's only used by detector_a + assert exclusive_workflow.organization_id == to_org.id + + # 5. DataConditionGroups follow their workflows + assert shared_dcg.organization_id == from_org.id + assert exclusive_dcg.organization_id == to_org.id + + # 6. Both detectors should still be connected to their workflows + assert DetectorWorkflow.objects.filter( + detector=detector_a, workflow=shared_workflow + ).exists() + assert DetectorWorkflow.objects.filter( + detector=detector_b, workflow=shared_workflow + ).exists() + assert DetectorWorkflow.objects.filter( + detector=detector_a, workflow=exclusive_workflow + ).exists() + + def test_transfer_to_organization_with_detector_workflow_condition_group(self) -> None: + from_org = self.create_organization() + team = self.create_team(organization=from_org) + to_org = self.create_organization() + + project = self.create_project(teams=[team]) + + # Create a detector with a workflow_condition_group + detector = self.create_detector(project=project) + workflow_condition_group = self.create_data_condition_group(organization=from_org) + + # Assign the condition group directly to the detector + detector.workflow_condition_group = workflow_condition_group + detector.save() + + # Verify initial state + assert detector.project_id == project.id + assert detector.workflow_condition_group_id == workflow_condition_group.id + assert workflow_condition_group.organization_id == from_org.id + + # Transfer the project + project.transfer_to(organization=to_org) + + # Refresh objects + project.refresh_from_db() + detector.refresh_from_db() + workflow_condition_group.refresh_from_db() + + # Expected behavior after transfer: + # 1. Project should be in the new organization + assert project.organization_id == to_org.id + + # 2. Detector should still be attached to the project + assert detector.project_id == project.id + + # 3. workflow_condition_group should transfer because it's exclusively owned by the detector + assert workflow_condition_group.organization_id == to_org.id + + # 4. The FK relationship should be preserved + assert detector.workflow_condition_group_id == workflow_condition_group.id + class ProjectOptionsTests(TestCase): """ From d98a955fa7b39dc4ab0ce03a286c9ce0ef7f6e4e Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 12:23:14 -0800 Subject: [PATCH 2/2] more DCGs, clean-up --- src/sentry/models/project.py | 13 ++ tests/sentry/models/test_project.py | 192 +++++----------------------- 2 files changed, 43 insertions(+), 162 deletions(-) diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index b76f28e65749ab..5c2fb7a38a3d90 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -717,6 +717,19 @@ def transfer_to(self, organization: Organization) -> None: organization_id=organization.id ) + # Update DataConditionGroups used as when_condition_group in transferred workflows + # DataConditionGroups are never shared, so transfer all when_condition_groups + when_condition_group_ids = ( + Workflow.objects.filter( + id__in=exclusive_workflow_ids, when_condition_group_id__isnull=False + ) + .values_list("when_condition_group_id", flat=True) + .distinct() + ) + DataConditionGroup.objects.filter(id__in=when_condition_group_ids).update( + organization_id=organization.id + ) + # Manually move over external issues to the new org linked_groups = GroupLink.objects.filter(project_id=self.id).values_list( "linked_id", flat=True diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index 77a8fa6832f9ea..016ab2e8552b47 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -483,165 +483,55 @@ def test_project_detectors(self) -> None: assert Detector.objects.filter(project=project, type=IssueStreamGroupType.slug).count() == 1 def test_transfer_to_organization_with_metric_issue_detector_and_workflow(self) -> None: - """ - Test that transferring a project properly handles MetricIssue Detectors and their Workflows. - - This test verifies the complex chain of related objects is correctly transferred: - - A MetricIssue Detector (attached to the project) - - A QuerySubscription (data source for the detector) - - A DataSource (links the detector to the query subscription) - - A Workflow (connected to the detector via DetectorWorkflow) - - When the project is transferred to a new organization, the following must happen: - 1. The project's organization_id is updated - 2. The detector remains attached to the project - 3. The DataSource's organization_id is updated to the new organization - 4. The QuerySubscription's project_id remains valid - 5. The Workflow's organization_id is updated to the new organization - 6. The DetectorWorkflow relationship is preserved - """ - from sentry.incidents.grouptype import MetricIssue - from sentry.incidents.models.alert_rule import AlertRuleDetectionType - from sentry.incidents.utils.constants import INCIDENTS_SNUBA_SUBSCRIPTION_TYPE - from sentry.incidents.utils.types import DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION - from sentry.snuba.models import QuerySubscription, SnubaQuery - from sentry.workflow_engine.models import DataSource, Detector, DetectorWorkflow, Workflow - from_org = self.create_organization() team = self.create_team(organization=from_org) to_org = self.create_organization() - project = self.create_project(teams=[team]) - # Create a SnubaQuery for the detector - snuba_query = SnubaQuery.objects.create( - type=SnubaQuery.Type.ERROR.value, - dataset="events", - aggregate="count()", - time_window=60, - resolution=60, - ) - - # Create a QuerySubscription - query_subscription = QuerySubscription.objects.create( - project=project, - snuba_query=snuba_query, - type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, - ) - - # Create a MetricIssue Detector with the QuerySubscription as data source - detector = Detector.objects.create( - project=project, - name="Test Metric Issue Detector", - type=MetricIssue.slug, - config={"detection_type": AlertRuleDetectionType.STATIC}, - ) - - # Create a DataSource linking the detector to the query subscription - data_source = DataSource.objects.create( - organization=from_org, - source_id=str(query_subscription.id), - type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, - ) + detector = self.create_detector(project=project) + data_source = self.create_data_source(organization=from_org) data_source.detectors.add(detector) + workflow = self.create_workflow(organization=from_org) + self.create_detector_workflow(detector=detector, workflow=workflow) - # Create a Workflow connected to the detector - workflow = Workflow.objects.create( - name="Test Workflow", - organization=from_org, - config={}, - ) - DetectorWorkflow.objects.create( - detector=detector, - workflow=workflow, - ) - - # Verify initial state - assert detector.project_id == project.id - assert data_source.organization_id == from_org.id - assert workflow.organization_id == from_org.id - assert query_subscription.project_id == project.id - - # Transfer the project project.transfer_to(organization=to_org) - # Refresh objects project.refresh_from_db() detector.refresh_from_db() data_source.refresh_from_db() workflow.refresh_from_db() - query_subscription.refresh_from_db() - # Expected behavior after transfer: - # 1. Project should be in the new organization assert project.organization_id == to_org.id - - # 2. Detector should still be attached to the project assert detector.project_id == project.id - - # 3. DataSource organization should be updated to the new organization assert data_source.organization_id == to_org.id - - # 4. QuerySubscription project should be updated to reflect the project's new org - assert query_subscription.project_id == project.id - - # 5. Workflow should be updated to the new organization assert workflow.organization_id == to_org.id - - # 6. DetectorWorkflow relationship should still exist - assert DetectorWorkflow.objects.filter( - detector=detector, - workflow=workflow, - ).exists() + assert DetectorWorkflow.objects.filter(detector=detector, workflow=workflow).exists() def test_transfer_to_organization_with_workflow_data_condition_groups(self) -> None: from_org = self.create_organization() team = self.create_team(organization=from_org) to_org = self.create_organization() - project = self.create_project(teams=[team]) - # Create a detector for this project detector = self.create_detector(project=project) - - # Create a workflow and connect it to the detector workflow = self.create_workflow(organization=from_org) self.create_detector_workflow(detector=detector, workflow=workflow) - - # Create a DataConditionGroup and connect it to the workflow condition_group = self.create_data_condition_group(organization=from_org) self.create_workflow_data_condition_group( workflow=workflow, condition_group=condition_group ) - # Verify initial state - assert detector.project_id == project.id - assert workflow.organization_id == from_org.id - assert condition_group.organization_id == from_org.id - - # Transfer the project project.transfer_to(organization=to_org) - # Refresh objects project.refresh_from_db() detector.refresh_from_db() workflow.refresh_from_db() condition_group.refresh_from_db() - # Expected behavior after transfer: - # 1. Project should be in the new organization assert project.organization_id == to_org.id - - # 2. Detector should still be attached to the project assert detector.project_id == project.id - - # 3. Workflow should be updated to the new organization assert workflow.organization_id == to_org.id - - # 4. DataConditionGroup should be updated to the new organization assert condition_group.organization_id == to_org.id - - # 5. WorkflowDataConditionGroup relationship should be preserved wdcg = condition_group.workflowdataconditiongroup_set.first() assert wdcg is not None assert wdcg.workflow_id == workflow.id @@ -651,24 +541,19 @@ def test_transfer_to_organization_does_not_transfer_shared_workflows(self) -> No team = self.create_team(organization=from_org) to_org = self.create_organization() - # Create two projects in the same organization project_a = self.create_project(teams=[team], name="Project A") project_b = self.create_project(teams=[team], organization=from_org, name="Project B") - # Create detectors for both projects detector_a = self.create_detector(project=project_a) detector_b = self.create_detector(project=project_b) - # Create a SHARED workflow connected to both detectors shared_workflow = self.create_workflow(organization=from_org, name="Shared Workflow") self.create_detector_workflow(detector=detector_a, workflow=shared_workflow) self.create_detector_workflow(detector=detector_b, workflow=shared_workflow) - # Create an EXCLUSIVE workflow only for project_a exclusive_workflow = self.create_workflow(organization=from_org, name="Exclusive Workflow") self.create_detector_workflow(detector=detector_a, workflow=exclusive_workflow) - # Create DataConditionGroups for both workflows shared_dcg = self.create_data_condition_group(organization=from_org) self.create_workflow_data_condition_group( workflow=shared_workflow, condition_group=shared_dcg @@ -679,16 +564,8 @@ def test_transfer_to_organization_does_not_transfer_shared_workflows(self) -> No workflow=exclusive_workflow, condition_group=exclusive_dcg ) - # Verify initial state - assert shared_workflow.organization_id == from_org.id - assert exclusive_workflow.organization_id == from_org.id - assert shared_dcg.organization_id == from_org.id - assert exclusive_dcg.organization_id == from_org.id - - # Transfer project_a to the new organization project_a.transfer_to(organization=to_org) - # Refresh all objects project_a.refresh_from_db() project_b.refresh_from_db() detector_a.refresh_from_db() @@ -698,29 +575,14 @@ def test_transfer_to_organization_does_not_transfer_shared_workflows(self) -> No shared_dcg.refresh_from_db() exclusive_dcg.refresh_from_db() - # Expected behavior after transfer: - - # 1. Both projects' basic attributes are correct assert project_a.organization_id == to_org.id assert project_b.organization_id == from_org.id - - # 2. Detectors follow their projects assert detector_a.project_id == project_a.id assert detector_b.project_id == project_b.id - - # 3. SHARED workflow should NOT transfer (still in old org) - # because it's used by detector_b which remains in from_org assert shared_workflow.organization_id == from_org.id - - # 4. EXCLUSIVE workflow SHOULD transfer (moved to new org) - # because it's only used by detector_a assert exclusive_workflow.organization_id == to_org.id - - # 5. DataConditionGroups follow their workflows assert shared_dcg.organization_id == from_org.id assert exclusive_dcg.organization_id == to_org.id - - # 6. Both detectors should still be connected to their workflows assert DetectorWorkflow.objects.filter( detector=detector_a, workflow=shared_workflow ).exists() @@ -735,43 +597,49 @@ def test_transfer_to_organization_with_detector_workflow_condition_group(self) - from_org = self.create_organization() team = self.create_team(organization=from_org) to_org = self.create_organization() - project = self.create_project(teams=[team]) - # Create a detector with a workflow_condition_group detector = self.create_detector(project=project) workflow_condition_group = self.create_data_condition_group(organization=from_org) - - # Assign the condition group directly to the detector detector.workflow_condition_group = workflow_condition_group detector.save() - # Verify initial state - assert detector.project_id == project.id - assert detector.workflow_condition_group_id == workflow_condition_group.id - assert workflow_condition_group.organization_id == from_org.id - - # Transfer the project project.transfer_to(organization=to_org) - # Refresh objects project.refresh_from_db() detector.refresh_from_db() workflow_condition_group.refresh_from_db() - # Expected behavior after transfer: - # 1. Project should be in the new organization assert project.organization_id == to_org.id - - # 2. Detector should still be attached to the project assert detector.project_id == project.id - - # 3. workflow_condition_group should transfer because it's exclusively owned by the detector assert workflow_condition_group.organization_id == to_org.id - - # 4. The FK relationship should be preserved assert detector.workflow_condition_group_id == workflow_condition_group.id + def test_transfer_to_organization_with_workflow_when_condition_groups(self) -> None: + from_org = self.create_organization() + to_org = self.create_organization() + team = self.create_team(organization=from_org) + project = self.create_project(teams=[team]) + + detector = self.create_detector(project=project) + when_condition_group = self.create_data_condition_group(organization=from_org) + workflow = self.create_workflow( + organization=from_org, when_condition_group=when_condition_group + ) + self.create_detector_workflow(detector=detector, workflow=workflow) + + project.transfer_to(organization=to_org) + + project.refresh_from_db() + detector.refresh_from_db() + workflow.refresh_from_db() + when_condition_group.refresh_from_db() + + assert project.organization_id == to_org.id + assert detector.project_id == project.id + assert workflow.organization_id == to_org.id + assert when_condition_group.organization_id == to_org.id + class ProjectOptionsTests(TestCase): """