diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py index a555896d..1e614773 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py @@ -121,7 +121,7 @@ def _resolve( ) variant, value = flag.get_variant(variant) - if not value: + if value is None: raise ParseError(f"Resolved variant {variant} not in variants config.") return FlagResolutionDetails( diff --git a/providers/openfeature-provider-flagd/tests/test_in_process.py b/providers/openfeature-provider-flagd/tests/test_in_process.py new file mode 100644 index 00000000..cc9a67fd --- /dev/null +++ b/providers/openfeature-provider-flagd/tests/test_in_process.py @@ -0,0 +1,218 @@ +from unittest.mock import Mock, create_autospec + +import pytest + +from openfeature.contrib.provider.flagd.config import Config +from openfeature.contrib.provider.flagd.resolvers.in_process import InProcessResolver +from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore +from openfeature.evaluation_context import EvaluationContext +from openfeature.exception import FlagNotFoundError, ParseError + + +def targeting(): + return { + "if": [ + {"==": [{"var": "targetingKey"}, "target_variant"]}, + "target_variant", + None, + ] + } + + +def context(targeting_key): + return EvaluationContext(targeting_key=targeting_key) + + +@pytest.fixture +def config(): + return create_autospec(Config) + + +@pytest.fixture +def flag_store(): + return create_autospec(FlagStore) + + +@pytest.fixture +def flag(): + return Flag( + key="flag", + state="ENABLED", + variants={"default_variant": False, "target_variant": True}, + default_variant="default_variant", + targeting=targeting(), + ) + + +@pytest.fixture +def resolver(config): + config.offline_flag_source_path = "flag.json" + config.deadline_ms = 100 + return InProcessResolver( + config=config, + emit_provider_ready=Mock(), + emit_provider_error=Mock(), + emit_provider_stale=Mock(), + emit_provider_configuration_changed=Mock(), + ) + + +def test_resolve_boolean_details_flag_not_found(resolver): + resolver.flag_store.get_flag = Mock(return_value=None) + with pytest.raises(FlagNotFoundError): + resolver.resolve_boolean_details("nonexistent_flag", False) + + +def test_resolve_boolean_details_disabled_flag(flag, resolver): + flag.state = "DISABLED" + resolver.flag_store.get_flag = Mock(return_value=flag) + + result = resolver.resolve_boolean_details("disabled_flag", False) + + assert result.reason == "DISABLED" + assert result.variant is None + assert not result.value + + +def test_resolve_boolean_details_invalid_variant(resolver, flag): + flag.targeting = {"var": ["targetingKey", "invalid_variant"]} + + resolver.flag_store.get_flag = Mock(return_value=flag) + + with pytest.raises(ParseError): + resolver.resolve_boolean_details("flag", False) + + +@pytest.mark.parametrize( + "input_config, resolve_config, expected", + [ + ( + { + "variants": {"default_variant": False, "target_variant": True}, + "targeting": None, + }, + { + "context": None, + "method": "resolve_boolean_details", + "default_value": False, + }, + {"reason": "STATIC", "variant": "default_variant", "value": False}, + ), + ( + { + "variants": {"default_variant": False, "target_variant": True}, + "targeting": targeting(), + }, + { + "context": context("no_target_variant"), + "method": "resolve_boolean_details", + "default_value": False, + }, + {"reason": "DEFAULT", "variant": "default_variant", "value": False}, + ), + ( + { + "variants": {"default_variant": False, "target_variant": True}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_boolean_details", + "default_value": False, + }, + {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": True}, + ), + ( + { + "variants": {"default_variant": "default", "target_variant": "target"}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_string_details", + "default_value": "placeholder", + }, + { + "reason": "TARGETING_MATCH", + "variant": "target_variant", + "value": "target", + }, + ), + ( + { + "variants": {"default_variant": 1.0, "target_variant": 2.0}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_float_details", + "default_value": 0.0, + }, + {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 2.0}, + ), + ( + { + "variants": {"default_variant": True, "target_variant": False}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_boolean_details", + "default_value": True, + }, + {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": False}, + ), + ( + { + "variants": {"default_variant": 10, "target_variant": 0}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_integer_details", + "default_value": 1, + }, + {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 0}, + ), + ( + { + "variants": {"default_variant": {}, "target_variant": {}}, + "targeting": targeting(), + }, + { + "context": context("target_variant"), + "method": "resolve_object_details", + "default_value": {}, + }, + {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": {}}, + ), + ], + ids=[ + "static_flag", + "boolean_default_fallback", + "boolean_targeting_match", + "string_targeting_match", + "float_targeting_match", + "boolean_falsy_target", + "integer_falsy_target", + "object_falsy_target", + ], +) +def test_resolver_details( + resolver, + flag, + input_config, + resolve_config, + expected, +): + flag.variants = input_config["variants"] + flag.targeting = input_config["targeting"] + resolver.flag_store.get_flag = Mock(return_value=flag) + + result = getattr(resolver, resolve_config["method"])( + "flag", resolve_config["default_value"], resolve_config["context"] + ) + + assert result.reason == expected["reason"] + assert result.variant == expected["variant"] + assert result.value == expected["value"]