|
8 | 8 |
|
9 | 9 | from features.constants import RESERVED_VARIANT_KEY_MESSAGE |
10 | 10 | from features.models import Feature |
| 11 | +from features.multivariate.models import MultivariateFeatureOption |
11 | 12 | from organisations.models import Organisation |
12 | 13 | from projects.models import Project |
13 | 14 | from users.models import FFAdminUser |
@@ -420,6 +421,91 @@ def test_create_mv_option__total_allocation_exceeds_100__returns_bad_request( # |
420 | 421 | ] |
421 | 422 |
|
422 | 423 |
|
| 424 | +@pytest.mark.parametrize( |
| 425 | + "client", |
| 426 | + [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], |
| 427 | +) |
| 428 | +def test_create_mv_option__environment_allocation_drifted_from_option_default__returns_bad_request( # type: ignore[no-untyped-def] # noqa: E501 |
| 429 | + project, environment, environment_api_key, mv_option_50_percent, client, feature |
| 430 | +): |
| 431 | + """ |
| 432 | + Reproduces https://github.com/Flagsmith/flagsmith/issues/7369: an |
| 433 | + environment's actual `MultivariateFeatureStateValue.percentage_allocation` |
| 434 | + can drift from the option's `default_percentage_allocation` via a |
| 435 | + per-environment edit. Adding a new option that only overflows 100% for |
| 436 | + that one environment must still be rejected, not just options that |
| 437 | + overflow the option-level defaults. |
| 438 | + """ |
| 439 | + # Given - the existing option's allocation is bumped from 50% to 100% for |
| 440 | + # this environment only, so this environment's actual feature state values |
| 441 | + # (100%) have diverged from the option-level default (50%). |
| 442 | + get_feature_states_url = reverse( |
| 443 | + "api-v1:environments:environment-featurestates-list", args=[environment_api_key] |
| 444 | + ) |
| 445 | + feature_state = next( |
| 446 | + filter( |
| 447 | + lambda fs: fs["feature"] == feature, |
| 448 | + client.get(get_feature_states_url).json()["results"], |
| 449 | + ) |
| 450 | + ) |
| 451 | + mv_fsv = feature_state["multivariate_feature_state_values"][0] |
| 452 | + update_url = reverse( |
| 453 | + "api-v1:environments:environment-featurestates-detail", |
| 454 | + args=[environment_api_key, feature_state["id"]], |
| 455 | + ) |
| 456 | + update_response = client.put( |
| 457 | + update_url, |
| 458 | + data=json.dumps( |
| 459 | + { |
| 460 | + "id": feature_state["id"], |
| 461 | + "feature_state_value": "big", |
| 462 | + "multivariate_feature_state_values": [ |
| 463 | + { |
| 464 | + "multivariate_feature_option": mv_fsv[ |
| 465 | + "multivariate_feature_option" |
| 466 | + ], |
| 467 | + "id": mv_fsv["id"], |
| 468 | + "percentage_allocation": 100, |
| 469 | + } |
| 470 | + ], |
| 471 | + "identity": None, |
| 472 | + "enabled": False, |
| 473 | + "feature": feature, |
| 474 | + "environment": environment, |
| 475 | + "feature_segment": None, |
| 476 | + } |
| 477 | + ), |
| 478 | + content_type="application/json", |
| 479 | + ) |
| 480 | + assert update_response.status_code == status.HTTP_200_OK |
| 481 | + |
| 482 | + url = reverse( |
| 483 | + "api-v1:projects:feature-mv-options-list", |
| 484 | + args=[project, feature], |
| 485 | + ) |
| 486 | + data = { |
| 487 | + "type": "unicode", |
| 488 | + "feature": feature, |
| 489 | + "string_value": "bigger", |
| 490 | + "default_percentage_allocation": 10, |
| 491 | + } |
| 492 | + |
| 493 | + # When - a new option is added. At the option level, 50% + 10% = 60% <= 100%, |
| 494 | + # so this looks valid, but it would push this environment's actual |
| 495 | + # allocation to 110%. |
| 496 | + response = client.post( |
| 497 | + url, |
| 498 | + data=json.dumps(data), |
| 499 | + content_type="application/json", |
| 500 | + ) |
| 501 | + |
| 502 | + # Then |
| 503 | + assert response.status_code == status.HTTP_400_BAD_REQUEST |
| 504 | + # The option must not be left half wired-up (created, but missing feature |
| 505 | + # state values for the environment that rejected it). |
| 506 | + assert MultivariateFeatureOption.objects.filter(feature=feature).count() == 1 |
| 507 | + |
| 508 | + |
423 | 509 | @pytest.mark.parametrize( |
424 | 510 | "client", |
425 | 511 | [lazy_fixture("admin_master_api_key_client"), lazy_fixture("admin_client")], |
|
0 commit comments