From 01f7f33635008d38080aa6c3e70c50e582686ff5 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Mon, 23 Feb 2026 15:58:38 +0530 Subject: [PATCH 01/15] fix(multivariate): Handled missing default_percentage_allocation in option validation --- api/features/multivariate/serializers.py | 3 +- .../test_integration_multivariate.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 6c04cb5ac68b..e8daa80cd11a 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -65,7 +65,8 @@ def validate(self, attrs): # type: ignore[no-untyped-def] or 0 ) total_percentage_allocation = ( - total_sibling_percentage_allocation + attrs["default_percentage_allocation"] + total_sibling_percentage_allocation + + attrs.get("default_percentage_allocation", 100) ) if total_percentage_allocation > 100: diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 49effb4c8ff9..a5cfe3d55e85 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -39,6 +39,39 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): assert response.json()["id"] assert set(data.items()).issubset(set(response.json().items())) +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_create_mv_option_without_default_percentage_allocation_uses_default( + client: APIClient, + project: int, + feature: int, +) -> None: + # Given + url = reverse( + "api-v1:projects:feature-mv-options-list", + args=[project, feature], + ) + data = { + "type": "unicode", + "feature": feature, + "string_value": "test_value", + # Note: default_percentage_allocation is intentionally omitted + } + + # When + response = client.post( + url, + data=json.dumps(data), + content_type="application/json", + ) + + # Then + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["id"] + assert response.json()["default_percentage_allocation"] == 100 + @pytest.mark.parametrize( "client, feature_id", From 2c2c0422e00e554a7d113692b0b99aecec804921 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:44:12 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/features/multivariate/serializers.py | 5 ++--- .../features/multivariate/test_integration_multivariate.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index e8daa80cd11a..7de804edbf8c 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -64,9 +64,8 @@ def validate(self, attrs): # type: ignore[no-untyped-def] )["total_percentage_allocation"] or 0 ) - total_percentage_allocation = ( - total_sibling_percentage_allocation - + attrs.get("default_percentage_allocation", 100) + total_percentage_allocation = total_sibling_percentage_allocation + attrs.get( + "default_percentage_allocation", 100 ) if total_percentage_allocation > 100: diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index a5cfe3d55e85..30aa61b9a798 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -39,6 +39,7 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): assert response.json()["id"] assert set(data.items()).issubset(set(response.json().items())) + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], From 69310cfb9079c5d413fb94d6a54acab728fa32c9 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Mon, 23 Feb 2026 16:26:25 +0530 Subject: [PATCH 03/15] fix(multivariate): Handled update operation with missing default_percentage_allocation --- api/features/multivariate/serializers.py | 11 +++++-- .../test_integration_multivariate.py | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 7de804edbf8c..6759b0400abb 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -58,14 +58,21 @@ class Meta(NestedMultivariateFeatureOptionSerializer.Meta): def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) + + # For updates, use existing instance value; for creates, use model default + default_allocation = ( + self.instance.default_percentage_allocation if self.instance else 100 + ) + total_sibling_percentage_allocation = ( self._get_siblings(attrs["feature"]).aggregate( total_percentage_allocation=Sum("default_percentage_allocation") )["total_percentage_allocation"] or 0 ) - total_percentage_allocation = total_sibling_percentage_allocation + attrs.get( - "default_percentage_allocation", 100 + total_percentage_allocation = ( + total_sibling_percentage_allocation + + attrs.get("default_percentage_allocation", default_allocation) ) if total_percentage_allocation > 100: diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 30aa61b9a798..9c16cb27482d 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -73,6 +73,38 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( assert response.json()["id"] assert response.json()["default_percentage_allocation"] == 100 +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_partial_update_mv_option_without_default_percentage_allocation_uses_existing_value( + client: APIClient, + project: int, + feature: int, + mv_option_50_percent: int, +) -> None: + # Given + url = reverse( + "api-v1:projects:feature-mv-options-detail", + args=[project, feature, mv_option_50_percent], + ) + data = { + "string_value": "updated_value", + # Note: default_percentage_allocation is intentionally omitted + } + + # When + response = client.patch( + url, + data=json.dumps(data), + content_type="application/json", + ) + + # Then + assert response.status_code == status.HTTP_200_OK + assert response.json()["string_value"] == "updated_value" + # Should retain original 50% allocation, not default to 100 + assert response.json()["default_percentage_allocation"] == 50 @pytest.mark.parametrize( "client, feature_id", From ea26d31ce9b0a9607c135f9c918884f65053c818 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:56:44 +0000 Subject: [PATCH 04/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/features/multivariate/serializers.py | 5 ++--- .../features/multivariate/test_integration_multivariate.py | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 6759b0400abb..bae1ea46a055 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -70,9 +70,8 @@ def validate(self, attrs): # type: ignore[no-untyped-def] )["total_percentage_allocation"] or 0 ) - total_percentage_allocation = ( - total_sibling_percentage_allocation - + attrs.get("default_percentage_allocation", default_allocation) + total_percentage_allocation = total_sibling_percentage_allocation + attrs.get( + "default_percentage_allocation", default_allocation ) if total_percentage_allocation > 100: diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 9c16cb27482d..ff9067f8a34e 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -73,6 +73,7 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( assert response.json()["id"] assert response.json()["default_percentage_allocation"] == 100 + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], @@ -106,6 +107,7 @@ def test_partial_update_mv_option_without_default_percentage_allocation_uses_exi # Should retain original 50% allocation, not default to 100 assert response.json()["default_percentage_allocation"] == 50 + @pytest.mark.parametrize( "client, feature_id", [ From 39af977ae01067edf9618f111f04db5df3c6f452 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Mon, 23 Feb 2026 20:45:46 +0530 Subject: [PATCH 05/15] fix(multivariate): Handled update operation with missing feature --- api/features/multivariate/serializers.py | 12 +++++++----- .../multivariate/test_integration_multivariate.py | 8 ++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index bae1ea46a055..9ce98323ae99 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -59,19 +59,21 @@ class Meta(NestedMultivariateFeatureOptionSerializer.Meta): def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) - # For updates, use existing instance value; for creates, use model default - default_allocation = ( + # For partial updated, use existing instance value as fallback + feature = attrs.get("feature", self.instance.feature if self.instance else None) + default_allocation = attrs.get( + "default_percentage_allocation", self.instance.default_percentage_allocation if self.instance else 100 ) total_sibling_percentage_allocation = ( - self._get_siblings(attrs["feature"]).aggregate( + self._get_siblings(feature).aggregate( total_percentage_allocation=Sum("default_percentage_allocation") )["total_percentage_allocation"] or 0 ) - total_percentage_allocation = total_sibling_percentage_allocation + attrs.get( - "default_percentage_allocation", default_allocation + total_percentage_allocation = ( + total_sibling_percentage_allocation + default_allocation ) if total_percentage_allocation > 100: diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index ff9067f8a34e..ece02206e620 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -39,7 +39,6 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): assert response.json()["id"] assert set(data.items()).issubset(set(response.json().items())) - @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], @@ -58,7 +57,6 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( "type": "unicode", "feature": feature, "string_value": "test_value", - # Note: default_percentage_allocation is intentionally omitted } # When @@ -78,7 +76,7 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) -def test_partial_update_mv_option_without_default_percentage_allocation_uses_existing_value( +def test_partial_update_mv_option_without_feature_and_allocation_uses_existing_values( client: APIClient, project: int, feature: int, @@ -91,7 +89,6 @@ def test_partial_update_mv_option_without_default_percentage_allocation_uses_exi ) data = { "string_value": "updated_value", - # Note: default_percentage_allocation is intentionally omitted } # When @@ -104,9 +101,8 @@ def test_partial_update_mv_option_without_default_percentage_allocation_uses_exi # Then assert response.status_code == status.HTTP_200_OK assert response.json()["string_value"] == "updated_value" - # Should retain original 50% allocation, not default to 100 assert response.json()["default_percentage_allocation"] == 50 - + assert response.json()["feature"] == feature @pytest.mark.parametrize( "client, feature_id", From f706f13b4dc6ad814594a06829525f91dacf9442 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:16:07 +0000 Subject: [PATCH 06/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/features/multivariate/serializers.py | 2 +- .../features/multivariate/test_integration_multivariate.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 9ce98323ae99..c708c1b38ae8 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -63,7 +63,7 @@ def validate(self, attrs): # type: ignore[no-untyped-def] feature = attrs.get("feature", self.instance.feature if self.instance else None) default_allocation = attrs.get( "default_percentage_allocation", - self.instance.default_percentage_allocation if self.instance else 100 + self.instance.default_percentage_allocation if self.instance else 100, ) total_sibling_percentage_allocation = ( diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index ece02206e620..d66dd1d9dc66 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -39,6 +39,7 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): assert response.json()["id"] assert set(data.items()).issubset(set(response.json().items())) + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], @@ -104,6 +105,7 @@ def test_partial_update_mv_option_without_feature_and_allocation_uses_existing_v assert response.json()["default_percentage_allocation"] == 50 assert response.json()["feature"] == feature + @pytest.mark.parametrize( "client, feature_id", [ From fcab2815d9d5e830df3b63a8dd10ed6a5fda96b9 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Mon, 23 Feb 2026 21:27:53 +0530 Subject: [PATCH 07/15] fix(multivariate): Handled None feature --- api/features/multivariate/serializers.py | 7 ++ .../test_integration_multivariate.py | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index c708c1b38ae8..f7889ddbca69 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -61,6 +61,13 @@ def validate(self, attrs): # type: ignore[no-untyped-def] # For partial updated, use existing instance value as fallback feature = attrs.get("feature", self.instance.feature if self.instance else None) + if feature is None: + raise serializers.ValidationError( + { + "feature": "Feature is required" + } + ) + default_allocation = attrs.get( "default_percentage_allocation", self.instance.default_percentage_allocation if self.instance else 100, diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index d66dd1d9dc66..4fb261f10922 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -73,6 +73,41 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( assert response.json()["default_percentage_allocation"] == 100 +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_create_mv_option_without_default_percentage_allocation_with_existing_sibling_and_total_gt_100_returns_400( # type: ignore[no-untyped-def] # noqa: E501 + client: APIClient, + project: int, + feature: int, + mv_option_50_percent: int, +) -> None: + # Given + url = reverse( + "api-v1:projects:feature-mv-options-list", + args=[project, feature], + ) + data = { + "type": "unicode", + "feature": feature, + "string_value": "another_value", + } + + # When + response = client.post( + url, + data=json.dumps(data), + content_type="application/json", + ) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["default_percentage_allocation"] == [ + "Invalid percentage allocation" + ] + + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], @@ -249,6 +284,55 @@ def test_can_update_default_percentage_allocation( # type: ignore[no-untyped-de assert set(data.items()).issubset(set(response.json().items())) +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_partial_update_default_percentage_allocation_that_pushes_the_total_percentage_allocation_over_100_returns_400( # type: ignore[no-untyped-def] # noqa: E501 + project, + mv_option_50_percent, + client, + feature, +): + # First let's create another mv_option with 30 percent allocation + url = reverse( + "api-v1:projects:feature-mv-options-list", + args=[project, feature], + ) + data = { + "type": "unicode", + "feature": feature, + "string_value": "bigger", + "default_percentage_allocation": 30, + } + response = client.post( + url, + data=json.dumps(data), + content_type="application/json", + ) + mv_option_30_percent = response.json()["id"] + + # Next, let's partially update the 30 percent mv option to 51 percent + url = reverse( + "api-v1:projects:feature-mv-options-detail", + args=[project, feature, mv_option_30_percent], + ) + data = { + "default_percentage_allocation": 51, + } + # When + response = client.patch( + url, + data=json.dumps(data), + content_type="application/json", + ) + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["default_percentage_allocation"] == [ + "Invalid percentage allocation" + ] + + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], From 689c2aa9a8778b6eea849214ba4552808b1f4ead Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:58:14 +0000 Subject: [PATCH 08/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/features/multivariate/serializers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index f7889ddbca69..cffe8cf72d8f 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -62,12 +62,8 @@ def validate(self, attrs): # type: ignore[no-untyped-def] # For partial updated, use existing instance value as fallback feature = attrs.get("feature", self.instance.feature if self.instance else None) if feature is None: - raise serializers.ValidationError( - { - "feature": "Feature is required" - } - ) - + raise serializers.ValidationError({"feature": "Feature is required"}) + default_allocation = attrs.get( "default_percentage_allocation", self.instance.default_percentage_allocation if self.instance else 100, From 42525554112af12918f4db2347537c491518381c Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Mon, 23 Feb 2026 21:35:25 +0530 Subject: [PATCH 09/15] fix(multivariate): Added a clear assertion --- .../features/multivariate/test_integration_multivariate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 4fb261f10922..6ee496a4c8d7 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -310,6 +310,7 @@ def test_partial_update_default_percentage_allocation_that_pushes_the_total_perc data=json.dumps(data), content_type="application/json", ) + assert response.status_code == status.HTTP_201_CREATED mv_option_30_percent = response.json()["id"] # Next, let's partially update the 30 percent mv option to 51 percent From bfbab35fe42fe1f3ab175c2d641f6e2be8dd8e1f Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Tue, 24 Feb 2026 22:57:41 +0530 Subject: [PATCH 10/15] fix(multivariate): Review suggestion - Updated the test naming convention and added types --- .../test_integration_multivariate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 6ee496a4c8d7..317a1e6a5544 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -44,7 +44,7 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) -def test_create_mv_option_without_default_percentage_allocation_uses_default( +def test_create_mv_option__without_default_percentage_allocation__uses_default( client: APIClient, project: int, feature: int, @@ -77,7 +77,7 @@ def test_create_mv_option_without_default_percentage_allocation_uses_default( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) -def test_create_mv_option_without_default_percentage_allocation_with_existing_sibling_and_total_gt_100_returns_400( # type: ignore[no-untyped-def] # noqa: E501 +def test_create_mv_option__without_default_percentage_allocation_with_existing_sibling_and_total_gt_100__returns_400( # noqa: E501 client: APIClient, project: int, feature: int, @@ -112,7 +112,7 @@ def test_create_mv_option_without_default_percentage_allocation_with_existing_si "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) -def test_partial_update_mv_option_without_feature_and_allocation_uses_existing_values( +def test_partial_update_mv_option__without_feature_and_allocation__uses_existing_values( client: APIClient, project: int, feature: int, @@ -288,12 +288,12 @@ def test_can_update_default_percentage_allocation( # type: ignore[no-untyped-de "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) -def test_partial_update_default_percentage_allocation_that_pushes_the_total_percentage_allocation_over_100_returns_400( # type: ignore[no-untyped-def] # noqa: E501 - project, - mv_option_50_percent, - client, - feature, -): +def test_partial_update_default_percentage_allocation__sibling_total_gt_100__returns_400( # noqa: E501 + project: int, + mv_option_50_percent: int, + client: APIClient, + feature: int, +) -> None: # First let's create another mv_option with 30 percent allocation url = reverse( "api-v1:projects:feature-mv-options-list", From eef3806aa812ff786b040d3045b164c923ff7fab Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Wed, 25 Feb 2026 00:05:44 +0530 Subject: [PATCH 11/15] fix(multivariate): Added support for path param feature name --- api/features/multivariate/serializers.py | 20 +++++++++-- .../test_integration_multivariate.py | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index cffe8cf72d8f..817a06fa73e4 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -59,11 +59,27 @@ class Meta(NestedMultivariateFeatureOptionSerializer.Meta): def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) - # For partial updated, use existing instance value as fallback - feature = attrs.get("feature", self.instance.feature if self.instance else None) + # For partial updates or nested routes, use existing instance or URL value as fallback + feature = attrs.get("feature") + if feature is None and self.instance is not None: + feature = self.instance.feature + + if feature is None: + view = self.context.get("view") + feature_pk = getattr(view, "kwargs", {}).get("feature_pk") if view else None + project_pk = getattr(view, "kwargs", {}).get("project_pk") if view else None + if feature_pk is not None: + qs = Feature.objects.filter(pk=feature_pk) + if project_pk is not None: + qs = qs.filter(project__id=project_pk) + feature = qs.first() + if feature is None: raise serializers.ValidationError({"feature": "Feature is required"}) + # Ensure feature is always present in validated data (e.g. for creates via nested routes) + attrs["feature"] = feature + default_allocation = attrs.get( "default_percentage_allocation", self.instance.default_percentage_allocation if self.instance else 100, diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 317a1e6a5544..fbf03ad482c0 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -40,6 +40,40 @@ def test_can_create_mv_option(client, project, mv_option_50_percent, feature): assert set(data.items()).issubset(set(response.json().items())) +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_create_mv_option__without_feature_in_payload__uses_feature_from_url( + client: APIClient, + project: int, + feature: int, +) -> None: + # Given + url = reverse( + "api-v1:projects:feature-mv-options-list", + args=[project, feature], + ) + data = { + "type": "unicode", + "string_value": "bigger", + "default_percentage_allocation": 50, + } + # When + response = client.post( + url, + data=json.dumps(data), + content_type="application/json", + ) + # Then + assert response.status_code == status.HTTP_201_CREATED + body = response.json() + assert body["id"] + assert body["feature"] == feature + assert body["string_value"] == "bigger" + assert body["default_percentage_allocation"] == 50 + + @pytest.mark.parametrize( "client", [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], From 06c79d096a0fc6cb914fcfa62bcb7ca389fbb8f1 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Wed, 25 Feb 2026 14:02:29 +0530 Subject: [PATCH 12/15] fix(multivariate): Added support for null default_percentage_allocation from database --- api/features/multivariate/serializers.py | 8 +++- .../test_integration_multivariate.py | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 817a06fa73e4..f080ce346bd3 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -80,9 +80,15 @@ def validate(self, attrs): # type: ignore[no-untyped-def] # Ensure feature is always present in validated data (e.g. for creates via nested routes) attrs["feature"] = feature + # Handle legacy records where default_percentage_allocation may be NULL in the database. + if self.instance and self.instance.default_percentage_allocation is not None: + instance_default_allocation = self.instance.default_percentage_allocation + else: + instance_default_allocation = 100 + default_allocation = attrs.get( "default_percentage_allocation", - self.instance.default_percentage_allocation if self.instance else 100, + instance_default_allocation, ) total_sibling_percentage_allocation = ( diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index fbf03ad482c0..69df3e1bad26 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -7,6 +7,7 @@ from rest_framework.test import APIClient from features.models import Feature +from features.multivariate.models import MultivariateFeatureOption from organisations.models import Organisation from projects.models import Project from users.models import FFAdminUser @@ -175,6 +176,44 @@ def test_partial_update_mv_option__without_feature_and_allocation__uses_existing assert response.json()["feature"] == feature +@pytest.mark.parametrize( + "client", + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], +) +def test_partial_update_mv_option__handles_legacy_null_default_percentage_allocation( # noqa: E501 + client: APIClient, + project: int, + feature: int, + mv_option_50_percent: int, +) -> None: + # Given + # Simulate a legacy database record with a NULL default_percentage_allocation. + MultivariateFeatureOption.objects.filter( + id=mv_option_50_percent, + ).update(default_percentage_allocation=None) + + url = reverse( + "api-v1:projects:feature-mv-options-detail", + args=[project, feature, mv_option_50_percent], + ) + data = { + "string_value": "updated_legacy_value", + } + + # When + response = client.patch( + url, + data=json.dumps(data), + content_type="application/json", + ) + # Then + assert response.status_code == status.HTTP_200_OK + body = response.json() + assert body["string_value"] == "updated_legacy_value" + assert body["default_percentage_allocation"] == 100 + assert body["feature"] == feature + + @pytest.mark.parametrize( "client, feature_id", [ From bd5508b6bcab0485f49340a227ce1bf49daf1a7a Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Wed, 25 Feb 2026 14:20:21 +0530 Subject: [PATCH 13/15] fix(multivariate): Added default allocation write --- api/features/multivariate/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index f080ce346bd3..516f7c8f0ef2 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -90,6 +90,7 @@ def validate(self, attrs): # type: ignore[no-untyped-def] "default_percentage_allocation", instance_default_allocation, ) + attrs["default_percentage_allocation"] = default_allocation total_sibling_percentage_allocation = ( self._get_siblings(feature).aggregate( From 60196d75526a1656eb3183723aadd597dac5e90d Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Wed, 25 Feb 2026 14:38:07 +0530 Subject: [PATCH 14/15] fix(multivariate): align MV option validation with persistence and add coverage --- api/features/multivariate/serializers.py | 3 ++ .../test_integration_multivariate.py | 54 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/api/features/multivariate/serializers.py b/api/features/multivariate/serializers.py index 516f7c8f0ef2..399d48ece8e6 100644 --- a/api/features/multivariate/serializers.py +++ b/api/features/multivariate/serializers.py @@ -55,6 +55,9 @@ def get_control_value(self, obj: dict[str, typing.Any]) -> str | int | bool | No class MultivariateFeatureOptionSerializer(NestedMultivariateFeatureOptionSerializer): class Meta(NestedMultivariateFeatureOptionSerializer.Meta): fields = NestedMultivariateFeatureOptionSerializer.Meta.fields + ("feature",) # type: ignore[assignment] + extra_kwargs = { + "feature": {"required": False}, + } def validate(self, attrs): # type: ignore[no-untyped-def] attrs = super().validate(attrs) diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 69df3e1bad26..39969a54ced9 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -8,6 +8,7 @@ from features.models import Feature from features.multivariate.models import MultivariateFeatureOption +from features.multivariate.serializers import MultivariateFeatureOptionSerializer from organisations.models import Organisation from projects.models import Project from users.models import FFAdminUser @@ -181,37 +182,38 @@ def test_partial_update_mv_option__without_feature_and_allocation__uses_existing [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) def test_partial_update_mv_option__handles_legacy_null_default_percentage_allocation( # noqa: E501 - client: APIClient, project: int, feature: int, mv_option_50_percent: int, ) -> None: # Given - # Simulate a legacy database record with a NULL default_percentage_allocation. - MultivariateFeatureOption.objects.filter( - id=mv_option_50_percent, - ).update(default_percentage_allocation=None) - - url = reverse( - "api-v1:projects:feature-mv-options-detail", - args=[project, feature, mv_option_50_percent], - ) - data = { - "string_value": "updated_legacy_value", - } - - # When - response = client.patch( - url, - data=json.dumps(data), - content_type="application/json", - ) - # Then - assert response.status_code == status.HTTP_200_OK - body = response.json() - assert body["string_value"] == "updated_legacy_value" - assert body["default_percentage_allocation"] == 100 - assert body["feature"] == feature + # Simulate a legacy instance in memory where default_percentage_allocation is NULL. + mv_option = MultivariateFeatureOption.objects.get(id=mv_option_50_percent) + mv_option.default_percentage_allocation = None + + serializer = MultivariateFeatureOptionSerializer( + instance=mv_option, + data={"string_value": "updated_legacy_value"}, + partial=True, + context={ + "view": type( + "View", + (), + { + "kwargs": { + "project_pk": project, + "feature_pk": feature, + }, + }, + )(), + }, + ) + assert serializer.is_valid(), serializer.errors + instance = serializer.save() + + assert instance.string_value == "updated_legacy_value" + assert instance.default_percentage_allocation == 100 + assert instance.feature_id == feature @pytest.mark.parametrize( From 2929c71482e47634d70bd971d75d9b9f252efd14 Mon Sep 17 00:00:00 2001 From: Aditya Pradhan Date: Wed, 25 Feb 2026 15:11:30 +0530 Subject: [PATCH 15/15] fix(multivariate): Added missing test arg --- .../features/multivariate/test_integration_multivariate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/integration/features/multivariate/test_integration_multivariate.py b/api/tests/integration/features/multivariate/test_integration_multivariate.py index 39969a54ced9..864370233658 100644 --- a/api/tests/integration/features/multivariate/test_integration_multivariate.py +++ b/api/tests/integration/features/multivariate/test_integration_multivariate.py @@ -182,6 +182,7 @@ def test_partial_update_mv_option__without_feature_and_allocation__uses_existing [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], ) def test_partial_update_mv_option__handles_legacy_null_default_percentage_allocation( # noqa: E501 + client: APIClient, project: int, feature: int, mv_option_50_percent: int,