Skip to content

Commit 7e7fef2

Browse files
committed
fix(flagd): handle falsy target values correctly
Signed-off-by: AdityaVallabh <[email protected]>
1 parent 2f85057 commit 7e7fef2

File tree

3 files changed

+181
-2
lines changed

3 files changed

+181
-2
lines changed

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ def _resolve(
121121
)
122122

123123
variant, value = flag.get_variant(variant)
124-
if not value:
125-
raise ParseError(f"Resolved variant {variant} not in variants config.")
126124

127125
return FlagResolutionDetails(
128126
value,

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,7 @@ def get_variant(
9292
if isinstance(variant_key, bool):
9393
variant_key = str(variant_key).lower()
9494

95+
if variant_key not in self.variants:
96+
raise ParseError(f"Resolved variant {variant_key} not in variants config.")
97+
9598
return variant_key, self.variants.get(variant_key)
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import pytest
2+
from unittest.mock import Mock, create_autospec
3+
from openfeature.contrib.provider.flagd.resolvers.in_process import InProcessResolver
4+
from openfeature.evaluation_context import EvaluationContext
5+
from openfeature.exception import FlagNotFoundError
6+
from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore
7+
from openfeature.contrib.provider.flagd.config import Config
8+
from openfeature.exception import FlagNotFoundError, ParseError
9+
10+
@pytest.fixture
11+
def config():
12+
return create_autospec(Config)
13+
14+
@pytest.fixture
15+
def flag_store():
16+
return create_autospec(FlagStore)
17+
18+
@pytest.fixture
19+
def targeting():
20+
return {
21+
"if": [
22+
{"==": [{"var": "targetingKey"}, "target_variant"]},
23+
"target_variant",
24+
None,
25+
]
26+
}
27+
28+
@pytest.fixture
29+
def flag(targeting):
30+
return Flag(
31+
key="flag",
32+
state="ENABLED",
33+
variants={"default_variant": False, "target_variant": True},
34+
default_variant="default_variant",
35+
targeting=targeting
36+
)
37+
38+
@pytest.fixture
39+
def context():
40+
return EvaluationContext(targeting_key="target_variant")
41+
42+
@pytest.fixture
43+
def resolver(config):
44+
config.offline_flag_source_path = 'flag.json'
45+
config.deadline_ms = 100
46+
return InProcessResolver(
47+
config=config,
48+
emit_provider_ready=Mock(),
49+
emit_provider_error=Mock(),
50+
emit_provider_stale=Mock(),
51+
emit_provider_configuration_changed=Mock()
52+
)
53+
54+
@pytest.fixture
55+
def flag():
56+
return Flag(
57+
key="flag",
58+
state="ENABLED",
59+
variants={"default_variant": False},
60+
default_variant="default_variant",
61+
targeting=None,
62+
)
63+
64+
def targeting():
65+
return {
66+
"if": [
67+
{"==": [{"var": "targetingKey"}, "target_variant"]},
68+
"target_variant",
69+
None,
70+
]
71+
}
72+
73+
def context(targeting_key):
74+
return EvaluationContext(targeting_key=targeting_key)
75+
76+
def test_resolve_boolean_details_flag_not_found(resolver):
77+
resolver.flag_store.get_flag = Mock(return_value=None)
78+
with pytest.raises(FlagNotFoundError):
79+
resolver.resolve_boolean_details("nonexistent_flag", False)
80+
81+
def test_resolve_boolean_details_disabled_flag(flag, resolver):
82+
flag.state = "DISABLED"
83+
resolver.flag_store.get_flag = Mock(return_value=flag)
84+
85+
result = resolver.resolve_boolean_details("disabled_flag", False)
86+
87+
assert result.reason == "DISABLED"
88+
assert result.variant == None
89+
assert result.value == False
90+
91+
def test_resolve_boolean_details_invalid_variant(resolver, flag):
92+
flag.targeting = {
93+
"var": ["targetingKey", "invalid_variant"]
94+
}
95+
96+
resolver.flag_store.get_flag = Mock(return_value=flag)
97+
98+
with pytest.raises(ParseError):
99+
resolver.resolve_boolean_details("flag", False)
100+
101+
@pytest.mark.parametrize(
102+
"variants, targeting,"
103+
"context, method, default_value,"
104+
"expected_reason, expected_variant, expected_value,",
105+
[
106+
(
107+
{"default_variant": False, "target_variant": True}, None,
108+
None, "resolve_boolean_details", False,
109+
"STATIC", "default_variant", False,
110+
),
111+
(
112+
{"default_variant": False, "target_variant": True}, targeting(),
113+
context("no_target_variant"), "resolve_boolean_details", False,
114+
"DEFAULT", "default_variant", False,
115+
),
116+
(
117+
{"default_variant": False, "target_variant": True}, targeting(),
118+
context("target_variant"), "resolve_boolean_details", False,
119+
"TARGETING_MATCH", "target_variant", True,
120+
),
121+
(
122+
{"default_variant": "default", "target_variant": "target"}, targeting(),
123+
context("target_variant"), "resolve_string_details", "placeholder",
124+
"TARGETING_MATCH", "target_variant", "target",
125+
),
126+
(
127+
{"default_variant": 1.0, "target_variant": 2.0}, targeting(),
128+
context("target_variant"), "resolve_float_details", 0.0,
129+
"TARGETING_MATCH", "target_variant", 2.0,
130+
),
131+
(
132+
{"default_variant": True, "target_variant": False}, targeting(),
133+
context("target_variant"), "resolve_boolean_details", True,
134+
"TARGETING_MATCH", "target_variant", False,
135+
),
136+
(
137+
{"default_variant": 10, "target_variant": 0}, targeting(),
138+
context("target_variant"), "resolve_integer_details", 1,
139+
"TARGETING_MATCH", "target_variant", 0,
140+
),
141+
(
142+
{"default_variant": {}, "target_variant": {}}, targeting(),
143+
context("target_variant"), "resolve_object_details", {},
144+
"TARGETING_MATCH", "target_variant", {},
145+
),
146+
(
147+
{"default_variant": None, "target_variant": None}, targeting(),
148+
context("target_variant"), "resolve_object_details", {},
149+
"TARGETING_MATCH", "target_variant", None,
150+
),
151+
],
152+
ids=[
153+
"static_flag",
154+
"boolean_default_fallback",
155+
"boolean_targeting_match",
156+
"string_targeting_match",
157+
"float_targeting_match",
158+
"boolean_falsy_target",
159+
"integer_falsy_target",
160+
"object_falsy_target",
161+
"none_target_value",
162+
],
163+
)
164+
def test_resolver_details(
165+
resolver, flag,
166+
variants, targeting,
167+
context, method, default_value,
168+
expected_reason, expected_variant, expected_value
169+
):
170+
flag.variants = variants
171+
flag.targeting = targeting
172+
resolver.flag_store.get_flag = Mock(return_value=flag)
173+
174+
result = getattr(resolver, method)("flag", default_value, context)
175+
176+
assert result.reason == expected_reason
177+
assert result.variant == expected_variant
178+
assert result.value == expected_value

0 commit comments

Comments
 (0)