Skip to content

Commit 4d2de56

Browse files
committed
Various ordering tests to meet spec.
1 parent 2520f7c commit 4d2de56

File tree

2 files changed

+121
-11
lines changed

2 files changed

+121
-11
lines changed

lib/src/main/java/javasdk/OpenFeatureClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void registerHooks(Hook... hooks) {
3333
this.clientHooks.addAll(Arrays.asList(hooks));
3434
}
3535

36-
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
36+
<T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
3737
FeatureProvider provider = this.openfeatureApi.getProvider();
3838
if (ctx == null) {
3939
ctx = new EvaluationContext();

lib/src/test/java/javasdk/HookSpecTests.java

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package javasdk;
22

3+
import com.google.common.collect.ImmutableMap;
34
import lombok.SneakyThrows;
45
import org.junit.jupiter.api.AfterEach;
56
import org.junit.jupiter.api.Disabled;
67
import org.junit.jupiter.api.Test;
8+
import org.mockito.InOrder;
79

810
import java.util.ArrayList;
911
import java.util.Arrays;
@@ -180,12 +182,14 @@ void emptyApiHooks() {
180182

181183
verify(h, times(0)).error(any(), any(), any());
182184
}
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")
183187
@Specification(spec="hooks", number="4.2", text="Hooks MUST be evaluated in the following order:" +
184188
"before: API, Client, Invocation" +
185189
"after: Invocation, Client, API" +
186190
"error (if applicable): Invocation, Client, API" +
187191
"finally: Invocation, Client, API")
188-
@Test void eval_order() {
192+
@Test void hook_eval_order() {
189193
List<String> evalOrder = new ArrayList<String>();
190194
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
191195
api.setProvider(new NoOpProvider());
@@ -220,7 +224,7 @@ void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
220224
}
221225

222226
@Override
223-
void after(HookContext ctx, FlagEvaluationDetails details) {
227+
void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, ImmutableMap<String, Object> hints) {
224228
evalOrder.add("client after");
225229
}
226230

@@ -268,25 +272,131 @@ void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints)
268272
assertEquals(expectedOrder, evalOrder);
269273
}
270274

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+
272289
@Specification(spec="hooks", number="2.1", text="HookHints MUST be a map of objects.")
273290
@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+
275334
@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.")
276335
@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+
278357
@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+
279370
@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.")
282394
@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.")
285395
void todo() {}
286396

287397
@SneakyThrows
288398
@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")
290400
@Test void doesnt_use_finally() {
291401
// Class [] carr = new Class[1];
292402
// carr[0] = HookContext.class;

0 commit comments

Comments
 (0)