Skip to content

Commit 235abba

Browse files
committed
Atopted tests
Signed-off-by: Guido Breitenhuber <[email protected]>
1 parent 2843298 commit 235abba

File tree

4 files changed

+273
-294
lines changed

4 files changed

+273
-294
lines changed

src/test/java/dev/openfeature/sdk/HookDataTest.java renamed to src/test/java/dev/openfeature/sdk/DefaultHookDataTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
import org.junit.jupiter.api.Test;
66

7-
class HookDataTest {
7+
class DefaultHookDataTest {
88

99
@Test
1010
void shouldStoreAndRetrieveValues() {
11-
HookData hookData = HookData.create();
11+
var hookData = new DefaultHookData();
1212

1313
hookData.set("key1", "value1");
1414
hookData.set("key2", 42);
@@ -21,14 +21,14 @@ void shouldStoreAndRetrieveValues() {
2121

2222
@Test
2323
void shouldReturnNullForMissingKeys() {
24-
HookData hookData = HookData.create();
24+
var hookData = new DefaultHookData();
2525

2626
assertNull(hookData.get("nonexistent"));
2727
}
2828

2929
@Test
3030
void shouldSupportTypeSafeRetrieval() {
31-
HookData hookData = HookData.create();
31+
var hookData = new DefaultHookData();
3232

3333
hookData.set("string", "hello");
3434
hookData.set("integer", 123);
@@ -41,14 +41,14 @@ void shouldSupportTypeSafeRetrieval() {
4141

4242
@Test
4343
void shouldReturnNullForMissingKeysWithType() {
44-
HookData hookData = HookData.create();
44+
var hookData = new DefaultHookData();
4545

4646
assertNull(hookData.get("missing", String.class));
4747
}
4848

4949
@Test
5050
void shouldThrowClassCastExceptionForWrongType() {
51-
HookData hookData = HookData.create();
51+
var hookData = new DefaultHookData();
5252

5353
hookData.set("string", "not a number");
5454

@@ -59,7 +59,7 @@ void shouldThrowClassCastExceptionForWrongType() {
5959

6060
@Test
6161
void shouldOverwriteExistingValues() {
62-
HookData hookData = HookData.create();
62+
var hookData = new DefaultHookData();
6363

6464
hookData.set("key", "original");
6565
assertEquals("original", hookData.get("key"));
@@ -70,7 +70,7 @@ void shouldOverwriteExistingValues() {
7070

7171
@Test
7272
void shouldSupportNullValues() {
73-
HookData hookData = HookData.create();
73+
var hookData = new DefaultHookData();
7474

7575
hookData.set("nullKey", null);
7676
assertNull(hookData.get("nullKey"));

src/test/java/dev/openfeature/sdk/HookContextTest.java

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,52 +29,4 @@ void metadata_field_is_type_metadata() {
2929
"The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters. It has no return value.")
3030
@Test
3131
void not_applicable_for_dynamic_context() {}
32-
33-
@Test
34-
void shouldCreateHookContextWithHookData() {
35-
HookContext<String> context = HookContext.<String>builder()
36-
.flagKey("test-flag")
37-
.type(FlagValueType.STRING)
38-
.defaultValue("default")
39-
.ctx(new ImmutableContext())
40-
.build();
41-
HookData hookData = HookData.create();
42-
hookData.set("test", "value");
43-
44-
HookContextWithData contextWithData = new HookContextWithData(context, hookData);
45-
46-
assertNotNull(contextWithData.getHookData());
47-
assertEquals("value", contextWithData.getHookData().get("test"));
48-
}
49-
50-
@Test
51-
void shouldCreateHookContextWithoutHookData() {
52-
HookContext<String> context = HookContext.<String>builder()
53-
.flagKey("test-flag")
54-
.type(FlagValueType.STRING)
55-
.defaultValue("default")
56-
.ctx(new ImmutableContext())
57-
.build();
58-
59-
assertNull(context.getHookData());
60-
}
61-
62-
@Test
63-
void shouldCreateHookContextWithHookDataUsingWith() {
64-
HookContext<String> originalContext = HookContext.<String>builder()
65-
.flagKey("test-flag")
66-
.type(FlagValueType.STRING)
67-
.defaultValue("default")
68-
.ctx(new ImmutableContext())
69-
.build();
70-
71-
HookData hookData = HookData.create();
72-
hookData.set("timing", System.currentTimeMillis());
73-
74-
HookContext<String> contextWithHookData = new HookContextWithData(originalContext, hookData);
75-
76-
assertNull(originalContext.getHookData());
77-
assertNotNull(contextWithHookData.getHookData());
78-
assertNotNull(contextWithHookData.getHookData().get("timing"));
79-
}
8032
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package dev.openfeature.sdk;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
9+
import dev.openfeature.sdk.fixtures.HookFixtures;
10+
import java.util.Arrays;
11+
import java.util.Collections;
12+
import java.util.HashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Optional;
16+
import org.junit.jupiter.api.DisplayName;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.params.ParameterizedTest;
19+
import org.junit.jupiter.params.provider.EnumSource;
20+
21+
class HookExecutorTest implements HookFixtures {
22+
@Test
23+
@DisplayName("should merge EvaluationContexts on before hooks correctly")
24+
void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
25+
Map<String, Value> attributes = new HashMap<>();
26+
attributes.put("baseKey", new Value("baseValue"));
27+
EvaluationContext baseContext = new ImmutableContext(attributes);
28+
29+
Hook<String> hook1 = mockStringHook();
30+
Hook<String> hook2 = mockStringHook();
31+
when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber")));
32+
when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar")));
33+
34+
HookExecutor executor = HookExecutor.create(
35+
Arrays.asList(hook1, hook2), getBaseHookContextForType(FlagValueType.STRING), baseContext, Collections.emptyMap());
36+
37+
executor.executeBeforeHooks();
38+
39+
EvaluationContext result = executor.getEvaluationContext();
40+
41+
assertThat(result.getValue("bla").asString()).isEqualTo("blubber");
42+
assertThat(result.getValue("foo").asString()).isEqualTo("bar");
43+
assertThat(result.getValue("baseKey").asString()).isEqualTo("baseValue");
44+
}
45+
46+
@ParameterizedTest
47+
@EnumSource(value = FlagValueType.class)
48+
@DisplayName("should always call generic hook")
49+
void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
50+
Hook<?> genericHook = mockGenericHook();
51+
52+
HookExecutor hookExecutor = HookExecutor.create(List.of(genericHook), getBaseHookContextForType(flagValueType), ImmutableContext.EMPTY, Collections.emptyMap());
53+
54+
callAllHooks(hookExecutor);
55+
56+
verify(genericHook).before(any(), any());
57+
verify(genericHook).after(any(), any(), any());
58+
verify(genericHook).finallyAfter(any(), any(), any());
59+
verify(genericHook).error(any(), any(), any());
60+
}
61+
62+
@ParameterizedTest
63+
@EnumSource(value = FlagValueType.class)
64+
@DisplayName("should allow hooks to store and retrieve data across stages")
65+
void shouldPassDataAcrossStages(FlagValueType flagValueType) {
66+
var testHook = new HookDataHook();
67+
HookExecutor hookExecutor = HookExecutor.create(List.of(testHook), getBaseHookContextForType(flagValueType), ImmutableContext.EMPTY, Collections.emptyMap());
68+
69+
hookExecutor.executeBeforeHooks();
70+
assertHookData(testHook, "before");
71+
72+
hookExecutor.executeAfterHooks(FlagEvaluationDetails.builder().build());
73+
assertHookData(testHook, "before", "after");
74+
75+
hookExecutor.executeAfterAllHooks(FlagEvaluationDetails.builder().build());
76+
assertHookData(testHook, "before", "after", "finallyAfter");
77+
78+
hookExecutor.executeErrorHooks(mock(Exception.class));
79+
assertHookData(testHook, "before", "after", "finallyAfter", "error");
80+
}
81+
82+
@ParameterizedTest
83+
@EnumSource(value = FlagValueType.class)
84+
@DisplayName("should isolate data between different hook instances")
85+
void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) {
86+
var testHook1 = new HookDataHook(1);
87+
var testHook2 = new HookDataHook(2);
88+
89+
HookExecutor hookExecutor = HookExecutor.create(List.of(testHook1, testHook2), getBaseHookContextForType(flagValueType), ImmutableContext.EMPTY, Collections.emptyMap());
90+
91+
callAllHooks(hookExecutor);
92+
93+
assertHookData(testHook1, 1, "before", "after", "finallyAfter", "error");
94+
assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error");
95+
}
96+
97+
@ParameterizedTest
98+
@EnumSource(value = FlagValueType.class)
99+
@DisplayName("should isolate data between the same hook executions")
100+
void shouldIsolateDataBetweenSameHookExecutions(FlagValueType flagValueType) {
101+
TestHookWithData testHook = new TestHookWithData("test-key", "value-1");
102+
103+
HookExecutor hookExecutor1 = HookExecutor.create(List.of(testHook), getBaseHookContextForType(flagValueType), ImmutableContext.EMPTY, Collections.emptyMap());
104+
HookExecutor hookExecutor2 = HookExecutor.create(List.of(testHook), getBaseHookContextForType(flagValueType), ImmutableContext.EMPTY, Collections.emptyMap());
105+
106+
// run hooks first time
107+
callAllHooks(hookExecutor1);
108+
assertHookData(testHook, "value-1");
109+
110+
// re-run with different value, will throw if HookData contains already data
111+
testHook.value = "value-2";
112+
callAllHooks(hookExecutor2);
113+
assertHookData(testHook, "value-2");
114+
}
115+
116+
private static void callAllHooks(HookExecutor hookExecutor) {
117+
hookExecutor.executeBeforeHooks();
118+
hookExecutor.executeAfterHooks(FlagEvaluationDetails.builder().build());
119+
hookExecutor.executeAfterAllHooks(FlagEvaluationDetails.builder().build());
120+
hookExecutor.executeErrorHooks(mock(Exception.class));
121+
}
122+
123+
private static void assertHookData(TestHookWithData testHook1, String expected) {
124+
assertThat(testHook1.onBeforeValue).isEqualTo(expected);
125+
assertThat(testHook1.onFinallyAfterValue).isEqualTo(expected);
126+
assertThat(testHook1.onAfterValue).isEqualTo(expected);
127+
assertThat(testHook1.onErrorValue).isEqualTo(expected);
128+
}
129+
130+
private static void assertHookData(HookDataHook testHook, String ... expectedKeys) {
131+
for (String expectedKey : expectedKeys) {
132+
assertThat(testHook.hookData.get(expectedKey))
133+
.withFailMessage("Expected key %s not present in hook data", expectedKey)
134+
.isNotNull();
135+
}
136+
}
137+
138+
private static void assertHookData(HookDataHook testHook, Object expectedValue, String ... expectedKeys) {
139+
for (String expectedKey : expectedKeys) {
140+
assertThat(testHook.hookData.get(expectedKey))
141+
.withFailMessage("Expected key '%s' not present in hook data", expectedKey)
142+
.isNotNull();
143+
assertThat(testHook.hookData.get(expectedKey))
144+
.withFailMessage("Expected key '%s' not containing expected value. Expected '%s' but found '%s'",
145+
expectedKey, expectedValue, testHook.hookData.get(expectedKey))
146+
.isEqualTo(expectedValue);
147+
}
148+
}
149+
150+
private SharedHookContext getBaseHookContextForType(FlagValueType flagValueType) {
151+
return new SharedHookContext<>(
152+
"flagKey",
153+
flagValueType,
154+
() -> "client",
155+
() -> "provider",
156+
createDefaultValue(flagValueType));
157+
}
158+
159+
private Object createDefaultValue(FlagValueType flagValueType) {
160+
switch (flagValueType) {
161+
case INTEGER:
162+
return 1;
163+
case BOOLEAN:
164+
return true;
165+
case STRING:
166+
return "defaultValue";
167+
case OBJECT:
168+
return "object";
169+
case DOUBLE:
170+
return "double";
171+
default:
172+
throw new IllegalArgumentException();
173+
}
174+
}
175+
176+
private EvaluationContext evaluationContextWithValue(String key, String value) {
177+
Map<String, Value> attributes = new HashMap<>();
178+
attributes.put(key, new Value(value));
179+
return new ImmutableContext(attributes);
180+
}
181+
182+
private static class HookDataHook implements Hook {
183+
private final Object value;
184+
HookData hookData = null;
185+
186+
public HookDataHook(Object value) {
187+
this.value = value;
188+
}
189+
190+
public HookDataHook() {
191+
this("test");
192+
}
193+
194+
@Override
195+
public Optional<EvaluationContext> before(HookContext ctx, Map hints) {
196+
ctx.getHookData().set("before", value);
197+
hookData = ctx.getHookData();
198+
return Optional.empty();
199+
}
200+
201+
@Override
202+
public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
203+
ctx.getHookData().set("after", value);
204+
hookData = ctx.getHookData();
205+
}
206+
207+
@Override
208+
public void error(HookContext ctx, Exception error, Map hints) {
209+
ctx.getHookData().set("error", value);
210+
hookData = ctx.getHookData();
211+
}
212+
213+
@Override
214+
public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {
215+
ctx.getHookData().set("finallyAfter", value);
216+
hookData = ctx.getHookData();
217+
}
218+
}
219+
220+
private class TestHookWithData implements Hook {
221+
222+
private final String key;
223+
Object value;
224+
225+
Object onBeforeValue;
226+
Object onAfterValue;
227+
Object onErrorValue;
228+
Object onFinallyAfterValue;
229+
230+
TestHookWithData(Object value) {
231+
this("test", value);
232+
}
233+
234+
TestHookWithData(String key, Object value) {
235+
this.key = key;
236+
this.value = value;
237+
}
238+
239+
@Override
240+
public Optional<EvaluationContext> before(HookContext ctx, Map hints) {
241+
var storedValue = ctx.getHookData().get(key);
242+
if (storedValue != null) {
243+
throw new Error("Hook data isolation violated! Data is already set.");
244+
}
245+
ctx.getHookData().set(key, value);
246+
onBeforeValue = ctx.getHookData().get(key);
247+
return Optional.empty();
248+
}
249+
250+
@Override
251+
public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
252+
onAfterValue = ctx.getHookData().get(key);
253+
}
254+
255+
@Override
256+
public void error(HookContext ctx, Exception error, Map hints) {
257+
onErrorValue = ctx.getHookData().get(key);
258+
}
259+
260+
@Override
261+
public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {
262+
onFinallyAfterValue = ctx.getHookData().get(key);
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)