-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathFlagEvaluationSpecTest.java
360 lines (301 loc) · 20.6 KB
/
FlagEvaluationSpecTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
package dev.openfeature.sdk;
import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.simplify4u.slf4jmock.LoggerMock;
import org.slf4j.Logger;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.fixtures.HookFixtures;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
import dev.openfeature.sdk.testutils.TestEventsProvider;
import lombok.SneakyThrows;
class FlagEvaluationSpecTest implements HookFixtures {
private Logger logger;
private OpenFeatureAPI api;
private Client _client() {
FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider());
return api.getClient();
}
@BeforeEach
void getApiInstance() {
api = OpenFeatureAPI.getInstance();
}
@AfterEach void reset_ctx() {
api.setEvaluationContext(null);
}
@BeforeEach void set_logger() {
logger = Mockito.mock(Logger.class);
LoggerMock.setMock(OpenFeatureClient.class, logger);
}
@AfterEach void reset_logs() {
LoggerMock.setMock(OpenFeatureClient.class, logger);
}
@Specification(number="1.1.1", text="The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")
@Test void global_singleton() {
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
}
@Specification(number="1.1.2.1", text="The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.")
@Test void provider() {
FeatureProvider mockProvider = mock(FeatureProvider.class);
FeatureProviderTestUtils.setFeatureProvider(mockProvider);
assertThat(api.getProvider()).isEqualTo(mockProvider);
}
@SneakyThrows
@Specification(number="1.1.8", text="The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
@Test void providerAndWait() {
FeatureProvider provider = new TestEventsProvider(500);
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
assertThat(api.getProvider().getState()).isEqualTo(ProviderState.READY);
provider = new TestEventsProvider(500);
String providerName = "providerAndWait";
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider);
assertThat(api.getProvider(providerName).getState()).isEqualTo(ProviderState.READY);
}
@SneakyThrows
@Specification(number="1.1.8", text="The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.")
@Test void providerAndWaitError() {
FeatureProvider provider1 = new TestEventsProvider(500, true, "fake error");
assertThrows(GeneralError.class, () -> api.setProviderAndWait(provider1));
FeatureProvider provider2 = new TestEventsProvider(500, true, "fake error");
String providerName = "providerAndWaitError";
assertThrows(GeneralError.class, () -> api.setProviderAndWait(providerName, provider2));
}
@Specification(number="2.4.5", text="The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.")
@Test void shouldReturnNotReadyIfNotInitialized() {
FeatureProvider provider = new InMemoryProvider(new HashMap<>()) {
@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
Awaitility.await().wait(3000);
}
};
String providerName = "shouldReturnNotReadyIfNotInitialized";
OpenFeatureAPI.getInstance().setProvider(providerName, provider);
assertThat(api.getProvider(providerName).getState()).isEqualTo(ProviderState.NOT_READY);
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
assertEquals(ErrorCode.PROVIDER_NOT_READY, client.getBooleanDetails("return_error_when_not_initialized", false).getErrorCode());
}
@Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
@Test void provider_metadata() {
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name);
}
@Specification(number="1.1.4", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
@Test void hook_addition() {
Hook h1 = mock(Hook.class);
Hook h2 = mock(Hook.class);
api.addHooks(h1);
assertEquals(1, api.getHooks().size());
assertEquals(h1, api.getHooks().get(0));
api.addHooks(h2);
assertEquals(2, api.getHooks().size());
assertEquals(h2, api.getHooks().get(1));
}
@Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
@Test void namedClient() {
assertThatCode(() -> api.getClient("Sir Calls-a-lot")).doesNotThrowAnyException();
// TODO: Doesn't say that you can *get* the client name.. which seems useful?
}
@Specification(number="1.2.1", text="The client MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
@Test void hookRegistration() {
Client c = _client();
Hook m1 = mock(Hook.class);
Hook m2 = mock(Hook.class);
c.addHooks(m1);
c.addHooks(m2);
List<Hook> hooks = c.getHooks();
assertEquals(2, hooks.size());
assertTrue(hooks.contains(m1));
assertTrue(hooks.contains(m2));
}
@Specification(number="1.3.1.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.")
@Specification(number="1.3.3.1", text="The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")
@Test void value_flags() {
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
Client c = api.getClient();
String key = "key";
assertEquals(true, c.getBooleanValue(key, false));
assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext()));
assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
assertEquals("gnirts-ym", c.getStringValue(key, "my-string"));
assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext()));
assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext(), FlagEvaluationOptions.builder().build()));
assertEquals(400, c.getIntegerValue(key, 4));
assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext()));
assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
assertEquals(40.0, c.getDoubleValue(key, .4));
assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext()));
assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
assertEquals(null, c.getObjectValue(key, new Value()));
assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext()));
assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext(), FlagEvaluationOptions.builder().build()));
}
@Specification(number="1.4.1.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
@Specification(number="1.4.3", text="The evaluation details structure's value field MUST contain the evaluated flag value.")
@Specification(number="1.4.4.1", text="The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
@Specification(number="1.4.5", text="The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.")
@Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.")
@Specification(number="1.4.7", text="In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.")
@Test void detail_flags() {
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
Client c = api.getClient();
String key = "key";
FlagEvaluationDetails<Boolean> bd = FlagEvaluationDetails.<Boolean>builder()
.flagKey(key)
.value(false)
.variant(null)
.flagMetadata(DEFAULT_METADATA)
.build();
assertEquals(bd, c.getBooleanDetails(key, true));
assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext()));
assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails<String> sd = FlagEvaluationDetails.<String>builder()
.flagKey(key)
.value("tset")
.variant(null)
.flagMetadata(DEFAULT_METADATA)
.build();
assertEquals(sd, c.getStringDetails(key, "test"));
assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext()));
assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext(), FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails<Integer> id = FlagEvaluationDetails.<Integer>builder()
.flagKey(key)
.value(400)
.flagMetadata(DEFAULT_METADATA)
.build();
assertEquals(id, c.getIntegerDetails(key, 4));
assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext()));
assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails<Double> dd = FlagEvaluationDetails.<Double>builder()
.flagKey(key)
.value(40.0)
.flagMetadata(DEFAULT_METADATA)
.build();
assertEquals(dd, c.getDoubleDetails(key, .4));
assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext()));
assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build()));
// TODO: Structure detail tests.
}
@Specification(number="1.5.1", text="The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.")
@Test void hooks() {
Client c = _client();
Hook<Boolean> clientHook = mockBooleanHook();
Hook<Boolean> invocationHook = mockBooleanHook();
c.addHooks(clientHook);
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
.hook(invocationHook)
.build());
verify(clientHook, times(1)).before(any(), any());
verify(invocationHook, times(1)).before(any(), any());
}
@Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.")
@Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
@Specification(number="1.4.13", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
@Test void broken_provider() {
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
assertFalse(c.getBooleanValue("key", false));
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false);
assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode());
assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage());
}
@Specification(number="1.4.11", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
@Test void log_on_error() throws NotImplementedException {
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR.toString(), result.getReason());
Mockito.verify(logger).error(
ArgumentMatchers.contains("Unable to correctly evaluate flag with key"),
any(),
ArgumentMatchers.isA(FlagNotFoundError.class));
}
@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable name field or accessor of type string, which corresponds to the name value supplied during client creation.")
@Test void clientMetadata() {
Client c = _client();
assertNull(c.getMetadata().getName());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c2 = api.getClient("test");
assertEquals("test", c2.getMetadata().getName());
}
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
@Test void reason_is_error_when_there_are_errors() {
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR.toString(), result.getReason());
}
@Specification(number="1.4.14", text="If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.")
@Test void flag_metadata_passed() {
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null));
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertNotNull(result.getFlagMetadata());
}
@Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
@Specification(number="3.2.2.1", text="The API MUST have a method for setting the global evaluation context.")
@Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.")
@Test void multi_layer_context_merges_correctly() {
DoSomethingProvider provider = new DoSomethingProvider();
FeatureProviderTestUtils.setFeatureProvider(provider);
Map<String, Value> attributes = new HashMap<>();
attributes.put("common", new Value("1"));
attributes.put("common2", new Value("1"));
attributes.put("api", new Value("2"));
EvaluationContext apiCtx = new ImmutableContext(attributes);
api.setEvaluationContext(apiCtx);
Client c = api.getClient();
Map<String, Value> attributes1 = new HashMap<>();
attributes.put("common", new Value("3"));
attributes.put("common2", new Value("3"));
attributes.put("client", new Value("4"));
attributes.put("common", new Value("5"));
attributes.put("invocation", new Value("6"));
EvaluationContext clientCtx = new ImmutableContext(attributes);
c.setEvaluationContext(clientCtx);
EvaluationContext invocationCtx = new ImmutableContext();
// dosomethingprovider inverts this value.
assertTrue(c.getBooleanValue("key", false, invocationCtx));
EvaluationContext merged = provider.getMergedContext();
assertEquals("6", merged.getValue("invocation").asString());
assertEquals("5", merged.getValue("common").asString(), "invocation merge is incorrect");
assertEquals("4", merged.getValue("client").asString());
assertEquals("3", merged.getValue("common2").asString(), "api client merge is incorrect");
assertEquals("2", merged.getValue("api").asString());
}
@Specification(number="1.3.4", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
@Test void type_system_prevents_this() {}
@Specification(number="1.1.7", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
@Test void constructor_does_not_throw() {}
@Specification(number="1.4.12", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
@Test void one_thread_per_request_model() {}
@Specification(number="1.4.14.1", text="Condition: Flag metadata MUST be immutable.")
@Test void compiler_enforced() {}
@Specification(number="1.4.2.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure.")
@Specification(number="1.3.2.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.")
@Specification(number="3.2.2.2", text="The Client and invocation MUST NOT have a method for supplying evaluation context.")
@Specification(number="3.2.4.1", text="When the global evaluation context is set, the on context changed handler MUST run.")
@Test void not_applicable_for_dynamic_context() {}
}