diff --git a/api/features/versioning/tasks.py b/api/features/versioning/tasks.py index abb885a88ad7..26f56e70e909 100644 --- a/api/features/versioning/tasks.py +++ b/api/features/versioning/tasks.py @@ -261,6 +261,18 @@ def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> No ) +def _build_feature_state_change_summary(fs: FeatureState) -> str: + if fs.identity_id: + scope = f"Identity override ({fs.identity.identifier})" # type: ignore[union-attr] + elif fs.feature_segment_id: + scope = f"Segment override ({fs.feature_segment.segment.name})" # type: ignore[union-attr] + else: + scope = "Environment default" + + state = "enabled" if fs.enabled else "disabled" + return f"{scope}: {state}" + + @register_task_handler() def create_environment_feature_version_published_audit_log_task( environment_feature_version_uuid: str, @@ -269,12 +281,31 @@ def create_environment_feature_version_published_audit_log_task( "environment", "feature" ).get(uuid=environment_feature_version_uuid) + header = ( + ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE + % environment_feature_version.feature.name + ) + + changed_states = get_updated_feature_states_for_version(environment_feature_version) + + if changed_states: + changed_states = list( + FeatureState.objects.filter( + id__in=[fs.id for fs in changed_states] + ).select_related("feature_segment__segment", "identity") + ) + change_lines = "\n".join( + f"- {_build_feature_state_change_summary(fs)}" for fs in changed_states + ) + log = f"{header}\n{change_lines}" + else: + log = header + AuditLog.objects.create( environment=environment_feature_version.environment, related_object_type=RelatedObjectType.EF_VERSION.name, related_object_uuid=environment_feature_version.uuid, - log=ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE - % environment_feature_version.feature.name, + log=log, author_id=environment_feature_version.published_by_id, master_api_key_id=environment_feature_version.published_by_api_key_id, ) diff --git a/api/tests/unit/audit/test_unit_audit_services.py b/api/tests/unit/audit/test_unit_audit_services.py index 48308bc97dd1..e0ce6b067b0b 100644 --- a/api/tests/unit/audit/test_unit_audit_services.py +++ b/api/tests/unit/audit/test_unit_audit_services.py @@ -5,6 +5,7 @@ create_feature_state_updated_by_change_request_audit_log, create_segment_priorities_changed_audit_log, ) +from environments.identities.models import Identity from environments.models import Environment from features.models import Feature, FeatureSegment, FeatureState from features.versioning.models import EnvironmentFeatureVersion @@ -12,6 +13,7 @@ create_environment_feature_version_published_audit_log_task, ) from features.workflows.core.models import ChangeRequest +from segments.models import Segment def test_get_audited_instance_from_audit_log_record__change_request__return_expected( @@ -98,6 +100,117 @@ def test_get_audited_instance_from_audit_log_record__historical_record__return_e assert instance == change_request +def test_create_environment_feature_version_published_audit_log_task__no_changes__uses_header_only( + environment_v2_versioning: Environment, + feature: Feature, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert audit_log.log == f"New version published for feature: {feature.name}" + + +def test_create_environment_feature_version_published_audit_log_task__environment_default_changed__includes_detail( + environment_v2_versioning: Environment, + feature: Feature, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + fs = version.feature_states.filter(feature=feature).first() + assert fs is not None + fs.enabled = not fs.enabled + fs.save() + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"New version published for feature: {feature.name}" in audit_log.log + assert "Environment default:" in audit_log.log + + +def test_create_environment_feature_version_published_audit_log_task__segment_override_changed__includes_segment_name( + environment_v2_versioning: Environment, + feature: Feature, + segment: Segment, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + feature_segment = FeatureSegment.objects.create( + feature=feature, + segment=segment, + environment=environment_v2_versioning, + environment_feature_version=version, + ) + FeatureState.objects.create( + feature=feature, + environment=environment_v2_versioning, + feature_segment=feature_segment, + environment_feature_version=version, + enabled=True, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"Segment override ({segment.name})" in audit_log.log + + +def test_create_environment_feature_version_published_audit_log_task__identity_override_changed__includes_identity( + environment_v2_versioning: Environment, + feature: Feature, + identity: Identity, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + FeatureState.objects.create( + feature=feature, + environment=environment_v2_versioning, + identity=identity, + environment_feature_version=version, + enabled=True, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"Identity override ({identity.identifier})" in audit_log.log + + def test_get_audited_instance_from_audit_log_record__unexpected_audit_log__return_none( change_request: ChangeRequest, ) -> None: diff --git a/api/tests/unit/features/versioning/test_unit_versioning_views.py b/api/tests/unit/features/versioning/test_unit_versioning_views.py index ba5f4003ac14..6f4cfe2a6d68 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_views.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_views.py @@ -336,7 +336,10 @@ def test_publish_feature_version__unpublished_version__publishes_and_creates_aud related_object_uuid=environment_feature_version.uuid, ).first() assert record - assert record.log == ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE % feature.name + assert record.log == ( + f"{ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE % feature.name}\n" + f"- Environment default: disabled" + ) @pytest.mark.parametrize("live_from", (None, tomorrow))