From e2da62d33c1c311fac86a6f097b10e142f9e8959 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 12 Mar 2026 23:21:52 -0300 Subject: [PATCH] Fix an audit trail race condition when deleting segment overrides --- api/features/models.py | 18 +++++---- api/tests/unit/audit/test_unit_audit_tasks.py | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/api/features/models.py b/api/features/models.py index 5e32ce7ef7df..c3c557bc5a71 100644 --- a/api/features/models.py +++ b/api/features/models.py @@ -962,9 +962,9 @@ def get_create_log_message(self, history_instance) -> typing.Optional[str]: # t return audit_helpers.get_environment_feature_state_created_audit_message(self) - def get_update_log_message(self, history_instance) -> typing.Optional[str]: # type: ignore[no-untyped-def] + def get_update_log_message(self, history_instance: "FeatureState") -> str | None: if self.change_request and self.is_scheduled: - live_from: datetime.datetime = timezone.localtime(self.live_from) + live_from = timezone.localtime(self.live_from) return FEATURE_STATE_SCHEDULED_TO_UPDATE_MESSAGE % ( self.feature.name, self.change_request.title, @@ -975,11 +975,15 @@ def get_update_log_message(self, history_instance) -> typing.Optional[str]: # t self.feature.name, self.identity.identifier, ) - elif self.feature_segment: - return SEGMENT_FEATURE_STATE_UPDATED_MESSAGE % ( - self.feature.name, - self.feature_segment.segment.name, - ) + if self.feature_segment_id: + try: + return SEGMENT_FEATURE_STATE_UPDATED_MESSAGE % ( + self.feature.name, + self.feature_segment.segment.name, # type: ignore[union-attr] + ) + except FeatureSegment.DoesNotExist: + # Cascade-deleted from segment overrides + return None return FEATURE_STATE_UPDATED_MESSAGE % self.feature.name def get_delete_log_message(self, history_instance) -> typing.Optional[str]: # type: ignore[no-untyped-def] diff --git a/api/tests/unit/audit/test_unit_audit_tasks.py b/api/tests/unit/audit/test_unit_audit_tasks.py index 9264d37114c2..3f8f9e30a91c 100644 --- a/api/tests/unit/audit/test_unit_audit_tasks.py +++ b/api/tests/unit/audit/test_unit_audit_tasks.py @@ -225,6 +225,44 @@ def test_create_audit_log_from_historical_record_creates_audit_log_with_correct_ ) +def test_create_audit_log_from_historical_record__cascade_deleted_feature_segment__does_nothing( + admin_user: FFAdminUser, + feature: Feature, + segment: Segment, + environment: Environment, + mocker: MockerFixture, +) -> None: + """https://github.com/Flagsmith/flagsmith/issues/6792""" + # Given + feature_segment = FeatureSegment.objects.create( + environment=environment, + feature=feature, + segment=segment, + ) + feature_state = FeatureState.objects.create( + environment=environment, + feature=feature, + feature_segment=feature_segment, + ) + feature_state.enabled = not feature_state.enabled + feature_state.save() # creates a "~" historical record + history_instance = feature_state.history.filter(history_type="~").first() + assert history_instance is not None + feature_segment.delete() # cascade-deletes the FeatureState + get_update_log_message = mocker.spy(FeatureState, "get_update_log_message") + + # When + create_audit_log_from_historical_record( + history_instance.history_id, + admin_user.id, + feature_state.history_record_class_path, + ) + + # Then + assert get_update_log_message.call_count == 1 + assert get_update_log_message.spy_return is None + + def test_create_segment_priorities_changed_audit_log( admin_user: FFAdminUser, feature_segment: FeatureSegment,