Skip to content

Commit fafd099

Browse files
fix(flagd): handle falsy target values correctly (#214)
* fix(flagd): handle falsy target values correctly Signed-off-by: AdityaVallabh <[email protected]> * chore(flagd): run pre-commit reformatting Signed-off-by: AdityaVallabh <[email protected]> * fix(flagd): skip handling 'None' Signed-off-by: AdityaVallabh <[email protected]> * chore(flagd): fix linting Signed-off-by: AdityaVallabh <[email protected]> --------- Signed-off-by: AdityaVallabh <[email protected]>
1 parent 1b9b5f1 commit fafd099

File tree

2 files changed

+219
-1
lines changed

2 files changed

+219
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _resolve(
121121
)
122122

123123
variant, value = flag.get_variant(variant)
124-
if not value:
124+
if value is None:
125125
raise ParseError(f"Resolved variant {variant} not in variants config.")
126126

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

0 commit comments

Comments
 (0)