diff --git a/api/features/models.py b/api/features/models.py index 1493a54c4849..5db67cc0dd8a 100644 --- a/api/features/models.py +++ b/api/features/models.py @@ -1085,12 +1085,21 @@ def copy_from(self, source_feature_state_value: "FeatureStateValue"): self.save() def get_skip_create_audit_log(self) -> bool: - return self.feature_state.get_skip_create_audit_log() + try: + return self.feature_state.get_skip_create_audit_log() + except ObjectDoesNotExist: + return False def get_update_log_message(self, history_instance) -> typing.Optional[str]: fs = self.feature_state - changes = history_instance.diff_against(history_instance.prev_record).changes + # NOTE: We have some feature state values that were created before we started + # tracking history, resulting in no prev_record. + changes = ( + history_instance.diff_against(history_instance.prev_record).changes + if history_instance.prev_record + else [] + ) if ( len(changes) == 1 and changes[0].field == "string_value" diff --git a/api/tests/unit/features/test_unit_features_models.py b/api/tests/unit/features/test_unit_features_models.py index ae26df187d41..0c5e8e47bb28 100644 --- a/api/tests/unit/features/test_unit_features_models.py +++ b/api/tests/unit/features/test_unit_features_models.py @@ -11,7 +11,12 @@ from environments.identities.models import Identity from environments.models import Environment from features.constants import ENVIRONMENT, FEATURE_SEGMENT, IDENTITY -from features.models import Feature, FeatureSegment, FeatureState +from features.models import ( + Feature, + FeatureSegment, + FeatureState, + FeatureStateValue, +) from features.versioning.models import EnvironmentFeatureVersion from features.workflows.core.models import ChangeRequest from projects.models import Project @@ -692,6 +697,28 @@ def test_feature_state_value_get_skip_create_audit_log_if_environment_feature_ve assert feature_state.feature_state_value.get_skip_create_audit_log() is True +def test_feature_state_value__get_skip_create_audit_log_for_deleted_feature_state( + feature: Feature, feature_segment: FeatureSegment, environment: Environment +): + # Give + feature_state = FeatureState.objects.create( + feature=feature, feature_segment=feature_segment, environment=environment + ) + feature_state_value = feature_state.feature_state_value + + # When + # Delete feature segment to cascade delete feature state + # instead of soft delete + feature_segment.delete() + + # Then + fsv_history_instance = FeatureStateValue.history.filter( + id=feature_state_value.id, history_type="-" + ).first() + + assert fsv_history_instance.instance.get_skip_create_audit_log() is False + + @pytest.mark.parametrize( "feature_segment_id, identity_id, expected_function_name", ( diff --git a/api/tests/unit/features/test_unit_features_views.py b/api/tests/unit/features/test_unit_features_views.py index 8874dbe91a50..b5b631efe007 100644 --- a/api/tests/unit/features/test_unit_features_views.py +++ b/api/tests/unit/features/test_unit_features_views.py @@ -2322,6 +2322,39 @@ def test_cannot_update_environment_of_a_feature_state( ) +def test_update_feature_state_without_history_of_fsv( + admin_client_new: APIClient, + environment: Environment, + feature: Feature, + feature_state: FeatureState, +) -> None: + # Given + url = reverse( + "api-v1:environments:environment-featurestates-detail", + args=[environment.api_key, feature_state.id], + ) + new_value = "new-value" + + # Remove historical feature state value + feature_state.feature_state_value.history.all().delete() + + data = { + "id": feature_state.id, + "feature_state_value": new_value, + "enabled": False, + "feature": feature.id, + "environment": environment.id, + "identity": None, + "feature_segment": None, + } + # When + response = admin_client_new.put( + url, data=json.dumps(data), content_type="application/json" + ) + # Then + assert response.status_code == status.HTTP_200_OK + + def test_cannot_update_feature_of_a_feature_state( admin_client_new: APIClient, environment: Environment,