|
1 |
| -import logging |
2 |
| -import time |
3 | 1 | import typing
|
4 | 2 |
|
5 | 3 | import pytest
|
6 |
| -from pytest_bdd import parsers, then, when |
| 4 | +from testcontainers.core.container import DockerContainer |
7 | 5 |
|
8 |
| -from openfeature.client import OpenFeatureClient, ProviderEvent |
9 |
| -from openfeature.evaluation_context import EvaluationContext |
| 6 | +from tests.e2e.flagd_container import FlagDContainer |
| 7 | +from tests.e2e.steps import * # noqa: F403 |
10 | 8 |
|
11 | 9 | JsonPrimitive = typing.Union[str, bool, float, int]
|
12 | 10 |
|
13 | 11 |
|
14 |
| -def to_bool(s: str) -> bool: |
15 |
| - return s.lower() == "true" |
16 |
| - |
17 |
| - |
18 |
| -@pytest.fixture |
19 |
| -def evaluation_context() -> EvaluationContext: |
20 |
| - return EvaluationContext() |
21 |
| - |
22 |
| - |
23 |
| -@when( |
24 |
| - parsers.cfparse( |
25 |
| - 'a zero-value boolean flag with key "{key}" is evaluated with default value "{default:bool}"', |
26 |
| - extra_types={"bool": to_bool}, |
27 |
| - ), |
28 |
| - target_fixture="key_and_default", |
29 |
| -) |
30 |
| -@when( |
31 |
| - parsers.cfparse( |
32 |
| - 'a zero-value string flag with key "{key}" is evaluated with default value "{default}"', |
33 |
| - ), |
34 |
| - target_fixture="key_and_default", |
35 |
| -) |
36 |
| -@when( |
37 |
| - parsers.cfparse( |
38 |
| - 'a string flag with key "{key}" is evaluated with default value "{default}"' |
39 |
| - ), |
40 |
| - target_fixture="key_and_default", |
41 |
| -) |
42 |
| -@when( |
43 |
| - parsers.cfparse( |
44 |
| - 'a zero-value integer flag with key "{key}" is evaluated with default value {default:d}', |
45 |
| - ), |
46 |
| - target_fixture="key_and_default", |
47 |
| -) |
48 |
| -@when( |
49 |
| - parsers.cfparse( |
50 |
| - 'an integer flag with key "{key}" is evaluated with default value {default:d}', |
51 |
| - ), |
52 |
| - target_fixture="key_and_default", |
53 |
| -) |
54 |
| -@when( |
55 |
| - parsers.cfparse( |
56 |
| - 'a zero-value float flag with key "{key}" is evaluated with default value {default:f}', |
57 |
| - ), |
58 |
| - target_fixture="key_and_default", |
59 |
| -) |
60 |
| -def setup_key_and_default( |
61 |
| - key: str, default: JsonPrimitive |
62 |
| -) -> typing.Tuple[str, JsonPrimitive]: |
63 |
| - return (key, default) |
64 |
| - |
65 |
| - |
66 |
| -@when( |
67 |
| - parsers.cfparse( |
68 |
| - 'a context containing a targeting key with value "{targeting_key}"' |
69 |
| - ), |
70 |
| -) |
71 |
| -def assign_targeting_context(evaluation_context: EvaluationContext, targeting_key: str): |
72 |
| - """a context containing a targeting key with value <targeting key>.""" |
73 |
| - evaluation_context.targeting_key = targeting_key |
74 |
| - |
75 |
| - |
76 |
| -@when( |
77 |
| - parsers.cfparse('a context containing a key "{key}", with value "{value}"'), |
78 |
| -) |
79 |
| -@when( |
80 |
| - parsers.cfparse('a context containing a key "{key}", with value {value:d}'), |
81 |
| -) |
82 |
| -def update_context( |
83 |
| - evaluation_context: EvaluationContext, key: str, value: JsonPrimitive |
84 |
| -): |
85 |
| - """a context containing a key and value.""" |
86 |
| - evaluation_context.attributes[key] = value |
87 |
| - |
88 |
| - |
89 |
| -@when( |
90 |
| - parsers.cfparse( |
91 |
| - 'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value "{value}"' |
92 |
| - ), |
93 |
| -) |
94 |
| -@when( |
95 |
| - parsers.cfparse( |
96 |
| - 'a context containing a nested property with outer key "{outer}" and inner key "{inner}", with value {value:d}' |
97 |
| - ), |
98 |
| -) |
99 |
| -def update_context_nested( |
100 |
| - evaluation_context: EvaluationContext, |
101 |
| - outer: str, |
102 |
| - inner: str, |
103 |
| - value: typing.Union[str, int], |
104 |
| -): |
105 |
| - """a context containing a nested property with outer key, and inner key, and value.""" |
106 |
| - if outer not in evaluation_context.attributes: |
107 |
| - evaluation_context.attributes[outer] = {} |
108 |
| - evaluation_context.attributes[outer][inner] = value |
109 |
| - |
110 |
| - |
111 |
| -@then( |
112 |
| - parsers.cfparse( |
113 |
| - 'the resolved boolean zero-value should be "{expected_value:bool}"', |
114 |
| - extra_types={"bool": to_bool}, |
115 |
| - ) |
116 |
| -) |
117 |
| -def assert_boolean_value( |
118 |
| - client: OpenFeatureClient, |
119 |
| - key_and_default: tuple, |
120 |
| - expected_value: bool, |
121 |
| - evaluation_context: EvaluationContext, |
122 |
| -): |
123 |
| - key, default = key_and_default |
124 |
| - evaluation_result = client.get_boolean_value(key, default, evaluation_context) |
125 |
| - assert evaluation_result == expected_value |
126 |
| - |
127 |
| - |
128 |
| -@then( |
129 |
| - parsers.cfparse( |
130 |
| - "the resolved integer zero-value should be {expected_value:d}", |
131 |
| - ) |
132 |
| -) |
133 |
| -@then(parsers.cfparse("the returned value should be {expected_value:d}")) |
134 |
| -def assert_integer_value( |
135 |
| - client: OpenFeatureClient, |
136 |
| - key_and_default: tuple, |
137 |
| - expected_value: bool, |
138 |
| - evaluation_context: EvaluationContext, |
139 |
| -): |
140 |
| - key, default = key_and_default |
141 |
| - evaluation_result = client.get_integer_value(key, default, evaluation_context) |
142 |
| - assert evaluation_result == expected_value |
143 |
| - |
144 |
| - |
145 |
| -@then( |
146 |
| - parsers.cfparse( |
147 |
| - "the resolved float zero-value should be {expected_value:f}", |
148 |
| - ) |
149 |
| -) |
150 |
| -def assert_float_value( |
151 |
| - client: OpenFeatureClient, |
152 |
| - key_and_default: tuple, |
153 |
| - expected_value: bool, |
154 |
| - evaluation_context: EvaluationContext, |
155 |
| -): |
156 |
| - key, default = key_and_default |
157 |
| - evaluation_result = client.get_float_value(key, default, evaluation_context) |
158 |
| - assert evaluation_result == expected_value |
159 |
| - |
160 |
| - |
161 |
| -@then(parsers.cfparse('the returned value should be "{expected_value}"')) |
162 |
| -def assert_string_value( |
163 |
| - client: OpenFeatureClient, |
164 |
| - key_and_default: tuple, |
165 |
| - expected_value: bool, |
166 |
| - evaluation_context: EvaluationContext, |
167 |
| -): |
168 |
| - key, default = key_and_default |
169 |
| - evaluation_result = client.get_string_value(key, default, evaluation_context) |
170 |
| - assert evaluation_result == expected_value |
171 |
| - |
172 |
| - |
173 |
| -@then( |
174 |
| - parsers.cfparse( |
175 |
| - 'the resolved string zero-value should be ""', |
176 |
| - ) |
177 |
| -) |
178 |
| -def assert_empty_string( |
179 |
| - client: OpenFeatureClient, |
180 |
| - key_and_default: tuple, |
181 |
| - evaluation_context: EvaluationContext, |
182 |
| -): |
183 |
| - key, default = key_and_default |
184 |
| - evaluation_result = client.get_string_value(key, default, evaluation_context) |
185 |
| - assert evaluation_result == "" |
186 |
| - |
187 |
| - |
188 |
| -@then(parsers.cfparse('the returned reason should be "{reason}"')) |
189 |
| -def assert_reason( |
190 |
| - client: OpenFeatureClient, |
191 |
| - key_and_default: tuple, |
192 |
| - evaluation_context: EvaluationContext, |
193 |
| - reason: str, |
194 |
| -): |
195 |
| - """the returned reason should be <reason>.""" |
196 |
| - key, default = key_and_default |
197 |
| - evaluation_result = client.get_string_details(key, default, evaluation_context) |
198 |
| - assert evaluation_result.reason.value == reason |
199 |
| - |
200 |
| - |
201 |
| -@pytest.fixture |
202 |
| -def handles() -> list: |
203 |
| - return [] |
204 |
| - |
205 |
| - |
206 |
| -@when( |
207 |
| - parsers.cfparse( |
208 |
| - "a {event_type:ProviderEvent} handler is added", |
209 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
210 |
| - ), |
211 |
| - target_fixture="handles", |
212 |
| -) |
213 |
| -def add_event_handler( |
214 |
| - client: OpenFeatureClient, event_type: ProviderEvent, handles: list |
215 |
| -): |
216 |
| - def handler(event): |
217 |
| - logging.info((event_type, event)) |
218 |
| - handles.append( |
219 |
| - { |
220 |
| - "type": event_type, |
221 |
| - "event": event, |
222 |
| - } |
223 |
| - ) |
224 |
| - |
225 |
| - client.add_handler(event_type, handler) |
226 |
| - return handles |
227 |
| - |
228 |
| - |
229 |
| -@when( |
230 |
| - parsers.cfparse( |
231 |
| - "a {event_type:ProviderEvent} handler and a {event_type2:ProviderEvent} handler are added", |
232 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
233 |
| - ), |
234 |
| - target_fixture="handles", |
235 |
| -) |
236 |
| -def add_event_handlers( |
237 |
| - client: OpenFeatureClient, |
238 |
| - event_type: ProviderEvent, |
239 |
| - event_type2: ProviderEvent, |
240 |
| - handles: list, |
241 |
| -): |
242 |
| - add_event_handler(client, event_type, handles) |
243 |
| - add_event_handler(client, event_type2, handles) |
244 |
| - |
245 |
| - |
246 |
| -def assert_handlers( |
247 |
| - handles, event_type: ProviderEvent, max_wait: int = 2, num_events: int = 1 |
248 |
| -): |
249 |
| - poll_interval = 0.05 |
250 |
| - while max_wait > 0: |
251 |
| - if sum([h["type"] == event_type for h in handles]) < num_events: |
252 |
| - max_wait -= poll_interval |
253 |
| - time.sleep(poll_interval) |
254 |
| - continue |
255 |
| - break |
256 |
| - |
257 |
| - logging.info(f"asserting num({event_type}) >= {num_events}: {handles}") |
258 |
| - actual_num_events = sum([h["type"] == event_type for h in handles]) |
259 |
| - assert ( |
260 |
| - num_events <= actual_num_events |
261 |
| - ), f"Expected {num_events} but got {actual_num_events}: {handles}" |
262 |
| - |
263 |
| - |
264 |
| -@then( |
265 |
| - parsers.cfparse( |
266 |
| - "the {event_type:ProviderEvent} handler must run", |
267 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
268 |
| - ) |
269 |
| -) |
270 |
| -@then( |
271 |
| - parsers.cfparse( |
272 |
| - "the {event_type:ProviderEvent} handler must run when the provider connects", |
273 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
274 |
| - ) |
275 |
| -) |
276 |
| -def assert_handler_run(handles, event_type: ProviderEvent): |
277 |
| - assert_handlers(handles, event_type, max_wait=3) |
278 |
| - |
279 |
| - |
280 |
| -@then( |
281 |
| - parsers.cfparse( |
282 |
| - "the {event_type:ProviderEvent} handler must run when the provider's connection is lost", |
283 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
284 |
| - ) |
285 |
| -) |
286 |
| -def assert_disconnect_handler(handles, event_type: ProviderEvent): |
287 |
| - # docker sync upstream restarts every 5s, waiting 2 cycles reduces test noise |
288 |
| - assert_handlers(handles, event_type, max_wait=10) |
289 |
| - |
290 |
| - |
291 |
| -@then( |
292 |
| - parsers.cfparse( |
293 |
| - "when the connection is reestablished the {event_type:ProviderEvent} handler must run again", |
294 |
| - extra_types={"ProviderEvent": ProviderEvent}, |
| 12 | +@pytest.fixture(autouse=True, scope="module") |
| 13 | +def setup(request, port, image): |
| 14 | + container: DockerContainer = FlagDContainer( |
| 15 | + image=image, |
| 16 | + port=port, |
295 | 17 | )
|
296 |
| -) |
297 |
| -def assert_disconnect_error(client: OpenFeatureClient, event_type: ProviderEvent): |
298 |
| - reconnect_handles = [] |
299 |
| - add_event_handler(client, event_type, reconnect_handles) |
300 |
| - assert_handlers(reconnect_handles, event_type, max_wait=6) |
| 18 | + # Setup code |
| 19 | + c = container.start() |
301 | 20 |
|
| 21 | + def fin(): |
| 22 | + c.stop() |
302 | 23 |
|
303 |
| -@then(parsers.cfparse('the event details must indicate "{key}" was altered')) |
304 |
| -def assert_flag_changed(handles, key): |
305 |
| - handle = None |
306 |
| - for h in handles: |
307 |
| - if h["type"] == ProviderEvent.PROVIDER_CONFIGURATION_CHANGED: |
308 |
| - handle = h |
309 |
| - break |
| 24 | + # Teardown code |
| 25 | + request.addfinalizer(fin) |
310 | 26 |
|
311 |
| - assert handle is not None |
312 |
| - assert key in handle["event"].flags_changed |
| 27 | + return c.get_exposed_port(port) |
0 commit comments