Skip to content

Commit 5135505

Browse files
msiebertclaude
andcommitted
Add missing OpenFeature provider tests and fix provider gaps
Adds variant key passthrough, SDK exception handling (try/except), null variant key tests, and context forwarding to evaluation calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7e267c8 commit 5135505

2 files changed

Lines changed: 178 additions & 10 deletions

File tree

openfeature-provider/src/mixpanel_openfeature/provider.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,31 @@ def resolve_boolean_details(
3131
default_value: bool,
3232
evaluation_context: typing.Optional[EvaluationContext] = None,
3333
) -> FlagResolutionDetails[bool]:
34-
return self._resolve(flag_key, default_value, bool)
34+
return self._resolve(flag_key, default_value, bool, evaluation_context)
3535

3636
def resolve_string_details(
3737
self,
3838
flag_key: str,
3939
default_value: str,
4040
evaluation_context: typing.Optional[EvaluationContext] = None,
4141
) -> FlagResolutionDetails[str]:
42-
return self._resolve(flag_key, default_value, str)
42+
return self._resolve(flag_key, default_value, str, evaluation_context)
4343

4444
def resolve_integer_details(
4545
self,
4646
flag_key: str,
4747
default_value: int,
4848
evaluation_context: typing.Optional[EvaluationContext] = None,
4949
) -> FlagResolutionDetails[int]:
50-
return self._resolve(flag_key, default_value, int)
50+
return self._resolve(flag_key, default_value, int, evaluation_context)
5151

5252
def resolve_float_details(
5353
self,
5454
flag_key: str,
5555
default_value: float,
5656
evaluation_context: typing.Optional[EvaluationContext] = None,
5757
) -> FlagResolutionDetails[float]:
58-
return self._resolve(flag_key, default_value, float)
58+
return self._resolve(flag_key, default_value, float, evaluation_context)
5959

6060
def resolve_object_details(
6161
self,
@@ -65,13 +65,28 @@ def resolve_object_details(
6565
) -> FlagResolutionDetails[
6666
Union[Sequence[FlagValueType], Mapping[str, FlagValueType]]
6767
]:
68-
return self._resolve(flag_key, default_value, None)
68+
return self._resolve(flag_key, default_value, None, evaluation_context)
69+
70+
@staticmethod
71+
def _build_user_context(
72+
evaluation_context: typing.Optional[EvaluationContext],
73+
) -> dict:
74+
user_context: dict = {}
75+
if evaluation_context is not None:
76+
if evaluation_context.targeting_key:
77+
user_context["distinct_id"] = evaluation_context.targeting_key
78+
if evaluation_context.attributes:
79+
user_context["custom_properties"] = dict(
80+
evaluation_context.attributes
81+
)
82+
return user_context
6983

7084
def _resolve(
7185
self,
7286
flag_key: str,
7387
default_value: typing.Any,
7488
expected_type: typing.Optional[type],
89+
evaluation_context: typing.Optional[EvaluationContext] = None,
7590
) -> FlagResolutionDetails:
7691
if not self._are_flags_ready():
7792
return FlagResolutionDetails(
@@ -81,7 +96,15 @@ def _resolve(
8196
)
8297

8398
fallback = SelectedVariant(variant_value=default_value)
84-
result = self._flags_provider.get_variant(flag_key, fallback, {})
99+
user_context = self._build_user_context(evaluation_context)
100+
try:
101+
result = self._flags_provider.get_variant(flag_key, fallback, user_context)
102+
except Exception:
103+
return FlagResolutionDetails(
104+
value=default_value,
105+
error_code=ErrorCode.GENERAL,
106+
reason=Reason.ERROR,
107+
)
85108

86109
if result is fallback:
87110
return FlagResolutionDetails(
@@ -91,14 +114,17 @@ def _resolve(
91114
)
92115

93116
value = result.variant_value
117+
variant_key = result.variant_key
94118

95119
if expected_type is None:
96-
return FlagResolutionDetails(value=value, reason=Reason.STATIC)
120+
return FlagResolutionDetails(
121+
value=value, variant=variant_key, reason=Reason.STATIC
122+
)
97123

98124
if expected_type is int and isinstance(value, float):
99125
if math.isfinite(value) and value == math.floor(value):
100126
return FlagResolutionDetails(
101-
value=int(value), reason=Reason.STATIC
127+
value=int(value), variant=variant_key, reason=Reason.STATIC
102128
)
103129
return FlagResolutionDetails(
104130
value=default_value,
@@ -108,7 +134,7 @@ def _resolve(
108134

109135
if expected_type is float and isinstance(value, (int, float)):
110136
return FlagResolutionDetails(
111-
value=float(value), reason=Reason.STATIC
137+
value=float(value), variant=variant_key, reason=Reason.STATIC
112138
)
113139

114140
if not isinstance(value, expected_type):
@@ -118,7 +144,9 @@ def _resolve(
118144
reason=Reason.ERROR,
119145
)
120146

121-
return FlagResolutionDetails(value=value, reason=Reason.STATIC)
147+
return FlagResolutionDetails(
148+
value=value, variant=variant_key, reason=Reason.STATIC
149+
)
122150

123151
def _are_flags_ready(self) -> bool:
124152
if hasattr(self._flags_provider, "are_flags_ready"):

openfeature-provider/tests/test_provider.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest.mock import MagicMock
22
import pytest
3+
from openfeature.evaluation_context import EvaluationContext
34
from openfeature.exception import ErrorCode
45
from openfeature.flag_evaluation import Reason
56

@@ -294,3 +295,142 @@ def test_remote_provider_always_ready():
294295

295296
def test_shutdown_is_noop(provider):
296297
provider.shutdown() # Should not raise
298+
299+
300+
# --- EvaluationContext forwarding ---
301+
302+
303+
def test_forwards_targeting_key_as_distinct_id(provider, mock_flags):
304+
setup_flag(mock_flags, "flag", "val")
305+
ctx = EvaluationContext(targeting_key="user-123")
306+
provider.resolve_string_details("flag", "default", ctx)
307+
_, _, user_context = mock_flags.get_variant.call_args[0]
308+
assert user_context["distinct_id"] == "user-123"
309+
310+
311+
def test_forwards_attributes_as_custom_properties(provider, mock_flags):
312+
setup_flag(mock_flags, "flag", "val")
313+
ctx = EvaluationContext(attributes={"plan": "pro", "beta": True})
314+
provider.resolve_string_details("flag", "default", ctx)
315+
_, _, user_context = mock_flags.get_variant.call_args[0]
316+
assert user_context["custom_properties"] == {"plan": "pro", "beta": True}
317+
318+
319+
def test_forwards_full_context(provider, mock_flags):
320+
setup_flag(mock_flags, "flag", "val")
321+
ctx = EvaluationContext(
322+
targeting_key="user-456", attributes={"tier": "enterprise"}
323+
)
324+
provider.resolve_string_details("flag", "default", ctx)
325+
_, _, user_context = mock_flags.get_variant.call_args[0]
326+
assert user_context == {
327+
"distinct_id": "user-456",
328+
"custom_properties": {"tier": "enterprise"},
329+
}
330+
331+
332+
def test_no_context_passes_empty_dict(provider, mock_flags):
333+
setup_flag(mock_flags, "flag", "val")
334+
provider.resolve_string_details("flag", "default")
335+
_, _, user_context = mock_flags.get_variant.call_args[0]
336+
assert user_context == {}
337+
338+
339+
# --- Variant key passthrough ---
340+
341+
342+
def test_variant_key_present_in_boolean_resolution(provider, mock_flags):
343+
setup_flag(mock_flags, "bool-flag", True, variant_key="control")
344+
result = provider.resolve_boolean_details("bool-flag", False)
345+
assert result.value is True
346+
assert result.variant == "control"
347+
assert result.reason == Reason.STATIC
348+
349+
350+
def test_variant_key_present_in_string_resolution(provider, mock_flags):
351+
setup_flag(mock_flags, "string-flag", "hello", variant_key="treatment-a")
352+
result = provider.resolve_string_details("string-flag", "default")
353+
assert result.value == "hello"
354+
assert result.variant == "treatment-a"
355+
assert result.reason == Reason.STATIC
356+
357+
358+
def test_variant_key_present_in_integer_resolution(provider, mock_flags):
359+
setup_flag(mock_flags, "int-flag", 42, variant_key="v2")
360+
result = provider.resolve_integer_details("int-flag", 0)
361+
assert result.value == 42
362+
assert result.variant == "v2"
363+
assert result.reason == Reason.STATIC
364+
365+
366+
def test_variant_key_present_in_float_resolution(provider, mock_flags):
367+
setup_flag(mock_flags, "float-flag", 3.14, variant_key="v3")
368+
result = provider.resolve_float_details("float-flag", 0.0)
369+
assert result.value == pytest.approx(3.14)
370+
assert result.variant == "v3"
371+
assert result.reason == Reason.STATIC
372+
373+
374+
def test_variant_key_present_in_object_resolution(provider, mock_flags):
375+
setup_flag(mock_flags, "obj-flag", {"key": "value"}, variant_key="v4")
376+
result = provider.resolve_object_details("obj-flag", {})
377+
assert result.value == {"key": "value"}
378+
assert result.variant == "v4"
379+
assert result.reason == Reason.STATIC
380+
381+
382+
# --- SDK exception handling ---
383+
384+
385+
def test_sdk_exception_returns_default_boolean(provider, mock_flags):
386+
mock_flags.get_variant.side_effect = RuntimeError("SDK failure")
387+
result = provider.resolve_boolean_details("flag", True)
388+
assert result.value is True
389+
assert result.error_code == ErrorCode.GENERAL
390+
assert result.reason == Reason.ERROR
391+
392+
393+
def test_sdk_exception_returns_default_string(provider, mock_flags):
394+
mock_flags.get_variant.side_effect = RuntimeError("SDK failure")
395+
result = provider.resolve_string_details("flag", "fallback")
396+
assert result.value == "fallback"
397+
assert result.error_code == ErrorCode.GENERAL
398+
assert result.reason == Reason.ERROR
399+
400+
401+
def test_sdk_exception_returns_default_integer(provider, mock_flags):
402+
mock_flags.get_variant.side_effect = RuntimeError("SDK failure")
403+
result = provider.resolve_integer_details("flag", 99)
404+
assert result.value == 99
405+
assert result.error_code == ErrorCode.GENERAL
406+
assert result.reason == Reason.ERROR
407+
408+
409+
# --- Null variant key ---
410+
411+
412+
def test_null_variant_key_boolean(provider, mock_flags):
413+
setup_flag(mock_flags, "flag", True, variant_key=None)
414+
result = provider.resolve_boolean_details("flag", False)
415+
assert result.value is True
416+
assert result.variant is None
417+
assert result.reason == Reason.STATIC
418+
assert result.error_code is None
419+
420+
421+
def test_null_variant_key_string(provider, mock_flags):
422+
setup_flag(mock_flags, "flag", "hello", variant_key=None)
423+
result = provider.resolve_string_details("flag", "default")
424+
assert result.value == "hello"
425+
assert result.variant is None
426+
assert result.reason == Reason.STATIC
427+
assert result.error_code is None
428+
429+
430+
def test_null_variant_key_object(provider, mock_flags):
431+
setup_flag(mock_flags, "flag", {"key": "value"}, variant_key=None)
432+
result = provider.resolve_object_details("flag", {})
433+
assert result.value == {"key": "value"}
434+
assert result.variant is None
435+
assert result.reason == Reason.STATIC
436+
assert result.error_code is None

0 commit comments

Comments
 (0)