|
1 | 1 | package javasdk;
|
2 | 2 |
|
| 3 | +import com.google.common.collect.ImmutableMap; |
3 | 4 | import lombok.SneakyThrows;
|
4 | 5 | import org.junit.jupiter.api.AfterEach;
|
5 | 6 | import org.junit.jupiter.api.Disabled;
|
6 | 7 | import org.junit.jupiter.api.Test;
|
| 8 | +import org.mockito.InOrder; |
7 | 9 |
|
8 | 10 | import java.util.ArrayList;
|
9 | 11 | import java.util.Arrays;
|
@@ -180,12 +182,14 @@ void emptyApiHooks() {
|
180 | 182 |
|
181 | 183 | verify(h, times(0)).error(any(), any(), any());
|
182 | 184 | }
|
| 185 | + |
| 186 | + @Specification(spec="hooks", number="4.1", text="The API, Client and invocation MUST have a method for registering hooks which accepts flag evaluation options") |
183 | 187 | @Specification(spec="hooks", number="4.2", text="Hooks MUST be evaluated in the following order:" +
|
184 | 188 | "before: API, Client, Invocation" +
|
185 | 189 | "after: Invocation, Client, API" +
|
186 | 190 | "error (if applicable): Invocation, Client, API" +
|
187 | 191 | "finally: Invocation, Client, API")
|
188 |
| - @Test void eval_order() { |
| 192 | + @Test void hook_eval_order() { |
189 | 193 | List<String> evalOrder = new ArrayList<String>();
|
190 | 194 | OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
191 | 195 | api.setProvider(new NoOpProvider());
|
@@ -220,7 +224,7 @@ void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
220 | 224 | }
|
221 | 225 |
|
222 | 226 | @Override
|
223 |
| - void after(HookContext ctx, FlagEvaluationDetails details) { |
| 227 | + void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, ImmutableMap<String, Object> hints) { |
224 | 228 | evalOrder.add("client after");
|
225 | 229 | }
|
226 | 230 |
|
@@ -268,25 +272,131 @@ void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints)
|
268 | 272 | assertEquals(expectedOrder, evalOrder);
|
269 | 273 | }
|
270 | 274 |
|
271 |
| - @Specification(spec="hooks", number="1.4", text="The evaluation context MUST be mutable only within the before hook.") |
| 275 | + @Specification(spec="hooks", number="4.6", text="If an error is encountered in the error stage, it MUST NOT be returned to the user.") |
| 276 | + @Disabled("Not actually sure what 'returned to the user' means in this context. There is no exception information returned.") |
| 277 | + @Test void error_in_error_stage() { |
| 278 | + Hook<Boolean> h = mock(Hook.class); |
| 279 | + doThrow(RuntimeException.class).when(h).error(any(), any(), any()); |
| 280 | + |
| 281 | + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); |
| 282 | + api.setProvider(new AlwaysBrokenProvider()); |
| 283 | + Client c = api.getClient(); |
| 284 | + |
| 285 | + FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false, null, FlagEvaluationOptions.builder().hook(h).build()); |
| 286 | + } |
| 287 | + |
| 288 | + |
272 | 289 | @Specification(spec="hooks", number="2.1", text="HookHints MUST be a map of objects.")
|
273 | 290 | @Specification(spec="hooks", number="2.2", text="Condition: HookHints MUST be immutable.")
|
274 |
| - @Specification(spec="hooks", number="3.1", text="Hooks MUST specify at least one stage.") |
| 291 | + @Specification(spec="hooks", number="5.4", text="The hook MUST NOT alter the HookHints object.") |
| 292 | + @Specification(spec="hooks", number="6.1", text="HookHints MUST passed between each hook.") |
| 293 | + @Test void hook_hints() { |
| 294 | + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); |
| 295 | + api.setProvider(new NoOpProvider<>()); |
| 296 | + Client client = api.getClient(); |
| 297 | + Hook<Boolean> mutatingHook = new Hook<Boolean>() { |
| 298 | + @Override |
| 299 | + void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) { |
| 300 | + assertTrue(hints instanceof ImmutableMap); |
| 301 | + } |
| 302 | + |
| 303 | + @Override |
| 304 | + void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, ImmutableMap<String, Object> hints) { |
| 305 | + assertTrue(hints instanceof ImmutableMap); |
| 306 | + } |
| 307 | + |
| 308 | + @Override |
| 309 | + void error(HookContext<Boolean> ctx, Exception error, ImmutableMap<String, Object> hints) { |
| 310 | + assertTrue(hints instanceof ImmutableMap); |
| 311 | + } |
| 312 | + |
| 313 | + @Override |
| 314 | + void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) { |
| 315 | + assertTrue(hints instanceof ImmutableMap); |
| 316 | + } |
| 317 | + }; |
| 318 | + |
| 319 | + ImmutableMap<String, Object> hh = ImmutableMap.of("My hint key", "My hint value"); |
| 320 | + |
| 321 | + client.getBooleanValue("key", false, new EvaluationContext(), FlagEvaluationOptions.builder() |
| 322 | + .hook(mutatingHook) |
| 323 | + .hookHints(hh) |
| 324 | + .build()); |
| 325 | + } |
| 326 | + |
| 327 | + @Specification(spec="hooks", number="5.2", text="Flag evaluation options MAY contain HookHints, a map of data to be provided to hook invocations.") |
| 328 | + @Test void missing_hook_hints() { |
| 329 | + FlagEvaluationOptions feo = FlagEvaluationOptions.builder().build(); |
| 330 | + assertNotNull(feo.getHookHints()); |
| 331 | + assertTrue(feo.getHookHints().isEmpty()); |
| 332 | + } |
| 333 | + |
275 | 334 | @Specification(spec="hooks", number="3.3", text="The after stage MUST run after flag evaluation occurs. It accepts a hook context (required), flag evaluation details (required) and HookHints (optional). It has no return value.")
|
276 | 335 | @Specification(spec="hooks", number="3.5", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and HookHints (optional). There is no return value.")
|
277 |
| - @Specification(spec="hooks", number="4.1", text="The API, Client and invocation MUST have a method for registering hooks which accepts flag evaluation options") |
| 336 | + @Test void flag_eval_hook_order() { |
| 337 | + Hook hook = mock(Hook.class); |
| 338 | + FeatureProvider provider = mock(FeatureProvider.class); |
| 339 | + when(provider.getBooleanEvaluation(any(), any(), any(), any())) |
| 340 | + .thenReturn(ProviderEvaluation.<Boolean>builder() |
| 341 | + .value(true) |
| 342 | + .build()); |
| 343 | + InOrder order = inOrder(hook, provider); |
| 344 | + |
| 345 | + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); |
| 346 | + api.setProvider(provider); |
| 347 | + Client client = api.getClient(); |
| 348 | + client.getBooleanValue("key", false, new EvaluationContext(), |
| 349 | + FlagEvaluationOptions.builder().hook(hook).build()); |
| 350 | + |
| 351 | + order.verify(hook).before(any(),any()); |
| 352 | + order.verify(provider).getBooleanEvaluation(any(), any(), any(), any()); |
| 353 | + order.verify(hook).after(any(),any(),any()); |
| 354 | + order.verify(hook).finallyAfter(any(),any()); |
| 355 | + } |
| 356 | + |
278 | 357 | @Specification(spec="hooks", number="4.4", text="If an error occurs in the before or after hooks, the error hooks MUST be invoked.")
|
| 358 | + @Test void error_hooks() { |
| 359 | + Hook hook = mock(Hook.class); |
| 360 | + doThrow(RuntimeException.class).when(hook).before(any(), any()); |
| 361 | + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); |
| 362 | + api.setProvider(new NoOpProvider<>()); |
| 363 | + Client client = api.getClient(); |
| 364 | + client.getBooleanValue("key", false, new EvaluationContext(), |
| 365 | + FlagEvaluationOptions.builder().hook(hook).build()); |
| 366 | + verify(hook, times(1)).before(any(), any()); |
| 367 | + verify(hook, times(1)).error(any(), any(), any()); |
| 368 | + } |
| 369 | + |
279 | 370 | @Specification(spec="hooks", number="4.5", text="If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked.")
|
280 |
| - @Specification(spec="hooks", number="4.6", text="If an error is encountered in the error stage, it MUST NOT be returned to the user.") |
281 |
| - @Specification(spec="hooks", number="5.2", text="Flag evaluation options MAY contain HookHints, a map of data to be provided to hook invocations.") |
| 371 | + @Test void multi_hooks_early_out__before() { |
| 372 | + Hook hook = mock(Hook.class); |
| 373 | + Hook hook2 = mock(Hook.class); |
| 374 | + doThrow(RuntimeException.class).when(hook).before(any(), any()); |
| 375 | + |
| 376 | + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); |
| 377 | + api.setProvider(new NoOpProvider<>()); |
| 378 | + Client client = api.getClient(); |
| 379 | + |
| 380 | + client.getBooleanValue("key", false, new EvaluationContext(), |
| 381 | + FlagEvaluationOptions.builder() |
| 382 | + .hook(hook2) |
| 383 | + .hook(hook) |
| 384 | + .build()); |
| 385 | + |
| 386 | + verify(hook, times(1)).before(any(), any()); |
| 387 | + verify(hook2, times(0)).before(any(), any()); |
| 388 | + |
| 389 | + verify(hook, times(1)).error(any(), any(), any()); |
| 390 | + verify(hook2, times(1)).error(any(), any(), any()); |
| 391 | + } |
| 392 | + @Specification(spec="hooks", number="1.4", text="The evaluation context MUST be mutable only within the before hook.") |
| 393 | + @Specification(spec="hooks", number="3.1", text="Hooks MUST specify at least one stage.") |
282 | 394 | @Specification(spec="hooks", number="5.3", text="HookHints MUST be passed to each hook through a parameter. It is merged into the object in the precedence order API -> Client -> Invocation (last wins).")
|
283 |
| - @Specification(spec="hooks", number="5.4", text="The hook MUST NOT alter the HookHints object.") |
284 |
| - @Specification(spec="hooks", number="6.1", text="HookHints MUST passed between each hook.") |
285 | 395 | void todo() {}
|
286 | 396 |
|
287 | 397 | @SneakyThrows
|
288 | 398 | @Specification(spec="hooks", number="3.6", text="Condition: If finally is a reserved word in the language, finallyAfter SHOULD be used.")
|
289 |
| - @Disabled |
| 399 | + @Disabled("Unsure why the getMethod() call doesn't work correctly") |
290 | 400 | @Test void doesnt_use_finally() {
|
291 | 401 | // Class [] carr = new Class[1];
|
292 | 402 | // carr[0] = HookContext.class;
|
|
0 commit comments