diff --git a/docs/mocked_backend/README.md b/docs/mocked_backend/README.md index 2a126db5116..2369ee50aff 100644 --- a/docs/mocked_backend/README.md +++ b/docs/mocked_backend/README.md @@ -23,7 +23,7 @@ One single way to observe what is sent to the Datadog backend, whatever the setu * **HTTPS on many hostnames**: `mocked-backend.invalid` and the intake hostnames derived from it (`trace.agent.`, `config.`, `instrumentation-telemetry-intake.`, `debugger-intake.`...) are network aliases of its container. Its certificate is generated when it starts, for all of them, signed by the mitmproxy CA (`utils/proxy/.mitmproxy`) the senders trust. * **Remote config**: it serves the remote config backend by default (no config until a test sends one). Tests send configs with `remote_config.send_state`, which sets them on the mocked backend, then waits for the library to apply them. See [Remote config](../edit/remote-config.md). * **Controlled responses**: `MockedBackendResponse` overrides the answer on a path, for the whole session (`MockedBackendContainer.internal_mocked_responses`) or at runtime (`client.set_mocked_responses`), like the proxy's mocked responses. -* **Feature flagging**: endpoints serving the agentless FFE configuration, with scripted responses (`client.ffe_set_response(s)`) and counters (`client.ffe_get_status`). +* **Feature flagging**: endpoints serving the agentless FFE configuration, with scripted responses (`client.ffe_set_response(s)`) and counters (`client.ffe_get_status`). The JSON configuration applies flag selection (`selected_flag_tags`, `selected_flag_key_patterns`) with the semantics of `ffe-system-test-data` `flag-selection/README.md`. `client.ffe_use_suite(name)` serves an `ffe-system-test-data` evaluation suite's UFC instead, matching tags from its `flag-tags.json`; `client.ffe_set_flag_selection_ignored_reason(...)` makes it ignore every valid, non-empty selection, as when production cannot load tags. * **Control endpoints**: under `/system-tests/`, never recorded, used by `MockedBackendClient` from the test runner. ## Using it in a scenario diff --git a/docs/understand/scenarios/parametric.md b/docs/understand/scenarios/parametric.md index 5839c4e13e9..93a7bffdb4b 100644 --- a/docs/understand/scenarios/parametric.md +++ b/docs/understand/scenarios/parametric.md @@ -226,7 +226,7 @@ Options use the Node helper's names (`distributionChannel`, `apiKey`, `clientTok The shared `mocked_backend` fixture serves these protobuf responses. Import shared FFE constants and fixture helpers from `utils.mocked_backend.ffe`. On the server distribution endpoint, explicitly accepting `application/protobuf` selects protobuf, including in comma-separated `Accept` lists with a positive quality value. Wildcard-only requests and existing agentless provider tests continue to receive JSON. -Use `mocked_backend.client.ffe_set_response` or `mocked_backend.client.ffe_set_responses` with `response_format="protobuf"` to configure these responses; the default format is JSON. Each format has an independent response timeline, and controls reject response IDs unsupported by the selected format. `mocked_backend.client.ffe_reset` resets both timelines and the shared request history. Use `mocked_backend.ffe_get_requests` to inspect paths, queries, and headers without including fixture control traffic. +Use `mocked_backend.client.ffe_set_response` or `mocked_backend.client.ffe_set_responses` with `response_format="protobuf"` to configure these responses; the default format is JSON. Each format has an independent response timeline, and controls reject response IDs unsupported by the selected format. `mocked_backend.client.ffe_reset` resets both timelines and the shared request history, serves the root UFC again instead of an `ffe_use_suite` suite, and clears `ffe_set_flag_selection_ignored_reason`. Use `mocked_backend.ffe_get_requests` to inspect paths, queries, and headers without including fixture control traffic. The protobuf response IDs `selection_ignored_empty`, `selection_ignored_tag_load_failed`, and `selection_ignored_unknown` return the full rules fixture with a `DD-Flag-Selection-Ignored` header set to an empty string, `tag_load_failed`, or `future_reason`, respectively. Use these shared responses to test selection diagnostics without replacing the rules endpoint handler. The `valid` response omits the header. diff --git a/manifests/cpp.yml b/manifests/cpp.yml index bc2ba4b839d..a9a59da329b 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -190,6 +190,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: missing_feature tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: irrelevant (cpp is not in scope for v1 agentless server SDK configuration-source coverage) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 5a1cc7af3a1..a2fd8b63787 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -1267,6 +1267,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/golang.yml b/manifests/golang.yml index 81b952fa63c..b71b91cfb31 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -2290,6 +2290,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: v2.13.0-dev # Easy win for all weblogs and version 2.12.0 tests/parametric/test_ffe/test_exposure_hooks.py::Test_FFE_ExposureHooks::test_evaluation_feature_aggregation: missing_feature (Test agent needs EVP flagevaluation capture) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: v2.10.1 tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata: diff --git a/manifests/java.yml b/manifests/java.yml index 32e8421e217..f39e599532e 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -4281,6 +4281,7 @@ manifest: tests/parametric/test_ffe/test_exposure_hooks.py: v1.68.0-SNAPSHOT tests/parametric/test_ffe/test_exposure_hooks.py::Test_FFE_ExposureHooks::test_evaluation_feature_aggregation: missing_feature (Test agent needs EVP flagevaluation capture) tests/parametric/test_ffe/test_exposure_hooks.py::Test_FFE_ExposureHooks::test_multiprovider_exposure_guard: irrelevant (Go exposure hook composition guard) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: v1.66.0 tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 5a81d06c711..15694b78869 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -2706,6 +2706,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_dependent_flags: missing_feature (FFL-1694) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: 7.0.0-pre # Exact development version until the first containing release is confirmed. tests/parametric/test_ffe/test_rules_configuration.py::Test_FFE_Rules_Configuration_Selection: missing_feature (FFL-3364 requires the Node flag-selection dependency update in dd-trace-js) tests/parametric/test_ffe/test_serial_id_metadata.py: v6.18.0 diff --git a/manifests/php.yml b/manifests/php.yml index de19d5146aa..19680c2b6de 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -1660,6 +1660,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_dependent_flags: missing_feature (FFL-1694) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/python.yml b/manifests/python.yml index f4d48b2d598..b7c50e1b6bf 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -3161,6 +3161,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_dependent_flags: missing_feature (FFL-1694) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/ruby.yml b/manifests/ruby.yml index e7111d5e851..ab8483e3784 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -2730,6 +2730,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_dependent_flags: missing_feature (FFL-1694) tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: missing_feature (The flag selection provider option is not implemented) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/manifests/rust.yml b/manifests/rust.yml index 8de4581130c..6896f548de9 100644 --- a/manifests/rust.yml +++ b/manifests/rust.yml @@ -304,6 +304,7 @@ manifest: tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Split_Features: incomplete_test_app (Split-feature hook capture is currently implemented only in the Java parametric client) tests/parametric/test_ffe/test_exposure_hooks.py: incomplete_test_app (Exposure hook observations are not implemented) + tests/parametric/test_ffe/test_flag_selection.py: irrelevant (rust is not in scope for v1 agentless server SDK configuration-source coverage) tests/parametric/test_ffe/test_rules_configuration.py: missing_feature (Standalone rules configuration helpers are currently tested only in Node.js) tests/parametric/test_ffe/test_serial_id_metadata.py: missing_feature tests/parametric/test_ffe/test_serial_id_metadata.py::Test_FFE_Serial_Id_Metadata_Go: irrelevant (Legacy Go metadata key) diff --git a/tests/parametric/test_ffe/ffe-system-test-data/SOURCE.md b/tests/parametric/test_ffe/ffe-system-test-data/SOURCE.md index a957d048cf0..1282438e2dd 100644 --- a/tests/parametric/test_ffe/ffe-system-test-data/SOURCE.md +++ b/tests/parametric/test_ffe/ffe-system-test-data/SOURCE.md @@ -3,7 +3,7 @@ These files are copied from the canonical FFE fixture repository. Canonical source: https://github.com/DataDog/ffe-system-test-data -Source commit: c8cc8da239341748dc8c30c2145c0f0b5432c958 +Source commit: 64572223965cfafa8428431f61e5ed4678c1a1f0 Do not edit these fixtures directly in system-tests. Add or update shared FFE behavior in ffe-system-test-data first, then refresh this snapshot. diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/canonical-form.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/canonical-form.json new file mode 100644 index 00000000000..5ee087eed16 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/canonical-form.json @@ -0,0 +1,148 @@ +[ + { + "description": "Selected by tag team:checkout.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Selected by tag team:checkout.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Selected by tag team:search.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Selected by key pattern ops-*.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [" team:search ", "team:checkout", "team:search", ""], + "keyPatterns": ["\tops-*", "ops-*", "billing-*", ""] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout", "team:search"], + "selectedFlagKeyPatterns": ["billing-*", "ops-*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/empty-selection.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/empty-selection.json new file mode 100644 index 00000000000..ecbef12eb7b --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/empty-selection.json @@ -0,0 +1,140 @@ +[ + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The selection is empty, so the SDK sends none and every flag is served.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [], + "keyPatterns": [] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/interior-whitespace-tag.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/interior-whitespace-tag.json new file mode 100644 index 00000000000..4abf3f666ca --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/interior-whitespace-tag.json @@ -0,0 +1,145 @@ +[ + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Selected by tag release:summer sale.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["release:summer sale"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["release:summer sale"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/key-patterns.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/key-patterns.json new file mode 100644 index 00000000000..deeffdfe28e --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/key-patterns.json @@ -0,0 +1,144 @@ +[ + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Selected by key pattern search-*.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Selected by key pattern *-sale.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "keyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [], + "selectedFlagKeyPatterns": ["*-sale", "search-*", "untagged"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-long-url.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-long-url.json new file mode 100644 index 00000000000..e80b90c8fd3 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-long-url.json @@ -0,0 +1,46 @@ +[ + { + "description": "Percent-encoded, the selection takes the request URL over 8192 bytes, so the SDK sends none and every flag is served.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "long-01:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-02:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-03:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-04:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-05:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-06:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-07:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-08:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-09:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-10:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-11:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-12:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-13:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-14:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-15:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-16:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-17:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-18:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-19:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-20:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-21:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-22:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-23:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-24:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-25:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-26:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-27:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-28:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-29:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-30:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Percent-encoded, the selection takes the request URL over 8192 bytes, so the SDK sends none and every flag is served.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "long-01:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-02:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-03:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-04:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-05:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-06:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-07:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-08:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-09:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-10:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-11:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-12:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-13:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-14:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-15:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-16:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-17:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-18:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-19:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-20:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-21:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-22:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-23:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-24:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-25:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-26:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-27:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-28:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-29:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "long-30:ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-too-many-values.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-too-many-values.json new file mode 100644 index 00000000000..90399c3f7a9 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-drops-too-many-values.json @@ -0,0 +1,134 @@ +[ + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "32 tags exceed the 31-value limit, so the SDK sends no selection and every flag is served.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout", "tag-01", "tag-02", "tag-03", "tag-04", "tag-05", "tag-06", "tag-07", "tag-08", "tag-09", "tag-10", "tag-11", "tag-12", "tag-13", "tag-14", "tag-15", "tag-16", "tag-17", "tag-18", "tag-19", "tag-20", "tag-21", "tag-22", "tag-23", "tag-24", "tag-25", "tag-26", "tag-27", "tag-28", "tag-29", "tag-30", "tag-31"] + } + }, + "expectations": { + "configurationRequest": { + "flagSelection": null + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-sends-selection-at-limits.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-sends-selection-at-limits.json new file mode 100644 index 00000000000..3e398065967 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/sdk-sends-selection-at-limits.json @@ -0,0 +1,569 @@ +[ + { + "description": "Selected: 31 values, 200-byte values and reserved URL characters are within the limits, and the percent-encoded request URL is under 8192 bytes, so the SDK sends the selection.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Selected: 31 values, 200-byte values and reserved URL characters are within the limits, and the percent-encoded request URL is under 8192 bytes, so the SDK sends the selection.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Selected: 31 values, 200-byte values and reserved URL characters are within the limits, and the percent-encoded request URL is under 8192 bytes, so the SDK sends the selection.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:checkout", + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:r&d+qa#1%/?=" + ], + "keyPatterns": [ + "ops-*", + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "limit-01-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-02-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-04-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-05-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-06-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-07-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-08-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-09-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-10-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-11-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-12-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-13-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-14-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-15-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-16-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-17-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-18-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-19-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-20-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-21-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-22-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-23-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-24-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-25-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-26-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-27-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-28-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "limit-29-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "team:checkout", + "team:r&d+qa#1%/?=" + ], + "selectedFlagKeyPatterns": [ + "limit-pattern-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ops-*" + ] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/selection-matches-no-flag.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/selection-matches-no-flag.json new file mode 100644 index 00000000000..9937c3ced77 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/selection-matches-no-flag.json @@ -0,0 +1,170 @@ +[ + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: no flag has the tag team:nobody, so the server applies the selection, returns an empty flags object and omits every flag.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": [ + "team:nobody" + ] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": [ + "team:nobody" + ], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/server-ignores-selection.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/server-ignores-selection.json new file mode 100644 index 00000000000..e800ffdab2f --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/server-ignores-selection.json @@ -0,0 +1,158 @@ +[ + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "The server ignores the selection with tag_load_failed, so every flag is served.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "configurationResponse": { + "flagSelectionIgnored": "tag_load_failed" + }, + "result": { + "value": true, + "reason": "STATIC" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags-or-key-patterns.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags-or-key-patterns.json new file mode 100644 index 00000000000..1af0831fa40 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags-or-key-patterns.json @@ -0,0 +1,150 @@ +[ + { + "description": "Selected by key pattern checkout-b*.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Selected by tag team:ops.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:ops"], + "keyPatterns": ["checkout-b*"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:ops"], + "selectedFlagKeyPatterns": ["checkout-b*"] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags.json new file mode 100644 index 00000000000..f0d7f81b4ea --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/cases/tags.json @@ -0,0 +1,144 @@ +[ + { + "description": "Selected by tag team:checkout.", + "flag": "checkout-banner", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Selected by tag team:checkout.", + "flag": "checkout-pricing", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": true, + "reason": "STATIC" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "search-ranking", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "summer-sale", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "ops-kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + }, + { + "description": "Not selected: it has no selected tag and matches no key pattern, so the server omits it.", + "flag": "untagged-flag", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": {}, + "providerOptions": { + "flagSelection": { + "tags": ["team:checkout"] + } + }, + "expectations": { + "configurationRequest": { + "selectedFlagTags": ["team:checkout"], + "selectedFlagKeyPatterns": [] + } + }, + "result": { + "value": false, + "reason": "ERROR", + "errorCode": "FLAG_NOT_FOUND" + } + } +] diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/flag-tags.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/flag-tags.json new file mode 100644 index 00000000000..fb041769c93 --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/flag-tags.json @@ -0,0 +1,10 @@ +{ + "flags": [ + {"key": "checkout-banner", "tags": ["team:checkout"]}, + {"key": "checkout-pricing", "tags": ["team:checkout"]}, + {"key": "search-ranking", "tags": ["team:search"]}, + {"key": "summer-sale", "tags": ["release:summer sale"]}, + {"key": "ops-kill-switch", "tags": ["team:ops"]}, + {"key": "untagged-flag", "tags": []} + ] +} diff --git a/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/ufc-config.json b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/ufc-config.json new file mode 100644 index 00000000000..22c5b15452d --- /dev/null +++ b/tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites/flag-selection/ufc-config.json @@ -0,0 +1,177 @@ +{ + "createdAt": "2026-11-03T00:00:00.000Z", + "format": "SERVER", + "environment": { + "name": "Test" + }, + "flags": { + "checkout-banner": { + "key": "checkout-banner", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "checkout-pricing": { + "key": "checkout-pricing", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "search-ranking": { + "key": "search-ranking", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "summer-sale": { + "key": "summer-sale", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "ops-kill-switch": { + "key": "ops-kill-switch", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "untagged-flag": { + "key": "untagged-flag", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "everyone", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": false + } + ] + } + } +} diff --git a/tests/parametric/test_ffe/test_flag_selection.py b/tests/parametric/test_ffe/test_flag_selection.py new file mode 100644 index 00000000000..18c69eeeea5 --- /dev/null +++ b/tests/parametric/test_ffe/test_flag_selection.py @@ -0,0 +1,191 @@ +"""Server SDK flag selection against the mocked FFE agentless backend. + +Feature under test: a server SDK given a flag selection sends it on the agentless configuration request and serves +only the flags the backend returns. The cases come from the ffe-system-test-data flag-selection evaluation suite: the +mocked backend serves the suite's ufc-config.json and applies the selection with the suite's flag-tags.json. Each case +file is one provider start. + +The test apps must pass the selection to the SDK as written, through ffe_start's flag_selection field, so the +canonical selection on the request is the SDK's own work. The Node.js test app builds its provider at boot, so it must +take the same raw selection from SYSTEM_TESTS_FFE_FLAG_SELECTION instead. +""" + +from dataclasses import dataclass +import json +from pathlib import Path +import time +from typing import Any +from urllib.parse import parse_qs, urlsplit + +from utils import pytest + +from tests.parametric.conftest import APMLibrary +from utils import context, features, scenarios +from utils.mocked_backend import MockedBackendServer +from utils.mocked_backend.ffe import ( + EXPECTED_DD_ENV, + SELECTED_FLAG_KEY_PATTERNS, + SELECTED_FLAG_TAGS, + MockedBackendFFERequest, + Selection, + ufc_etag_for, +) + +SUITE = "flag-selection" +SUITE_CASES_DIR = Path(__file__).parent / "ffe-system-test-data" / "evaluation-suites" / SUITE / "cases" +REVALIDATION_SELECTION = {"tags": ["team:checkout"]} + + +@dataclass(frozen=True) +class _CaseFile: + """One provider start: every case in the file shares its selection, request and server response.""" + + name: str + cases: list[dict[str, Any]] + + @property + def flag_selection(self) -> object: + return self.cases[0].get("providerOptions", {}).get("flagSelection") + + @property + def flag_selection_ignored(self) -> str | None: + return self.cases[0].get("configurationResponse", {}).get("flagSelectionIgnored") + + @property + def expected_query(self) -> dict[str, list[str]]: + request = self.cases[0]["expectations"]["configurationRequest"] + query = {"dd_env": [EXPECTED_DD_ENV]} + for parameter, field in ( + (SELECTED_FLAG_TAGS, "selectedFlagTags"), + (SELECTED_FLAG_KEY_PATTERNS, "selectedFlagKeyPatterns"), + ): + if request.get(field): + query[parameter] = request[field] + return query + + +def _load_case_files() -> list[_CaseFile]: + paths = sorted(SUITE_CASES_DIR.glob("*.json")) + if not paths: + raise FileNotFoundError(f"No flag selection suite cases found in {SUITE_CASES_DIR}") + return [_CaseFile(path.stem, json.loads(path.read_text(encoding="utf-8"))) for path in paths] + + +CASE_FILES = _load_case_files() + + +def _applies_to(case: dict[str, Any], platform: str) -> bool: + skipped = {entry["platform"] for entry in case.get("_skip", [])} + included = {entry["platform"] for entry in case.get("_include", [])} + return platform not in skipped and (not included or platform in included) + + +def _agentless_env(mocked_backend: MockedBackendServer, flag_selection: object) -> dict[str, str]: + env = { + "DD_API_KEY": scenarios.parametric.dd_api_key, + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "false", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": "agentless", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": mocked_backend.ffe_library_config_url, + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": "1", + } + if flag_selection is not None: + env["SYSTEM_TESTS_FFE_FLAG_SELECTION"] = json.dumps(flag_selection) + return env + + +def _config_requests(mocked_backend: MockedBackendServer, count: int) -> list[MockedBackendFFERequest]: + deadline = time.monotonic() + 5 + while len(requests := mocked_backend.ffe_get_requests()) < count and time.monotonic() < deadline: + time.sleep(0.2) + assert len(requests) >= count, f"the SDK made {len(requests)} configuration requests, expected {count}" + return requests + + +def _query(request: MockedBackendFFERequest) -> dict[str, list[str]]: + return parse_qs(urlsplit(request["path"]).query, keep_blank_values=True) + + +def _is_waiting_for_config(result: dict[str, Any]) -> bool: + provider_state = result.get("providerState") + return result.get("errorCode") == "PROVIDER_NOT_READY" or ( + isinstance(provider_state, dict) and provider_state.get("hasConfig") is False + ) + + +def _evaluate(test_library: APMLibrary, case: dict[str, Any]) -> dict[str, Any]: + for _ in range(10): + result = test_library.ffe_evaluate( + flag=case["flag"], + variation_type=case["variationType"], + default_value=case["defaultValue"], + targeting_key=case["targetingKey"], + attributes=case["attributes"], + ) + if not _is_waiting_for_config(result): + break + time.sleep(0.2) + return result + + +@scenarios.parametric +@features.feature_flags_configuration_filtering +class Test_FFE_Flag_Selection: + @pytest.fixture + def library_env(self, mocked_backend: MockedBackendServer, case_file: _CaseFile) -> dict[str, str]: + # Some SDKs fetch their configuration as soon as the library starts. + mocked_backend.client.ffe_use_suite(SUITE) + if case_file.flag_selection_ignored is not None: + mocked_backend.client.ffe_set_flag_selection_ignored_reason(case_file.flag_selection_ignored) + return _agentless_env(mocked_backend, case_file.flag_selection) + + @pytest.mark.parametrize("case_file", CASE_FILES, ids=[case_file.name for case_file in CASE_FILES]) + def test_flag_selection_suite( + self, case_file: _CaseFile, test_library: APMLibrary, mocked_backend: MockedBackendServer + ) -> None: + cases = [case for case in case_file.cases if _applies_to(case, context.library.name)] + if not cases: + pytest.skip(f"No cases in {case_file.name} apply to {context.library.name}") + + assert test_library.ffe_start(flag_selection=case_file.flag_selection), "Failed to start FFE provider" + request = _config_requests(mocked_backend, 1)[0] + assert _query(request) == case_file.expected_query, ( + f"{case_file.name}: the configuration request does not carry the expected selection" + ) + + for index, case in enumerate(cases): + expected = case["result"] + result = _evaluate(test_library, case) + location = f"{case_file.name} case {index} ({case['flag']})" + assert not _is_waiting_for_config(result), f"{location}: the provider loaded no configuration: {result}" + assert result.get("value") == expected["value"], f"{location}: {result}" + assert result.get("reason") == expected["reason"], f"{location}: {result}" + if "errorCode" in expected: + assert result.get("errorCode") == expected["errorCode"], f"{location}: {result}" + else: + assert result.get("errorCode") in {None, ""}, f"{location}: {result}" + + +@scenarios.parametric +@features.feature_flags_configuration_filtering +class Test_FFE_Flag_Selection_Revalidation: + @pytest.fixture + def library_env(self, mocked_backend: MockedBackendServer) -> dict[str, str]: + mocked_backend.client.ffe_use_suite(SUITE) + mocked_backend.client.ffe_set_responses(["valid", "not_modified"]) + return _agentless_env(mocked_backend, REVALIDATION_SELECTION) + + def test_selected_configuration_revalidates_with_its_etag( + self, test_library: APMLibrary, mocked_backend: MockedBackendServer + ) -> None: + assert test_library.ffe_start(flag_selection=REVALIDATION_SELECTION), "Failed to start FFE provider" + assert _query(_config_requests(mocked_backend, 1)[0]) == { + "dd_env": [EXPECTED_DD_ENV], + SELECTED_FLAG_TAGS: ["team:checkout"], + }, "the configuration request does not carry the selection" + + first, second = _config_requests(mocked_backend, 2)[:2] + # The mock answers a scripted 304 to a request without a selection whatever its If-None-Match. + assert _query(second) == _query(first), "the SDK dropped its selection on the revalidation poll" + assert second["headers"].get("if-none-match") == ufc_etag_for( + Selection(tags=("team:checkout",), key_patterns=()), SUITE + ), "the SDK did not revalidate with the ETag of its selection" diff --git a/tests/test_the_test/test_mock_backend_v2.py b/tests/test_the_test/test_mock_backend_v2.py index 61189455064..98c4c1cc3e9 100644 --- a/tests/test_the_test/test_mock_backend_v2.py +++ b/tests/test_the_test/test_mock_backend_v2.py @@ -24,7 +24,7 @@ from utils._context._scenarios.agentless_endtoend import JAVA_TRUSTSTORE_SOURCE from utils.mocked_backend import MockedBackendClient, MockedBackendServer from utils.mocked_backend._tls import generate_certificate -from utils.mocked_backend.ffe import UFC_FIXTURE_RELATIVE_PATH +from utils.mocked_backend.ffe import EVALUATION_SUITES_RELATIVE_PATH, UFC_FIXTURE_RELATIVE_PATH from utils.proxy.mocked_response import MockedBackendResponse @@ -323,14 +323,15 @@ def test_control_requests_are_not_recorded(tmp_path: Path): @scenarios.test_the_test def test_runs_without_the_rest_of_utils(tmp_path: Path): """The mocked backend must work with only what its container mounts (utils/mocked_backend, utils/proxy and the - FFE fixture, see MockedBackendContainer), and without pytest. + FFE fixtures, see MockedBackendContainer), and without pytest. """ repo_root = Path(__file__).parents[2] (tmp_path / "utils").mkdir() # no __init__.py: a namespace package, as in the proxy image for package in ("mocked_backend", "proxy"): (tmp_path / "utils" / package).symlink_to(repo_root / "utils" / package) - (tmp_path / UFC_FIXTURE_RELATIVE_PATH).parent.mkdir(parents=True) - (tmp_path / UFC_FIXTURE_RELATIVE_PATH).symlink_to(repo_root / UFC_FIXTURE_RELATIVE_PATH) + for fixture in (UFC_FIXTURE_RELATIVE_PATH, EVALUATION_SUITES_RELATIVE_PATH): + (tmp_path / fixture).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / fixture).symlink_to(repo_root / fixture) # Not `python -I`: third-party packages may come from PYTHONPATH (e.g. Nix). Instead, drop the repository # from sys.path, otherwise the real utils package would be imported, and this test would always pass. @@ -346,11 +347,18 @@ def test_runs_without_the_rest_of_utils(tmp_path: Path): "from utils.mocked_backend.ffe import CONFIG_PATH\n" "from utils.mocked_backend.ffe import ffe_library_config_url\n" "import utils.mocked_backend.__main__\n" + "import json\n" "from urllib.request import urlopen\n" "server = MockedBackendServer(port=0)\n" "try:\n" " with urlopen(ffe_library_config_url(server.base_url), timeout=5) as response:\n" " assert response.status == 200, response.status\n" + " server.client.ffe_use_suite('flag-selection')\n" + " selection_url = ffe_library_config_url(server.base_url) + '&selected_flag_tags=team%3Acheckout'\n" + " with urlopen(selection_url, timeout=5) as response:\n" + " assert response.status == 200, response.status\n" + " assert sorted(json.load(response)['data']['attributes']['flags']) == " + "['checkout-banner', 'checkout-pricing']\n" "finally:\n" " server.close()\n" ) diff --git a/tests/test_the_test/test_mock_ffe_flag_selection.py b/tests/test_the_test/test_mock_ffe_flag_selection.py new file mode 100644 index 00000000000..abe62ee842a --- /dev/null +++ b/tests/test_the_test/test_mock_ffe_flag_selection.py @@ -0,0 +1,425 @@ +"""Unit coverage for flag selection on the mocked FFE agentless configuration endpoint.""" + +from dataclasses import dataclass +import http.client +import json +from pathlib import Path +from urllib.parse import quote, urlsplit + +import requests +from utils import pytest + +from utils import scenarios +from utils.mocked_backend import MockedBackendServer +from utils.mocked_backend._ffe_flag_selection import ( + SelectionIgnoredError, + has_selection_parameters, + parse_selection, + select_flags, +) +from utils.mocked_backend.ffe import ( + CONFIG_PATH, + CONFIG_QUERY, + EVALUATION_SUITES_RELATIVE_PATH, + FLAG_SELECTION_IGNORED_HEADER, + UFC_FIXTURE_RELATIVE_PATH, + Selection, + ufc_etag_for, +) + +REPO_ROOT = Path(__file__).parents[2] +VECTORS = json.loads( + (REPO_ROOT / "tests/parametric/test_ffe/ffe-system-test-data/flag-selection/selection-vectors.json").read_text() +) +UFC_FLAGS = sorted(json.loads((REPO_ROOT / UFC_FIXTURE_RELATIVE_PATH).read_text())["flags"]) +SUITE = "flag-selection" +SUITE_FLAGS = sorted( + json.loads((REPO_ROOT / EVALUATION_SUITES_RELATIVE_PATH / SUITE / "ufc-config.json").read_text())["flags"] +) +CHECKOUT_FLAGS = ["checkout-banner", "checkout-pricing"] +SUITE_CASE_FILES = sorted((REPO_ROOT / EVALUATION_SUITES_RELATIVE_PATH / SUITE / "cases").glob("*.json")) + + +def _encode(value: str) -> str: + # Escape every reserved character, so '+', '%', '&', '=' and '*' in a value reach the parser literally. + return quote(value, safe="-_.~") + + +def _vector_query(case: dict) -> str: + if "rawQuery" in case: + return case["rawQuery"] + return "&".join(f"{name}={_encode(value)}" for name, values in case["query"].items() for value in values) + + +@scenarios.test_the_test +@pytest.mark.parametrize("case", VECTORS["cases"], ids=[case["name"] for case in VECTORS["cases"]]) +def test_selection_matches_shared_vectors(case: dict) -> None: + flag_tags = {flag["key"]: flag["tags"] for flag in VECTORS["flags"]} + raw_query = _vector_query(case) + expected = case["expected"] + + if expected["outcome"] == "ignored": + with pytest.raises(SelectionIgnoredError) as error: + parse_selection(raw_query) + assert error.value.reason == expected["reason"] + return + + if not has_selection_parameters(raw_query): + assert expected["selectedKeys"] == sorted(flag_tags) + return + selection = parse_selection(raw_query) + if "selection" in case.get("sdk", {}): + # The server normalizes a selection exactly like the SDK canonicalizes it. + assert selection == Selection( + tags=tuple(case["sdk"]["selection"]["tags"]), + key_patterns=tuple(case["sdk"]["selection"]["keyPatterns"]), + ) + assert select_flags(selection, flag_tags) == expected["selectedKeys"] + + +@dataclass +class _Response: + status: int + headers: http.client.HTTPMessage + body: bytes + + def json(self) -> dict: + return json.loads(self.body) + + +def _get(server: MockedBackendServer, query: str, headers: dict[str, str] | None = None) -> _Response: + # Not requests: it re-encodes invalid percent-escapes such as %zz, which the malformed cases must send as is. + connection = http.client.HTTPConnection(urlsplit(server.base_url).netloc, timeout=5) + try: + connection.request("GET", f"{CONFIG_PATH}?{query}", headers=headers or {}) + response = connection.getresponse() + return _Response(response.status, response.headers, response.read()) + finally: + connection.close() + + +def _get_config(server: MockedBackendServer, selection_query: str = "") -> _Response: + response = _get(server, f"{CONFIG_QUERY}&{selection_query}" if selection_query else CONFIG_QUERY) + assert response.status == 200, response.body + return response + + +def _flag_keys(response: _Response) -> list[str]: + return sorted(response.json()["data"]["attributes"]["flags"]) + + +@scenarios.test_the_test +@pytest.mark.parametrize( + ("selection_query", "expected_keys"), + [ + ("selected_flag_tags=team%3Acheckout", CHECKOUT_FLAGS), + ("selected_flag_tags=release%3Asummer%20sale", ["summer-sale"]), + ("selected_flag_tags=release:summer+sale", ["summer-sale"]), + ("selected_flag_key_patterns=search-%2A", ["search-ranking"]), + ( + "selected_flag_tags=team%3Aops&selected_flag_key_patterns=checkout-b%2A", + ["checkout-banner", "ops-kill-switch"], + ), + ("selected_flag_tags=no-flag-has-this-tag", []), + ("selected_flag_tags=%85", []), + ("selected%5Fflag%5Ftags=team%3Acheckout", CHECKOUT_FLAGS), + ], +) +def test_valid_selection_serves_only_selected_flags(selection_query: str, expected_keys: list[str]) -> None: + server = MockedBackendServer() + try: + server.client.ffe_use_suite(SUITE) + unfiltered = _get_config(server) + response = _get_config(server, selection_query) + + assert _flag_keys(response) == expected_keys + assert FLAG_SELECTION_IGNORED_HEADER not in response.headers + assert response.headers["ETag"] != unfiltered.headers["ETag"] + assert ( + response.json()["data"]["attributes"]["environment"] + == unfiltered.json()["data"]["attributes"]["environment"] + ) + assert server.ffe_get_requests()[-1]["path"].endswith(selection_query) + finally: + server.close() + + +@scenarios.test_the_test +def test_root_configuration_has_no_tags() -> None: + server = MockedBackendServer() + try: + assert _flag_keys(_get_config(server, "selected_flag_tags=team%3Acheckout")) == [] + assert _flag_keys(_get_config(server, "selected_flag_key_patterns=serial-id-%2A")) == [ + "serial-id-basic-flag", + "serial-id-missing-flag", + "serial-id-targeting-flag", + "serial-id-zero-flag", + ] + finally: + server.close() + + +@scenarios.test_the_test +def test_suite_control_serves_the_suite_configuration() -> None: + server = MockedBackendServer() + try: + server.client.ffe_use_suite(SUITE) + response = _get_config(server) + assert _flag_keys(response) == SUITE_FLAGS + assert response.headers["ETag"] == ufc_etag_for(None, SUITE) + + server.client.ffe_use_suite("dependent-flags") + assert _flag_keys(_get_config(server, "selected_flag_tags=team%3Acheckout")) == [] + + server.client.ffe_use_suite(None) + response = _get_config(server) + assert _flag_keys(response) == UFC_FLAGS + assert response.headers["ETag"] == ufc_etag_for(None) + + server.client.ffe_use_suite(SUITE) + server.client.ffe_reset() + assert _flag_keys(_get_config(server)) == UFC_FLAGS + finally: + server.close() + + +@scenarios.test_the_test +def test_suite_control_rejects_unknown_suites() -> None: + server = MockedBackendServer() + try: + for body in ( + {"suite": "missing-suite"}, + {"suite": "../evaluation-suites/flag-selection"}, + {"suite": "Flag-Selection"}, + {"suite": ""}, + {"suite": 1}, + {}, + {"other": None}, + ): + response = requests.post(f"{server.base_url}/system-tests/ffe/suite", json=body, timeout=5) + assert response.status_code == 400, body + finally: + server.close() + + +@scenarios.test_the_test +@pytest.mark.parametrize( + ("selection_query", "reason"), + [ + ("selected_flag_tags=%zz", "malformed_selection"), + ("&".join(f"selected_flag_tags=tag-{index}" for index in range(32)), "too_many_values"), + ("selected_flag_key_patterns=" + "a" * 201, "invalid_key_pattern"), + ("selected_flag_tags=" + "a" * 201 + "&selected_flag_tags=team%3Acheckout", "invalid_tag"), + ], +) +def test_invalid_selection_serves_every_flag_with_reason(selection_query: str, reason: str) -> None: + server = MockedBackendServer() + try: + unfiltered = _get_config(server) + response = _get_config(server, selection_query) + + assert _flag_keys(response) == UFC_FLAGS + assert response.headers[FLAG_SELECTION_IGNORED_HEADER] == reason + assert response.headers["ETag"] == unfiltered.headers["ETag"] + finally: + server.close() + + +@scenarios.test_the_test +def test_absent_or_empty_selection_serves_every_flag_without_reason() -> None: + server = MockedBackendServer() + try: + for selection_query in ("", "selected_flag_tags=%20&selected_flag_key_patterns="): + response = _get_config(server, selection_query) + assert _flag_keys(response) == UFC_FLAGS, selection_query + assert FLAG_SELECTION_IGNORED_HEADER not in response.headers, selection_query + finally: + server.close() + + +@scenarios.test_the_test +def test_selection_does_not_relax_the_environment_query() -> None: + server = MockedBackendServer() + try: + for query in ( + "selected_flag_tags=team%3Acheckout", + "dd_env=test&dd_env=wrong&selected_flag_tags=team%3Acheckout", + "dd_env=test&other=1&selected_flag_tags=team%3Acheckout", + "dd_env=test&selected_flag_bogus=1", + ): + assert _get(server, query).status == 404, query + finally: + server.close() + + +@scenarios.test_the_test +def test_forced_ignored_reason_applies_only_to_valid_selections() -> None: + server = MockedBackendServer() + try: + server.client.ffe_use_suite(SUITE) + server.client.ffe_set_flag_selection_ignored_reason("tag_load_failed") + + response = _get_config(server, "selected_flag_tags=team%3Acheckout") + assert _flag_keys(response) == SUITE_FLAGS + assert response.headers[FLAG_SELECTION_IGNORED_HEADER] == "tag_load_failed" + + for selection_query in ("", "selected_flag_tags=%20&selected_flag_key_patterns="): + response = _get_config(server, selection_query) + assert _flag_keys(response) == SUITE_FLAGS, selection_query + assert FLAG_SELECTION_IGNORED_HEADER not in response.headers, selection_query + + response = _get_config(server, "selected_flag_tags=%zz") + assert response.headers[FLAG_SELECTION_IGNORED_HEADER] == "malformed_selection" + + server.client.ffe_set_flag_selection_ignored_reason(None) + response = _get_config(server, "selected_flag_tags=team%3Acheckout") + assert _flag_keys(response) == CHECKOUT_FLAGS + + server.client.ffe_set_flag_selection_ignored_reason("future_reason") + server.client.ffe_reset() + response = _get_config(server, "selected_flag_tags=team%3Acheckout") + assert FLAG_SELECTION_IGNORED_HEADER not in response.headers + finally: + server.close() + + +@scenarios.test_the_test +def test_forced_ignored_reason_control_rejects_invalid_bodies() -> None: + server = MockedBackendServer() + try: + for body in ({}, {"ignored_reason": ""}, {"ignored_reason": "a b"}, {"ignored_reason": 1}, {"other": None}): + response = requests.post(f"{server.base_url}/system-tests/ffe/flag-selection", json=body, timeout=5) + assert response.status_code == 400, body + finally: + server.close() + + +@scenarios.test_the_test +def test_selection_is_not_applied_to_error_responses() -> None: + server = MockedBackendServer() + try: + server.client.ffe_set_responses(["server_error", "not_modified"]) + for expected_status in (500, 304): + response = _get(server, f"{CONFIG_QUERY}&selected_flag_tags=%zz", {"If-None-Match": ufc_etag_for(None)}) + assert response.status == expected_status + assert FLAG_SELECTION_IGNORED_HEADER not in response.headers + finally: + server.close() + + +@scenarios.test_the_test +def test_not_modified_under_a_selection_revalidates_only_its_own_etag() -> None: + server = MockedBackendServer() + selection_query = f"{CONFIG_QUERY}&selected_flag_tags=team%3Acheckout" + try: + server.client.ffe_use_suite(SUITE) + server.client.ffe_set_responses(["valid", "not_modified"]) + etag = _get(server, selection_query).headers["ETag"] + assert etag == ufc_etag_for(Selection(tags=("team:checkout",), key_patterns=()), SUITE) + assert etag not in { + ufc_etag_for(None, SUITE), + ufc_etag_for(Selection(tags=("team:checkout",), key_patterns=())), + } + + for validator in (etag, f"W/{etag}", f'"other", {etag}'): + revalidated = _get(server, selection_query, {"If-None-Match": validator}) + assert revalidated.status == 304, validator + assert revalidated.headers["ETag"] == etag, validator + assert FLAG_SELECTION_IGNORED_HEADER not in revalidated.headers, validator + + for headers in ({"If-None-Match": ufc_etag_for(None, SUITE)}, {}): + stale = _get(server, selection_query, headers) + assert stale.status == 200, headers + assert stale.headers["ETag"] == etag, headers + assert _flag_keys(stale) == CHECKOUT_FLAGS, headers + finally: + server.close() + + +@scenarios.test_the_test +def test_not_modified_does_not_confirm_a_selected_body_once_the_selection_is_ignored() -> None: + server = MockedBackendServer() + selection_query = f"{CONFIG_QUERY}&selected_flag_tags=team%3Acheckout" + try: + server.client.ffe_use_suite(SUITE) + server.client.ffe_set_responses(["valid", "not_modified"]) + selected_etag = _get(server, selection_query).headers["ETag"] + server.client.ffe_set_flag_selection_ignored_reason("tag_load_failed") + + response = _get(server, selection_query, {"If-None-Match": selected_etag}) + assert response.status == 200 + assert _flag_keys(response) == SUITE_FLAGS + assert response.headers[FLAG_SELECTION_IGNORED_HEADER] == "tag_load_failed" + assert response.headers["ETag"] == ufc_etag_for(None, SUITE) + + revalidated = _get(server, selection_query, {"If-None-Match": ufc_etag_for(None, SUITE)}) + assert revalidated.status == 304 + assert FLAG_SELECTION_IGNORED_HEADER not in revalidated.headers + finally: + server.close() + + +@scenarios.test_the_test +def test_not_modified_without_a_selection_keeps_the_plain_etag() -> None: + server = MockedBackendServer() + try: + server.client.ffe_set_response("not_modified") + for headers in ({"If-None-Match": ufc_etag_for(None)}, {}): + response = _get(server, CONFIG_QUERY, headers) + assert response.status == 304, headers + assert response.headers["ETag"] == ufc_etag_for(None), headers + finally: + server.close() + + +@scenarios.test_the_test +def test_each_selection_and_suite_has_its_own_etag() -> None: + server = MockedBackendServer() + queries = ( + "selected_flag_tags=team%3Acheckout", + "selected_flag_tags=team%3Aops", + "selected_flag_key_patterns=team%3Acheckout", + "selected_flag_key_patterns=search-%2A", + "selected_flag_tags=team%3Acheckout&selected_flag_key_patterns=search-%2A", + ) + try: + server.client.ffe_use_suite(SUITE) + etags = [_get_config(server, query).headers["ETag"] for query in ("", *queries)] + assert len(set(etags)) == len(etags), etags + equivalent = "selected_flag_tags=%20team%3Acheckout&selected_flag_tags=team%3Acheckout" + assert _get_config(server, equivalent).headers["ETag"] == etags[1] + + server.client.ffe_use_suite("dependent-flags") + assert _get_config(server, queries[0]).headers["ETag"] not in etags + finally: + server.close() + + +@scenarios.test_the_test +@pytest.mark.parametrize("case_path", SUITE_CASE_FILES, ids=[path.stem for path in SUITE_CASE_FILES]) +def test_suite_case_files_agree_with_the_mock(case_path: Path) -> None: + """For the request a suite case file expects, the mock serves each case's flag unless it expects FLAG_NOT_FOUND.""" + cases = json.loads(case_path.read_text(encoding="utf-8")) + request = cases[0]["expectations"]["configurationRequest"] + ignored_reason = cases[0].get("configurationResponse", {}).get("flagSelectionIgnored") + query = "&".join( + f"{parameter}={_encode(value)}" + for parameter, field in ( + ("selected_flag_tags", "selectedFlagTags"), + ("selected_flag_key_patterns", "selectedFlagKeyPatterns"), + ) + for value in request.get(field, []) + ) + server = MockedBackendServer() + try: + server.client.ffe_use_suite(SUITE) + if ignored_reason is not None: + server.client.ffe_set_flag_selection_ignored_reason(ignored_reason) + response = _get_config(server, query) + + assert response.headers.get(FLAG_SELECTION_IGNORED_HEADER) == ignored_reason + served = set(_flag_keys(response)) + for case in cases: + assert (case["flag"] in served) == (case["result"].get("errorCode") != "FLAG_NOT_FOUND"), case + finally: + server.close() diff --git a/tests/test_the_test/test_update_ffe_fixtures.py b/tests/test_the_test/test_update_ffe_fixtures.py index bca1db6bf6f..73c528e7aa8 100644 --- a/tests/test_the_test/test_update_ffe_fixtures.py +++ b/tests/test_the_test/test_update_ffe_fixtures.py @@ -10,6 +10,20 @@ from utils import scenarios from utils.scripts import update_ffe_fixtures +VALID_VECTORS = json.dumps( + { + "flags": [{"key": "one", "tags": []}], + "cases": [ + { + "name": "case", + "query": {}, + "expected": {"outcome": "applied", "selectedKeys": ["one"]}, + "sdk": {"selection": {"tags": [], "keyPatterns": []}}, + } + ], + } +) + def _write_selection_vectors(root: Path, cases: list[dict[str, Any]] | None = None) -> None: path = root / update_ffe_fixtures.SELECTION_VECTORS_PATH @@ -19,7 +33,10 @@ def _write_selection_vectors(root: Path, cases: list[dict[str, Any]] | None = No {"name": "empty selection", "query": {}, "sdk": {"selection": {"tags": [], "keyPatterns": []}}}, {"name": "raw query", "rawQuery": "selected_flag_tags=%zz"}, ] - path.write_text(json.dumps({"cases": cases}), encoding="utf-8") + for case in cases: + if isinstance(case, dict): + case.setdefault("expected", {"outcome": "applied", "selectedKeys": []}) + path.write_text(json.dumps({"flags": [{"key": "one", "tags": []}], "cases": cases}), encoding="utf-8") @scenarios.test_the_test @@ -106,6 +123,63 @@ def test_copy_fixture_snapshot_rejects_selection_symbolic_links(tmp_path: Path, update_ffe_fixtures._copy_fixture_snapshot(source, snapshot) +@scenarios.test_the_test +def test_validate_selection_vectors_rejects_what_the_mock_tests_cannot_read(tmp_path: Path) -> None: + vectors = tmp_path / update_ffe_fixtures.SELECTION_VECTORS_PATH + vectors.parent.mkdir() + case = {"name": "case", "query": {}, "sdk": {"selection": {"tags": [], "keyPatterns": []}}} + for content, message in ( + ({"cases": [{**case, "expected": {"outcome": "applied"}}]}, "flags must be a non-empty array"), + ( + {"flags": [{"key": "one", "tags": [1]}], "cases": [{**case, "expected": {"outcome": "applied"}}]}, + "flags must be a non-empty array", + ), + ({"flags": [{"key": "one", "tags": []}], "cases": [case]}, "applied or ignored expected outcome"), + ({"flags": [{"key": "one", "tags": []}], "cases": [{**case, "expected": {}}]}, "applied or ignored"), + ): + vectors.write_text(json.dumps(content), encoding="utf-8") + with pytest.raises(ValueError, match=message): + update_ffe_fixtures._validate_selection_vectors(tmp_path) + + +def _write_suite(snapshot: Path) -> Path: + (snapshot / "evaluation-cases").mkdir(parents=True) + (snapshot / "ufc-config.json").write_text('{"flags": {"one": {}}}', encoding="utf-8") + (snapshot / "evaluation-cases" / "case.json").write_text('[{"flag": "one"}]', encoding="utf-8") + suite = snapshot / "evaluation-suites" / "flag-selection" + (suite / "cases").mkdir(parents=True) + (suite / "ufc-config.json").write_text('{"flags": {"two": {}}}', encoding="utf-8") + (suite / "cases" / "tags.json").write_text('[{"flag": "two"}]', encoding="utf-8") + return suite + + +@scenarios.test_the_test +def test_validate_fixture_snapshot_accepts_suite_flag_tags(tmp_path: Path) -> None: + suite = _write_suite(tmp_path) + (suite / "flag-tags.json").write_text('{"flags": [{"key": "two", "tags": ["team:checkout"]}]}', encoding="utf-8") + + assert update_ffe_fixtures._validate_fixture_snapshot(tmp_path) == 2 + + +@scenarios.test_the_test +@pytest.mark.parametrize( + ("name", "content", "message"), + [ + ("flag-tags.json", '{"flags": {}}', "flags must be a non-empty array of key and tags entries"), + ("flag-tags.json", '{"flags": [{"key": "two"}]}', "flags must be a non-empty array of key and tags entries"), + ("flag-tags.json", '{"flags": [], "extra": 1}', "must contain only flags"), + ("notes.json", "{}", "must contain only ufc-config.json, cases and an optional flag-tags.json"), + ], +) +def test_validate_fixture_snapshot_rejects_invalid_suite_entries( + tmp_path: Path, name: str, content: str, message: str +) -> None: + (_write_suite(tmp_path) / name).write_text(content, encoding="utf-8") + + with pytest.raises(ValueError, match=message): + update_ffe_fixtures._validate_fixture_snapshot(tmp_path) + + @scenarios.test_the_test def test_validate_fixture_snapshot_counts_evaluations(tmp_path: Path) -> None: cases_directory = tmp_path / "evaluation-cases" @@ -250,3 +324,31 @@ def fake_git(working_directory: Path, *arguments: str) -> str: source / update_ffe_fixtures.SELECTION_VECTORS_PATH ).read_bytes() assert (destination / "evaluation-cases" / "case.json").read_bytes() == case_file.read_bytes() + + +@scenarios.test_the_test +def test_update_fixtures_vendors_and_checks_selection_vectors(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + source = tmp_path / "upstream" + destination = tmp_path / "destination" + _write_suite(source) + (source / "flag-selection").mkdir() + (source / "flag-selection" / "selection-vectors.json").write_text(VALID_VECTORS, encoding="utf-8") + (source / "flag-selection" / "README.md").write_text("upstream documentation", encoding="utf-8") + destination.mkdir() + + def fake_git(working_directory: Path, *arguments: str) -> str: + if arguments[0] == "checkout": + shutil.copytree(source, working_directory, dirs_exist_ok=True) + return "a" * 40 + + monkeypatch.setattr(update_ffe_fixtures, "_run_git", fake_git) + monkeypatch.setattr(update_ffe_fixtures, "FIXTURE_DESTINATION", destination) + monkeypatch.setenv("GITHUB_OUTPUT", str(tmp_path / "outputs")) + assert update_ffe_fixtures.update_fixtures("main") + assert [path.name for path in (destination / "flag-selection").iterdir()] == ["selection-vectors.json"] + + invalid = json.loads(VALID_VECTORS) | {"flags": []} + (source / "flag-selection" / "selection-vectors.json").write_text(json.dumps(invalid), encoding="utf-8") + with pytest.raises(ValueError, match="flags must be a non-empty array"): + update_ffe_fixtures.update_fixtures("main") + assert (destination / "flag-selection" / "selection-vectors.json").read_text(encoding="utf-8") == VALID_VECTORS diff --git a/utils/_context/containers.py b/utils/_context/containers.py index 5afb87bd780..8a58798ac80 100644 --- a/utils/_context/containers.py +++ b/utils/_context/containers.py @@ -27,7 +27,7 @@ from utils.docker_fixtures._core import extra_hosts_for_environment from utils.proxy.tuf import get_tuf_root_json from utils.mocked_backend import MOCKED_BACKEND_DOMAIN, MockedBackendClient, MockedBackendServer -from utils.mocked_backend.ffe import UFC_FIXTURE_RELATIVE_PATH +from utils.mocked_backend.ffe import EVALUATION_SUITES_RELATIVE_PATH, UFC_FIXTURE_RELATIVE_PATH from utils.proxy.ports import ProxyPorts from utils.proxy.mocked_response import ( RemoveMetaStructsSupport, @@ -766,6 +766,11 @@ def __init__(self, *, tls: bool = False) -> None: "./utils/proxy": {"bind": "/app/utils/proxy", "mode": "ro"}, # served as the FFE agentless configuration f"./{UFC_FIXTURE_RELATIVE_PATH}": {"bind": f"/app/{UFC_FIXTURE_RELATIVE_PATH}", "mode": "ro"}, + # served instead with client.ffe_use_suite, with the tags its flag selection matches against + f"./{EVALUATION_SUITES_RELATIVE_PATH}": { + "bind": f"/app/{EVALUATION_SUITES_RELATIVE_PATH}", + "mode": "ro", + }, }, # for the control endpoints, used from the host. Docker picks a free host port ports={f"{self.port}/tcp": ("127.0.0.1", None)}, diff --git a/utils/_features.py b/utils/_features.py index df80e069672..53701db2da8 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2756,6 +2756,11 @@ def feature_flags_evaluation_telemetry_opt_out(test_object): """ return _mark_test_object(test_object, feature_id=649, owner=_Owner.ffe) + @staticmethod + def feature_flags_configuration_filtering(test_object): + """Feature Flags configuration filtering (flag selection by tags and key patterns)""" + return _mark_test_object(test_object, feature_id=NOT_REPORTED_ID, owner=_Owner.ffe) + @staticmethod def appsec_extended_data_collection(test_object): """AppSec supports extended data collection including headers and body diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index c0dd271e277..d875017bec0 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -822,16 +822,21 @@ def ffe_fetch_rules_configuration(self, options: dict[str, Any]) -> dict[str, An response.raise_for_status() return cast("dict[str, Any]", response.json()) - def ffe_start(self, configuration: dict | None = None) -> bool: + def ffe_start(self, configuration: dict | None = None, *, flag_selection: object = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider. + flag_selection, unless None, is sent as is in the flag_selection field; None omits the field. The test app must + pass it untouched to the SDK's flag selection provider option: only the SDK canonicalizes and validates it. + Returns: bool: True if the provider was initialized successfully, False otherwise """ - payload = {} + payload: dict[str, object] = {} if configuration is not None: payload["configuration"] = configuration + if flag_selection is not None: + payload["flag_selection"] = flag_selection resp = self._session.post(self._url("/ffe/start"), json=payload) return HTTPStatus(resp.status_code).is_success @@ -1307,9 +1312,9 @@ def ffe_fetch_rules_configuration(self, options: dict[str, Any]) -> dict[str, An """Fetch and serialize portable rules without initializing a tracer or provider.""" return self._client.ffe_fetch_rules_configuration(options) - def ffe_start(self, configuration: dict | None = None) -> bool: + def ffe_start(self, configuration: dict | None = None, *, flag_selection: object = None) -> bool: """Initialize the FFE (Feature Flagging & Experimentation) provider.""" - return self._client.ffe_start(configuration) + return self._client.ffe_start(configuration, flag_selection=flag_selection) def ffe_evaluate( self, diff --git a/utils/mocked_backend/_backend_ffe.py b/utils/mocked_backend/_backend_ffe.py index 8f7a6b2a8ae..1e554e5a9c9 100644 --- a/utils/mocked_backend/_backend_ffe.py +++ b/utils/mocked_backend/_backend_ffe.py @@ -5,6 +5,15 @@ default to JSON; use ``response_format="protobuf"`` for an independent protobuf timeline. ``mocked_backend.client.ffe_get_status()`` reports request counts, auth, ETag, response codes, and paths observed across both formats. + +The JSON configuration serves the root UFC, or with ``mocked_backend.client.ffe_use_suite(name)`` +the UFC of that ffe-system-test-data evaluation suite. It applies the ``selected_flag_tags`` and +``selected_flag_key_patterns`` query parameters with the semantics of ffe-system-test-data +``flag-selection/README.md``, matching tags from the suite's ``flag-tags.json``; the root UFC has no tags. +``mocked_backend.client.ffe_set_flag_selection_ignored_reason(...)`` makes it ignore every valid, non-empty +selection with that reason, as when production cannot load flag tags. +The protobuf helpers ignore both; their ``selection_ignored_*`` scripted responses add a +``DD-Flag-Selection-Ignored`` header to the unfiltered configuration. """ from __future__ import annotations @@ -15,12 +24,24 @@ import json import os from pathlib import Path +import re import socket import threading import time from typing import TYPE_CHECKING, Literal, TypedDict, cast from urllib.parse import parse_qs, urlencode, urlparse +from ._ffe_flag_selection import ( + FLAG_SELECTION_IGNORED_HEADER, + Selection, + SelectionIgnoredError, + has_selection_parameters, + is_selection_parameter, + load_flag_tags, + parse_selection, + select_flags, + validate_forced_reason, +) from ._utils import CONTROL_URL_PREFIX @@ -78,9 +99,11 @@ EXPECTED_DD_ENV = "test" CONFIG_QUERY = urlencode({"dd_env": EXPECTED_DD_ENV}) REPO_ROOT = Path(__file__).parents[2] -# relative to the repository root: the mocked backend container mounts it at the same place +# relative to the repository root: the mocked backend container mounts them at the same place UFC_FIXTURE_RELATIVE_PATH = "tests/parametric/test_ffe/ffe-system-test-data/ufc-config.json" +EVALUATION_SUITES_RELATIVE_PATH = "tests/parametric/test_ffe/ffe-system-test-data/evaluation-suites" UFC_FIXTURE_PATH = REPO_ROOT / UFC_FIXTURE_RELATIVE_PATH +SUITE_NAME_PATTERN = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") MALFORMED_UFC_BYTES = b'{"flags": [' UFC_RESPONSE_TYPE = "universal-flag-configuration" RESPONSE_SECRET = "private-response-body" # noqa: S105 - dummy body used to detect error-message leakage @@ -126,6 +149,8 @@ def __init__(self) -> None: self.last_status_code: int | None = None self.status_codes: list[int] = [] self.requests: list[MockedBackendFFERequest] = [] + self._flag_selection_ignored_reason: str | None = None + self._suite: str | None = None def reset(self) -> None: with self._lock: @@ -139,6 +164,8 @@ def reset(self) -> None: self.last_status_code = None self.status_codes = [] self.requests = [] + self._flag_selection_ignored_reason = None + self._suite = None def set_responses(self, responses: object, response_format: ResponseFormat) -> None: validated_responses = validate_responses(responses, response_format) @@ -166,6 +193,24 @@ def record_request(self, headers: Mapping[str, str], path: str, response_format: responses.pop(0) return response + @property + def flag_selection_ignored_reason(self) -> str | None: + with self._lock: + return self._flag_selection_ignored_reason + + def set_flag_selection_ignored_reason(self, reason: str | None) -> None: + with self._lock: + self._flag_selection_ignored_reason = reason + + @property + def suite(self) -> str | None: + with self._lock: + return self._suite + + def set_suite(self, suite: str | None) -> None: + with self._lock: + self._suite = suite + def record_response(self, status_code: int) -> None: with self._lock: self.last_status_code = status_code @@ -198,8 +243,36 @@ def _get_dd_api_key(headers: Mapping[str, str]) -> str | None: return normalized.get("dd-api-key") -def _valid_ufc_bytes() -> bytes: - attributes = json.loads(UFC_FIXTURE_PATH.read_text()) +def _suite_directory(suite: str) -> Path: + return REPO_ROOT / EVALUATION_SUITES_RELATIVE_PATH / suite + + +def validate_suite(suite: object) -> str | None: + if suite is None: + return None + if ( + not isinstance(suite, str) + or SUITE_NAME_PATTERN.fullmatch(suite) is None + or not (_suite_directory(suite) / "ufc-config.json").is_file() + ): + raise ValueError("suite must be null or the name of an evaluation suite in the FFE fixture snapshot") + return suite + + +def _ufc_path(suite: str | None) -> Path: + return UFC_FIXTURE_PATH if suite is None else _suite_directory(suite) / "ufc-config.json" + + +def _flag_tags(suite: str | None) -> dict[str, list[str]]: + if suite is None or not (path := _suite_directory(suite) / "flag-tags.json").is_file(): + return {} + return load_flag_tags(path) + + +def _valid_ufc_bytes(selected_keys: list[str] | None = None, suite: str | None = None) -> bytes: + attributes = json.loads(_ufc_path(suite).read_text()) + if selected_keys is not None: + attributes["flags"] = {key: attributes["flags"][key] for key in selected_keys} return json.dumps( { "data": { @@ -234,17 +307,77 @@ def validate_responses(responses: object, response_format: ResponseFormat = "jso return responses -def _response_for_response(response: str) -> tuple[int, bytes, dict[str, str]]: +def _flag_selection_for_query(raw_query: str, forced_reason: str | None) -> tuple[Selection | None, str | None]: + """Return the selection to apply, or None to serve every flag, and the DD-Flag-Selection-Ignored reason.""" + if not has_selection_parameters(raw_query): + return None, None + try: + selection = parse_selection(raw_query) + except SelectionIgnoredError as error: + return None, error.reason + if selection.is_empty(): + return None, None + if forced_reason is not None: + return None, forced_reason + return selection, None + + +def _has_expected_config_query(raw_query: str) -> bool: + other_pairs = [pair for pair in raw_query.split("&") if not is_selection_parameter(pair)] + return parse_qs("&".join(other_pairs), keep_blank_values=True) == {"dd_env": [EXPECTED_DD_ENV]} + + +def ufc_etag_for(selection: Selection | None, suite: str | None = None) -> str: + """The ETag of the JSON configuration served from suite (None: the root UFC) for selection (None: every flag).""" + # A 304 must not confirm a body built from another configuration or for another selection. + etag = UFC_ETAG[:-1] + if suite is not None: + etag += f"-{suite}" + if selection is not None: + etag += f"-{selection.digest()}" + return f'{etag}"' + + +def _matches_if_none_match(if_none_match: str | None, etag: str) -> bool: + """'*' or a weak match with any comma-separated validator in the header.""" + if if_none_match is None: + return False + return any( + candidate.strip() == "*" or candidate.strip().removeprefix("W/") == etag.removeprefix("W/") + for candidate in if_none_match.split(",") + ) + + +def _response_for_response( + response: str, + selection: Selection | None = None, + ignored_reason: str | None = None, + if_none_match: str | None = None, + suite: str | None = None, + *, + selection_requested: bool = False, +) -> tuple[int, bytes, dict[str, str]]: if response == "unauthorized": return HTTPStatus.UNAUTHORIZED, b"", {} if response == "malformed": return HTTPStatus.OK, MALFORMED_UFC_BYTES, {"Content-Type": "application/json"} - if response == "not_modified": - return HTTPStatus.NOT_MODIFIED, b"", {"ETag": UFC_ETAG} + # A request carrying a selection, applied or ignored, gets a scripted 304 only for the validator of the body it + # would get otherwise, so a 304 never confirms a body built for another selection. + etag = ufc_etag_for(selection, suite) + if response == "not_modified" and (not selection_requested or _matches_if_none_match(if_none_match, etag)): + return HTTPStatus.NOT_MODIFIED, b"", {"ETag": etag} if response == "server_error": return HTTPStatus.INTERNAL_SERVER_ERROR, b"", {} - if response in {"valid", "delayed_valid", "timeout"}: - return HTTPStatus.OK, _valid_ufc_bytes(), {"Content-Type": "application/json", "ETag": UFC_ETAG} + if response in {"valid", "delayed_valid", "timeout", "not_modified"}: + headers = {"Content-Type": "application/json", "ETag": etag} + if ignored_reason is not None: + headers[FLAG_SELECTION_IGNORED_HEADER] = ignored_reason + if selection is None: + return HTTPStatus.OK, _valid_ufc_bytes(suite=suite), headers + flags = json.loads(_ufc_path(suite).read_text())["flags"] + flag_tags = _flag_tags(suite) + selected_keys = select_flags(selection, {key: flag_tags.get(key, []) for key in flags}) + return HTTPStatus.OK, _valid_ufc_bytes(selected_keys, suite), headers raise ValueError(f"Unsupported JSON response: {response}") @@ -288,6 +421,8 @@ class MockedBackendServerFfeMixin(_Base): # - GET /system-tests/ffe/status # - POST /system-tests/ffe/responses # - POST /system-tests/ffe/reset + # - POST /system-tests/ffe/flag-selection + # - POST /system-tests/ffe/suite def _ffe_configure(self) -> None: self._ffe_state = _MockedBackendFFEState() @@ -304,6 +439,8 @@ def _ffe_configure(self) -> None: self.add_handler("GET", f"{self._ffe_control_path}/status", self._ffe_handle_status) self.add_handler("POST", f"{self._ffe_control_path}/responses", self._ffe_handle_control_responses) self.add_handler("POST", f"{self._ffe_control_path}/reset", self._ffe_handle_control_reset) + self.add_handler("POST", f"{self._ffe_control_path}/flag-selection", self._ffe_handle_control_flag_selection) + self.add_handler("POST", f"{self._ffe_control_path}/suite", self._ffe_handle_control_suite) @property def ffe_library_base_url(self) -> str: @@ -341,7 +478,7 @@ def _ffe_handle_config( return self._ffe_handle_rules_config(request, _) parsed = urlparse(request.path) - if parse_qs(parsed.query, keep_blank_values=True) == {"dd_env": [EXPECTED_DD_ENV]}: + if _has_expected_config_query(parsed.query): return self._ffe_handle_config_exec(request) return request.write_json(HTTPStatus.NOT_FOUND, {"error": "not found"}) @@ -353,7 +490,16 @@ def _ffe_handle_config_exec(self, request: MockedBackendRequestHandler) -> tuple if response in {"delayed_valid", "timeout"}: time.sleep(TIMEOUT_RESPONSE_SECONDS if response == "timeout" else DELAYED_RESPONSE_SECONDS) - status_code, body, headers = _response_for_response(response) + query = urlparse(request.path).query + selection, ignored_reason = _flag_selection_for_query(query, self._ffe_state.flag_selection_ignored_reason) + status_code, body, headers = _response_for_response( + response, + selection, + ignored_reason, + request.headers.get("If-None-Match"), + self._ffe_state.suite, + selection_requested=has_selection_parameters(query), + ) self._ffe_state.record_response(status_code) with contextlib.suppress(BrokenPipeError, ConnectionResetError): request.send_response(status_code) @@ -375,7 +521,7 @@ def _ffe_handle_rules_config( headers = {"Content-Type": "application/protobuf", "ETag": wire_fixture["rules"]["etag"]} body = base64.b64decode(wire_fixture["rules"]["response"]) if response in SELECTION_IGNORED_RESPONSES: - headers["DD-Flag-Selection-Ignored"] = SELECTION_IGNORED_RESPONSES[response] + headers[FLAG_SELECTION_IGNORED_HEADER] = SELECTION_IGNORED_RESPONSES[response] elif response == "delayed": time.sleep(4) elif response == "delayed_body": @@ -436,6 +582,30 @@ def _ffe_handle_control_responses( return request.write_json(HTTPStatus.OK, self._ffe_state.status()) + def _ffe_handle_control_flag_selection( + self, request: MockedBackendRequestHandler, payload: bytes | dict | None + ) -> tuple[Mapping, bytes]: + if not isinstance(payload, dict) or set(payload) != {"ignored_reason"}: + return request.write_json(HTTPStatus.BAD_REQUEST, {"error": "control body must contain ignored_reason"}) + try: + reason = validate_forced_reason(payload["ignored_reason"]) + except ValueError as error: + return request.write_json(HTTPStatus.BAD_REQUEST, {"error": str(error)}) + self._ffe_state.set_flag_selection_ignored_reason(reason) + return request.write_json(HTTPStatus.OK, {"ignored_reason": reason}) + + def _ffe_handle_control_suite( + self, request: MockedBackendRequestHandler, payload: bytes | dict | None + ) -> tuple[Mapping, bytes]: + if not isinstance(payload, dict) or set(payload) != {"suite"}: + return request.write_json(HTTPStatus.BAD_REQUEST, {"error": "control body must contain suite"}) + try: + suite = validate_suite(payload["suite"]) + except ValueError as error: + return request.write_json(HTTPStatus.BAD_REQUEST, {"error": str(error)}) + self._ffe_state.set_suite(suite) + return request.write_json(HTTPStatus.OK, {"suite": suite}) + def _ffe_handle_control_reset( self, request: MockedBackendRequestHandler, _: bytes | dict | None ) -> tuple[Mapping, bytes]: diff --git a/utils/mocked_backend/_client.py b/utils/mocked_backend/_client.py index 707e6a7aacc..951d1309425 100644 --- a/utils/mocked_backend/_client.py +++ b/utils/mocked_backend/_client.py @@ -43,6 +43,28 @@ def ffe_set_responses(self, responses: object, *, response_format: ResponseForma ) response.raise_for_status() + def ffe_set_flag_selection_ignored_reason(self, reason: str | None) -> None: + """Ignore valid, non-empty flag selections with this DD-Flag-Selection-Ignored reason; None applies them""" + response = requests.post( + f"{self.base_url}{_FFE_CONTROL_PATH}/flag-selection", + json={"ignored_reason": reason}, + timeout=5, + verify=self.verify, + ) + response.raise_for_status() + + def ffe_use_suite(self, suite: str | None) -> None: + """Serve the JSON FFE configuration from this ffe-system-test-data evaluation suite, filtering selections with + its flag-tags.json; None serves the root UFC again + """ + response = requests.post( + f"{self.base_url}{_FFE_CONTROL_PATH}/suite", + json={"suite": suite}, + timeout=5, + verify=self.verify, + ) + response.raise_for_status() + def set_mocked_responses(self, responses: Sequence[MockedBackendResponse]) -> None: """Replace the mocked responses set by tests, like MockedBackendResponse.send() does with the proxy""" response = requests.put( diff --git a/utils/mocked_backend/_ffe_flag_selection.py b/utils/mocked_backend/_ffe_flag_selection.py new file mode 100644 index 00000000000..5c62b1b3928 --- /dev/null +++ b/utils/mocked_backend/_ffe_flag_selection.py @@ -0,0 +1,181 @@ +"""Flag selection for the mocked FFE rules-based configuration endpoint. + +Mirrors the production endpoint: a valid selection serves only the flags it selects, and a selection that cannot be +applied serves every flag and names the reason in the DD-Flag-Selection-Ignored response header. The shared +ffe-system-test-data vectors in flag-selection/selection-vectors.json define the semantics. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +import re +from typing import TYPE_CHECKING +from urllib.parse import unquote_to_bytes, urlencode + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + from pathlib import Path + + +SELECTED_FLAG_TAGS = "selected_flag_tags" +SELECTED_FLAG_KEY_PATTERNS = "selected_flag_key_patterns" +FLAG_SELECTION_IGNORED_HEADER = "DD-Flag-Selection-Ignored" +# Limits from flag-selection/README.md in DataDog/ffe-system-test-data. +MAX_VALUES = 31 +MAX_VALUE_BYTES = 200 +FORCED_REASON_PATTERN = re.compile(r"[A-Za-z0-9_.-]{1,64}") + +# The Unicode White_Space property. str.strip() would also remove U+001C..U+001F, which production keeps. +_WHITE_SPACE = "".join( + chr(code_point) + for code_point in ( + *range(0x09, 0x0E), + 0x20, + 0x85, + 0xA0, + 0x1680, + *range(0x2000, 0x200B), + 0x2028, + 0x2029, + 0x202F, + 0x205F, + 0x3000, + ) +) +_INVALID_ESCAPE = re.compile(r"%(?![0-9A-Fa-f]{2})") + + +class SelectionIgnoredError(ValueError): + def __init__(self, reason: str) -> None: + super().__init__(reason) + self.reason = reason + + +@dataclass(frozen=True) +class Selection: + tags: tuple[str, ...] + key_patterns: tuple[str, ...] + + def is_empty(self) -> bool: + return not self.tags and not self.key_patterns + + def digest(self) -> str: + canonical = urlencode( + [(SELECTED_FLAG_KEY_PATTERNS, _utf8(value)) for value in self.key_patterns] + + [(SELECTED_FLAG_TAGS, _utf8(value)) for value in self.tags] + ) + return hashlib.sha256(canonical.encode()).hexdigest()[:16] + + +def _raw_bytes(value: str) -> bytes: + # http.server decodes the request line as ISO-8859-1, so this restores the bytes on the wire. + try: + return value.encode("latin-1") + except UnicodeEncodeError: + return value.encode("utf-8") + + +def _query_unescape(value: str) -> str: + """Decode like Go's url.QueryUnescape: '+' is a space and a '%' must start a two-digit hex escape.""" + if _INVALID_ESCAPE.search(value): + raise ValueError("invalid percent-escape") + return unquote_to_bytes(_raw_bytes(value.replace("+", " "))).decode("utf-8", "surrogateescape") + + +def _utf8(value: str) -> bytes: + """The value's bytes on the wire, including bytes that are not valid UTF-8.""" + return value.encode("utf-8", "surrogateescape") + + +def _parameter_name(pair: str) -> str | None: + try: + return _query_unescape(pair.partition("=")[0]) + except ValueError: + return None + + +def is_selection_parameter(pair: str) -> bool: + return _parameter_name(pair) in {SELECTED_FLAG_TAGS, SELECTED_FLAG_KEY_PATTERNS} + + +def has_selection_parameters(raw_query: str) -> bool: + return any(is_selection_parameter(pair) for pair in raw_query.split("&")) + + +def _normalize(values: Iterable[str]) -> tuple[str, ...]: + trimmed = {value.strip(_WHITE_SPACE) for value in values} + trimmed.discard("") + return tuple(sorted(trimmed, key=_utf8)) + + +def parse_selection(raw_query: str) -> Selection: + """Parse the raw query as http.server receives it. + + Raise SelectionIgnoredError with the first reason, in the documented precedence, that ignores the selection. + """ + raw: dict[str, list[str]] = {SELECTED_FLAG_TAGS: [], SELECTED_FLAG_KEY_PATTERNS: []} + for pair in raw_query.split("&"): + name = _parameter_name(pair) + if name not in raw: + continue + try: + raw[name].append(_query_unescape(pair.partition("=")[2])) + except ValueError: + raise SelectionIgnoredError("malformed_selection") from None + + selection = Selection( + tags=_normalize(raw[SELECTED_FLAG_TAGS]), key_patterns=_normalize(raw[SELECTED_FLAG_KEY_PATTERNS]) + ) + if len(selection.tags) > MAX_VALUES or len(selection.key_patterns) > MAX_VALUES: + raise SelectionIgnoredError("too_many_values") + if any(len(_utf8(pattern)) > MAX_VALUE_BYTES for pattern in selection.key_patterns): + raise SelectionIgnoredError("invalid_key_pattern") + if any(len(_utf8(tag)) > MAX_VALUE_BYTES for tag in selection.tags): + raise SelectionIgnoredError("invalid_tag") + return selection + + +def match_key_pattern(pattern: str, key: str) -> bool: + """'*' matches any run of characters, including none; every other character is literal.""" + parts = pattern.split("*") + if len(parts) == 1: + return pattern == key + first, *middle, last = parts + if len(key) < len(first) + len(last) or not key.startswith(first) or not key.endswith(last): + return False + # Leftmost matching of the middle literals is enough: a later occurrence leaves less room for the rest. + remaining = key[len(first) : len(key) - len(last)] + for part in middle: + index = remaining.find(part) + if index < 0: + return False + remaining = remaining[index + len(part) :] + return True + + +def select_flags(selection: Selection, flag_tags: Mapping[str, Sequence[str]]) -> list[str]: + """Return the sorted keys selected among flag_tags, which maps every served flag key to its tags.""" + wanted = set(selection.tags) + return sorted( + key + for key, tags in flag_tags.items() + if selection.is_empty() + or not wanted.isdisjoint(tags) + or any(match_key_pattern(pattern, key) for pattern in selection.key_patterns) + ) + + +def load_flag_tags(path: Path) -> dict[str, list[str]]: + """Tags per flag key from an evaluation suite's flag-tags.json. A flag that is not listed has no tags.""" + fixture = json.loads(path.read_text(encoding="utf-8")) + return {flag["key"]: list(flag["tags"]) for flag in fixture["flags"]} + + +def validate_forced_reason(reason: object) -> str | None: + if reason is None: + return None + if not isinstance(reason, str) or FORCED_REASON_PATTERN.fullmatch(reason) is None: + raise ValueError("ignored_reason must be null or a token of letters, digits, '_', '.', or '-'") + return reason diff --git a/utils/mocked_backend/ffe.py b/utils/mocked_backend/ffe.py index a9281b222e2..dd28d7c508f 100644 --- a/utils/mocked_backend/ffe.py +++ b/utils/mocked_backend/ffe.py @@ -1,6 +1,14 @@ +from ._ffe_flag_selection import ( + FLAG_SELECTION_IGNORED_HEADER, + SELECTED_FLAG_KEY_PATTERNS, + SELECTED_FLAG_TAGS, + Selection, +) from ._backend_ffe import ( CONFIG_PATH, CONFIG_QUERY, + EVALUATION_SUITES_RELATIVE_PATH, + MockedBackendFFERequest, MockedBackendFFEStatus, EXPECTED_DD_ENV, RULES_CONFIG_PATH, @@ -10,18 +18,26 @@ UFC_RESPONSE_TYPE, ffe_library_config_url, load_rules_wire_fixture, + ufc_etag_for, ) __all__ = [ "CONFIG_PATH", "CONFIG_QUERY", + "EVALUATION_SUITES_RELATIVE_PATH", "EXPECTED_DD_ENV", + "FLAG_SELECTION_IGNORED_HEADER", "RESPONSE_SECRET", "RULES_CONFIG_PATH", + "SELECTED_FLAG_KEY_PATTERNS", + "SELECTED_FLAG_TAGS", "UFC_FIXTURE_RELATIVE_PATH", "UFC_RESPONSE_TYPE", + "MockedBackendFFERequest", "MockedBackendFFEStatus", "ResponseFormat", + "Selection", "ffe_library_config_url", "load_rules_wire_fixture", + "ufc_etag_for", ] diff --git a/utils/scripts/update_ffe_fixtures.py b/utils/scripts/update_ffe_fixtures.py index 4a41aaf2cdf..6b39a1b105f 100644 --- a/utils/scripts/update_ffe_fixtures.py +++ b/utils/scripts/update_ffe_fixtures.py @@ -16,6 +16,7 @@ FIXTURE_REF_PATTERN = re.compile(r"[A-Za-z0-9._/-]+") SELECTION_VECTORS_PATH = Path("flag-selection/selection-vectors.json") SELECTION_DROP_REASONS = frozenset({"malformed_selection", "too_many_values", "invalid_key_pattern", "invalid_tag"}) +SUITE_FLAG_TAGS_FILE = "flag-tags.json" def _validate_fixture_ref(fixture_ref: str) -> None: @@ -113,6 +114,32 @@ def _validate_evaluation_suite(config_path: Path, cases_directory: Path) -> int: return case_count +def _validate_flag_entries(flags: object, location: str) -> None: + if ( + not isinstance(flags, list) + or not flags + or not all( + isinstance(flag, dict) + and isinstance(flag.get("key"), str) + and isinstance(flag.get("tags"), list) + and all(isinstance(tag, str) for tag in flag["tags"]) + for flag in flags + ) + ): + msg = f"{location} flags must be a non-empty array of key and tags entries" + raise ValueError(msg) + + +def _validate_flag_tags(path: Path) -> None: + location = f"{path.parent.name}/{path.name}" + with path.open(encoding="utf-8") as tags_file: + content = json.load(tags_file) + if not isinstance(content, dict) or set(content) != {"flags"}: + msg = f"{location} must contain only flags" + raise ValueError(msg) + _validate_flag_entries(content["flags"], location) + + def _validate_fixture_snapshot(snapshot: Path) -> int: fixture_count = _validate_evaluation_suite(snapshot / "ufc-config.json", snapshot / "evaluation-cases") @@ -130,9 +157,15 @@ def _validate_fixture_snapshot(snapshot: Path) -> int: if not suite_directory.is_dir() or suite_directory.is_symlink(): msg = "FFE evaluation-suites must contain only suite directories" raise ValueError(msg) - if {entry.name for entry in suite_directory.iterdir()} != {"ufc-config.json", "cases"}: - msg = f"FFE feature suite {suite_directory.name} must contain only ufc-config.json and cases" + entries = {entry.name for entry in suite_directory.iterdir()} + if not {"ufc-config.json", "cases"} <= entries <= {"ufc-config.json", "cases", SUITE_FLAG_TAGS_FILE}: + msg = ( + f"FFE feature suite {suite_directory.name} must contain only ufc-config.json, cases and an optional " + f"{SUITE_FLAG_TAGS_FILE}" + ) raise ValueError(msg) + if SUITE_FLAG_TAGS_FILE in entries: + _validate_flag_tags(suite_directory / SUITE_FLAG_TAGS_FILE) fixture_count += _validate_evaluation_suite( suite_directory / "ufc-config.json", suite_directory / "cases", @@ -148,6 +181,8 @@ def _validate_selection_vectors(snapshot: Path) -> int: if not isinstance(cases, list) or not cases: msg = "FFE selection vectors must contain a non-empty cases array" raise ValueError(msg) + # The mocked backend's flag selection unit tests read flags and each case's expected outcome. + _validate_flag_entries(vectors.get("flags"), SELECTION_VECTORS_PATH.name) names: set[str] = set() sdk_count = 0 @@ -160,6 +195,9 @@ def _validate_selection_vectors(snapshot: Path) -> int: msg = f"Duplicate FFE selection vector name: {name}" raise ValueError(msg) names.add(name) + if not isinstance(case.get("expected"), dict) or case["expected"].get("outcome") not in {"applied", "ignored"}: + msg = f"FFE selection vector must have an applied or ignored expected outcome: {name}" + raise ValueError(msg) if ("query" in case) == ("rawQuery" in case): msg = f"FFE selection vector must have exactly one query form: {name}" raise ValueError(msg)