diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index abb09e0823ec..13f2560d4b09 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -82,8 +82,13 @@ def validate_key(self, value: str | None) -> str | None: def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) - feature = attrs["feature"] - default_percentage_allocation = attrs["default_percentage_allocation"] + + # Safely get feature and allocation, falling back to instance for PATCH requests + feature = attrs.get("feature", getattr(self.instance, "feature", None)) + default_percentage_allocation = attrs.get( + "default_percentage_allocation", + getattr(self.instance, "default_percentage_allocation", 0), + ) total_sibling_percentage_allocation = ( self._get_siblings(feature).aggregate( @@ -91,6 +96,7 @@ def validate(self, attrs): # type: ignore[no-untyped-def] )["total_percentage_allocation"] or 0 ) + total_percentage_allocation = ( total_sibling_percentage_allocation + default_percentage_allocation ) @@ -136,10 +142,12 @@ def _validate_environment_allocations( ) def _validate_key_is_unique(self, attrs: dict[str, typing.Any]) -> None: - key = attrs.get("key") + key = attrs.get("key", getattr(self.instance, "key", None)) if key is None: return - if self._get_siblings(attrs["feature"]).filter(key=key).exists(): + + feature = attrs.get("feature", getattr(self.instance, "feature", None)) + if self._get_siblings(feature).filter(key=key).exists(): raise ValidationError( { "key": "Multivariate option with this key already exists for the feature." diff --git a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py index 3fb923d257b3..1c4a0ad629f9 100644 --- a/api/tests/unit/features/multivariate/test_unit_multivariate_views.py +++ b/api/tests/unit/features/multivariate/test_unit_multivariate_views.py @@ -1,3 +1,4 @@ +import json import uuid import pytest @@ -105,3 +106,33 @@ def test_list_mv_options__feature_in_other_project__returns_404( # Then assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_partial_update_multivariate_option__valid_data__returns_200_and_updates( + admin_client: APIClient, + project: Project, + multivariate_feature: Feature, +) -> None: + # Given + mv_option = multivariate_feature.multivariate_options.first() + + assert mv_option is not None + + url = f"/api/v1/projects/{project.id}/features/{multivariate_feature.id}/mv-options/{mv_option.id}/" + + new_key = "hero" + data = {"key": new_key} + + initial_allocation = mv_option.default_percentage_allocation + + # When + response = admin_client.patch( + url, data=json.dumps(data), content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_200_OK + + mv_option.refresh_from_db() + assert mv_option.key == new_key + assert mv_option.default_percentage_allocation == initial_allocation