Skip to content

Commit a277973

Browse files
committed
test(isolated): add tests for isolated API instances
1 parent 5afc4ab commit a277973

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed

tests/test_isolated_api.py

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
"""Tests for isolated OpenFeature API instances (spec section 1.8)."""
2+
3+
from unittest.mock import MagicMock
4+
5+
from openfeature import api
6+
from openfeature._event_support import _default_event_support
7+
from openfeature.evaluation_context import EvaluationContext
8+
from openfeature.event import ProviderEvent, ProviderEventDetails
9+
from openfeature.hook import Hook
10+
from openfeature.isolated import OpenFeatureAPI, create_api
11+
from openfeature.provider import FeatureProvider, ProviderStatus
12+
from openfeature.provider.no_op_provider import NoOpProvider
13+
from openfeature.transaction_context import ContextVarsTransactionContextPropagator
14+
15+
# --- Spec 1.8.1: Factory returns independent instances ---
16+
17+
18+
def test_create_api_returns_new_instance():
19+
api1 = create_api()
20+
api2 = create_api()
21+
assert api1 is not api2
22+
23+
24+
def test_isolated_instance_is_openfeature_api():
25+
api_instance = create_api()
26+
assert isinstance(api_instance, OpenFeatureAPI)
27+
28+
29+
# --- Spec 1.8.2: Same API contract ---
30+
31+
32+
def test_isolated_api_provides_full_api_contract():
33+
api_instance = create_api()
34+
35+
# Provider management
36+
assert hasattr(api_instance, "set_provider")
37+
assert hasattr(api_instance, "get_provider_metadata")
38+
assert hasattr(api_instance, "clear_providers")
39+
assert hasattr(api_instance, "shutdown")
40+
41+
# Client creation
42+
assert hasattr(api_instance, "get_client")
43+
44+
# Hooks
45+
assert hasattr(api_instance, "add_hooks")
46+
assert hasattr(api_instance, "clear_hooks")
47+
assert hasattr(api_instance, "get_hooks")
48+
49+
# Context
50+
assert hasattr(api_instance, "get_evaluation_context")
51+
assert hasattr(api_instance, "set_evaluation_context")
52+
53+
# Events
54+
assert hasattr(api_instance, "add_handler")
55+
assert hasattr(api_instance, "remove_handler")
56+
57+
# Transaction context
58+
assert hasattr(api_instance, "get_transaction_context")
59+
assert hasattr(api_instance, "set_transaction_context")
60+
assert hasattr(api_instance, "set_transaction_context_propagator")
61+
62+
63+
def test_isolated_api_get_client_returns_working_client():
64+
provider = MagicMock(spec=FeatureProvider)
65+
provider.get_metadata.return_value = MagicMock(name="test-provider")
66+
67+
api_instance = create_api()
68+
api_instance.set_provider(provider)
69+
70+
client = api_instance.get_client()
71+
assert client is not None
72+
assert client.provider is provider
73+
74+
75+
def test_isolated_api_get_client_with_domain():
76+
provider = MagicMock(spec=FeatureProvider)
77+
provider.get_metadata.return_value = MagicMock(name="domain-provider")
78+
79+
api_instance = create_api()
80+
api_instance.set_provider(provider, domain="my-domain")
81+
82+
client = api_instance.get_client(domain="my-domain")
83+
assert client.provider is provider
84+
85+
86+
# --- Isolated state: providers ---
87+
88+
89+
def test_isolated_providers_are_independent():
90+
provider_a = MagicMock(spec=FeatureProvider)
91+
provider_a.get_metadata.return_value = MagicMock(name="provider-a")
92+
provider_b = MagicMock(spec=FeatureProvider)
93+
provider_b.get_metadata.return_value = MagicMock(name="provider-b")
94+
95+
api1 = create_api()
96+
api2 = create_api()
97+
98+
api1.set_provider(provider_a)
99+
api2.set_provider(provider_b)
100+
101+
client1 = api1.get_client()
102+
client2 = api2.get_client()
103+
104+
assert client1.provider is provider_a
105+
assert client2.provider is provider_b
106+
107+
108+
def test_isolated_provider_does_not_affect_global():
109+
provider = MagicMock(spec=FeatureProvider)
110+
provider.get_metadata.return_value = MagicMock(name="isolated-provider")
111+
112+
api_instance = create_api()
113+
api_instance.set_provider(provider)
114+
115+
# Global singleton should still have NoOpProvider
116+
global_client = api.get_client()
117+
assert isinstance(global_client.provider, NoOpProvider)
118+
119+
120+
# --- Isolated state: hooks ---
121+
122+
123+
def test_isolated_hooks_are_independent():
124+
hook_a = MagicMock(spec=Hook)
125+
hook_b = MagicMock(spec=Hook)
126+
127+
api1 = create_api()
128+
api2 = create_api()
129+
130+
api1.add_hooks([hook_a])
131+
api2.add_hooks([hook_b])
132+
133+
assert hook_a in api1.get_hooks()
134+
assert hook_b not in api1.get_hooks()
135+
assert hook_b in api2.get_hooks()
136+
assert hook_a not in api2.get_hooks()
137+
138+
139+
def test_isolated_hooks_do_not_affect_global():
140+
hook = MagicMock(spec=Hook)
141+
142+
api_instance = create_api()
143+
api_instance.add_hooks([hook])
144+
145+
assert hook not in api.get_hooks()
146+
147+
148+
def test_clear_hooks_on_isolated_api():
149+
hook = MagicMock(spec=Hook)
150+
151+
api_instance = create_api()
152+
api_instance.add_hooks([hook])
153+
assert len(api_instance.get_hooks()) == 1
154+
155+
api_instance.clear_hooks()
156+
assert len(api_instance.get_hooks()) == 0
157+
158+
159+
# --- Isolated state: evaluation context ---
160+
161+
162+
def test_isolated_evaluation_context_is_independent():
163+
ctx_a = EvaluationContext(targeting_key="user-a")
164+
ctx_b = EvaluationContext(targeting_key="user-b")
165+
166+
api1 = create_api()
167+
api2 = create_api()
168+
169+
api1.set_evaluation_context(ctx_a)
170+
api2.set_evaluation_context(ctx_b)
171+
172+
assert api1.get_evaluation_context().targeting_key == "user-a"
173+
assert api2.get_evaluation_context().targeting_key == "user-b"
174+
175+
176+
def test_isolated_evaluation_context_does_not_affect_global():
177+
ctx = EvaluationContext(targeting_key="isolated-user")
178+
179+
api_instance = create_api()
180+
api_instance.set_evaluation_context(ctx)
181+
182+
assert api.get_evaluation_context().targeting_key != "isolated-user"
183+
184+
185+
# --- Isolated state: events ---
186+
187+
188+
def test_isolated_event_handlers_are_independent():
189+
handler_a = MagicMock()
190+
handler_b = MagicMock()
191+
192+
api1 = create_api()
193+
api2 = create_api()
194+
195+
provider1 = MagicMock(spec=FeatureProvider)
196+
provider1.get_metadata.return_value = MagicMock(name="p1")
197+
provider2 = MagicMock(spec=FeatureProvider)
198+
provider2.get_metadata.return_value = MagicMock(name="p2")
199+
200+
api1.set_provider(provider1)
201+
api2.set_provider(provider2)
202+
203+
# Register handlers for CONFIGURATION_CHANGED to test dispatch isolation
204+
api1.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler_a)
205+
api2.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler_b)
206+
207+
# Dispatch event on api1's registry — only handler_a should fire
208+
api1._provider_registry.dispatch_event(
209+
provider1,
210+
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
211+
ProviderEventDetails(),
212+
)
213+
214+
assert handler_a.call_count == 1
215+
assert handler_b.call_count == 0
216+
217+
218+
def test_isolated_event_handlers_do_not_affect_global():
219+
handler = MagicMock()
220+
221+
api_instance = create_api()
222+
provider = MagicMock(spec=FeatureProvider)
223+
provider.get_metadata.return_value = MagicMock(name="p")
224+
api_instance.set_provider(provider)
225+
api_instance.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler)
226+
227+
# Dispatch on global — isolated handler should NOT fire
228+
global_provider = MagicMock(spec=FeatureProvider)
229+
global_provider.get_metadata.return_value = MagicMock(name="gp")
230+
api.set_provider(global_provider)
231+
232+
handler.reset_mock()
233+
234+
_default_event_support.run_handlers_for_provider(
235+
global_provider,
236+
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
237+
ProviderEventDetails(),
238+
)
239+
240+
assert handler.call_count == 0
241+
242+
243+
# --- Provider lifecycle on isolated instances ---
244+
245+
246+
def test_isolated_api_initializes_provider():
247+
provider = MagicMock(spec=FeatureProvider)
248+
provider.get_metadata.return_value = MagicMock(name="init-provider")
249+
250+
api_instance = create_api()
251+
api_instance.set_provider(provider)
252+
253+
provider.initialize.assert_called_once()
254+
255+
256+
def test_isolated_api_shuts_down_provider():
257+
provider = MagicMock(spec=FeatureProvider)
258+
provider.get_metadata.return_value = MagicMock(name="shutdown-provider")
259+
260+
api_instance = create_api()
261+
api_instance.set_provider(provider)
262+
api_instance.shutdown()
263+
264+
provider.shutdown.assert_called_once()
265+
266+
267+
def test_isolated_api_clear_providers():
268+
provider = MagicMock(spec=FeatureProvider)
269+
provider.get_metadata.return_value = MagicMock(name="clear-provider")
270+
271+
api_instance = create_api()
272+
api_instance.set_provider(provider)
273+
api_instance.clear_providers()
274+
275+
client = api_instance.get_client()
276+
assert isinstance(client.provider, NoOpProvider)
277+
278+
279+
# --- Provider status on isolated instances ---
280+
281+
282+
def test_isolated_client_provider_status():
283+
provider = MagicMock(spec=FeatureProvider)
284+
provider.get_metadata.return_value = MagicMock(name="status-provider")
285+
286+
api_instance = create_api()
287+
api_instance.set_provider(provider)
288+
289+
client = api_instance.get_client()
290+
assert client.get_provider_status() == ProviderStatus.READY
291+
292+
293+
# --- Transaction context on isolated instances ---
294+
295+
296+
def test_isolated_transaction_context_propagator():
297+
api1 = create_api()
298+
api2 = create_api()
299+
300+
api1.set_transaction_context_propagator(
301+
ContextVarsTransactionContextPropagator()
302+
)
303+
304+
ctx = EvaluationContext(targeting_key="tx-user")
305+
api1.set_transaction_context(ctx)
306+
307+
assert api1.get_transaction_context().targeting_key == "tx-user"
308+
# api2 still uses NoOpTransactionContextPropagator → empty context
309+
assert api2.get_transaction_context().targeting_key is None
310+
311+
312+
def test_isolated_transaction_context_with_both_using_contextvars():
313+
"""Two APIs with ContextVars propagators must not share state."""
314+
api1 = create_api()
315+
api2 = create_api()
316+
317+
api1.set_transaction_context_propagator(
318+
ContextVarsTransactionContextPropagator()
319+
)
320+
api2.set_transaction_context_propagator(
321+
ContextVarsTransactionContextPropagator()
322+
)
323+
324+
api1.set_transaction_context(EvaluationContext(targeting_key="api1-user"))
325+
326+
assert api1.get_transaction_context().targeting_key == "api1-user"
327+
assert api2.get_transaction_context().targeting_key is None
328+
329+
330+
# --- Global singleton backward compatibility ---
331+
332+
333+
def test_global_api_still_works():
334+
provider = MagicMock(spec=FeatureProvider)
335+
provider.get_metadata.return_value = MagicMock(name="global-provider")
336+
337+
api.set_provider(provider)
338+
client = api.get_client()
339+
340+
assert client.provider is provider
341+
provider.initialize.assert_called_once()
342+
343+
344+
def test_global_hooks_still_work():
345+
hook = MagicMock(spec=Hook)
346+
347+
api.add_hooks([hook])
348+
assert hook in api.get_hooks()
349+
350+
api.clear_hooks()
351+
assert len(api.get_hooks()) == 0
352+
353+
354+
def test_global_evaluation_context_still_works():
355+
ctx = EvaluationContext(targeting_key="global-user")
356+
api.set_evaluation_context(ctx)
357+
assert api.get_evaluation_context().targeting_key == "global-user"

0 commit comments

Comments
 (0)