diff --git a/spotbugs-exclusions.xml b/spotbugs-exclusions.xml
index 66032ad08..b841bbad4 100644
--- a/spotbugs-exclusions.xml
+++ b/spotbugs-exclusions.xml
@@ -49,7 +49,6 @@
-
@@ -58,4 +57,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java
index f6c1d742e..945e0ea17 100644
--- a/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java
+++ b/src/main/java/dev/openfeature/sdk/ImmutableMetadata.java
@@ -13,6 +13,8 @@
@Slf4j
@EqualsAndHashCode
public class ImmutableMetadata {
+ public static final ImmutableMetadata EMPTY = new ImmutableMetadata(Collections.emptyMap());
+
private final Map metadata;
private ImmutableMetadata(Map metadata) {
diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java
deleted file mode 100644
index bd0ac2c21..000000000
--- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package dev.openfeature.sdk;
-
-public class AlwaysBrokenWithDetailsProvider implements FeatureProvider {
-
- private final String name = "always broken with details";
-
- @Override
- public Metadata getMetadata() {
- return () -> name;
- }
-
- @Override
- public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .errorMessage(TestConstants.BROKEN_MESSAGE)
- .errorCode(ErrorCode.FLAG_NOT_FOUND)
- .build();
- }
-
- @Override
- public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .errorMessage(TestConstants.BROKEN_MESSAGE)
- .errorCode(ErrorCode.FLAG_NOT_FOUND)
- .build();
- }
-
- @Override
- public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .errorMessage(TestConstants.BROKEN_MESSAGE)
- .errorCode(ErrorCode.FLAG_NOT_FOUND)
- .build();
- }
-
- @Override
- public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .errorMessage(TestConstants.BROKEN_MESSAGE)
- .errorCode(ErrorCode.FLAG_NOT_FOUND)
- .build();
- }
-
- @Override
- public ProviderEvaluation getObjectEvaluation(
- String key, Value defaultValue, EvaluationContext invocationContext) {
- return ProviderEvaluation.builder()
- .errorMessage(TestConstants.BROKEN_MESSAGE)
- .errorCode(ErrorCode.FLAG_NOT_FOUND)
- .build();
- }
-}
diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java
deleted file mode 100644
index 0ad09db29..000000000
--- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package dev.openfeature.sdk;
-
-import dev.openfeature.sdk.exceptions.FlagNotFoundError;
-
-public class AlwaysBrokenWithExceptionProvider implements FeatureProvider {
-
- private final String name = "always broken";
-
- @Override
- public Metadata getMetadata() {
- return () -> name;
- }
-
- @Override
- public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getObjectEvaluation(
- String key, Value defaultValue, EvaluationContext invocationContext) {
- throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
- }
-}
diff --git a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java
index beadf7aad..6bbb2e6c3 100644
--- a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java
+++ b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java
@@ -2,6 +2,7 @@
import static org.junit.jupiter.api.Assertions.*;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import org.junit.jupiter.api.Test;
class ClientProviderMappingTest {
@@ -10,13 +11,19 @@ class ClientProviderMappingTest {
void clientProviderTest() {
OpenFeatureAPI api = new OpenFeatureAPI();
- api.setProviderAndWait("client1", new DoSomethingProvider());
- api.setProviderAndWait("client2", new NoOpProvider());
+ var provider1 = TestProvider.builder().initsToReady();
+ var provider2 = TestProvider.builder().initsToReady();
+
+ api.setProviderAndWait("client1", provider1);
+ api.setProviderAndWait("client2", provider2);
Client c1 = api.getClient("client1");
Client c2 = api.getClient("client2");
- assertTrue(c1.getBooleanValue("test", false));
- assertFalse(c2.getBooleanValue("test", false));
+ c1.getBooleanValue("test", false);
+ c2.getBooleanValue("test", false);
+
+ assertEquals(1, provider1.getFlagEvaluations().size());
+ assertEquals(1, provider2.getFlagEvaluations().size());
}
}
diff --git a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
index c954c8b19..19108bde5 100644
--- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
+++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
@@ -8,7 +8,7 @@
import static org.mockito.Mockito.verify;
import dev.openfeature.sdk.fixtures.HookFixtures;
-import dev.openfeature.sdk.testutils.TestEventsProvider;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -23,13 +23,13 @@ class DeveloperExperienceTest implements HookFixtures {
private OpenFeatureAPI api;
@BeforeEach
- public void setUp() throws Exception {
+ void setUp() {
api = new OpenFeatureAPI();
}
@Test
void simpleBooleanFlag() {
- api.setProviderAndWait(new TestEventsProvider());
+ api.setProviderAndWait(TestProvider.builder().initsToReady());
Client client = api.getClient();
Boolean retval = client.getBooleanValue(flagKey, false);
assertFalse(retval);
@@ -39,7 +39,7 @@ void simpleBooleanFlag() {
void clientHooks() {
Hook exampleHook = mockBooleanHook();
- api.setProviderAndWait(new TestEventsProvider());
+ api.setProviderAndWait(TestProvider.builder().initsToReady());
Client client = api.getClient();
client.addHooks(exampleHook);
Boolean retval = client.getBooleanValue(flagKey, false);
@@ -52,7 +52,7 @@ void evalHooks() {
Hook clientHook = mockBooleanHook();
Hook evalHook = mockBooleanHook();
- api.setProviderAndWait(new TestEventsProvider());
+ api.setProviderAndWait(TestProvider.builder().initsToReady());
Client client = api.getClient();
client.addHooks(clientHook);
Boolean retval = client.getBooleanValue(
@@ -72,7 +72,7 @@ void evalHooks() {
@Test
void providingContext() {
- api.setProviderAndWait(new TestEventsProvider());
+ api.setProviderAndWait(TestProvider.builder().initsToReady());
Client client = api.getClient();
Map attributes = new HashMap<>();
List values = Arrays.asList(new Value(2), new Value(4));
@@ -88,7 +88,7 @@ void providingContext() {
@Test
void brokenProvider() {
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client client = api.getClient();
FlagEvaluationDetails retval = client.getBooleanDetails(flagKey, false);
assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode());
@@ -102,6 +102,8 @@ void providerLockedPerTransaction() {
final String defaultValue = "string-value";
final OpenFeatureAPI api = new OpenFeatureAPI();
+ var provider1 = TestProvider.builder().initsToReady();
+ var provider2 = TestProvider.builder().initsToReady();
class MutatingHook implements Hook {
@@ -110,31 +112,31 @@ class MutatingHook implements Hook {
// change the provider during a before hook - this should not impact the evaluation in progress
public Optional before(HookContext ctx, Map hints) {
- api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider());
+ api.setProviderAndWait(provider2);
return Optional.empty();
}
}
final Client client = api.getClient();
- api.setProviderAndWait(new DoSomethingProvider());
+ api.setProviderAndWait(provider1);
api.addHooks(new MutatingHook());
// if provider is changed during an evaluation transaction it should proceed with the original provider
- String doSomethingValue = client.getStringValue("val", defaultValue);
- assertEquals(new StringBuilder(defaultValue).reverse().toString(), doSomethingValue);
+ client.getStringValue("val", defaultValue);
+ assertEquals(1, provider1.getFlagEvaluations().size());
api.clearHooks();
// subsequent evaluations should now use new provider set by hook
- String noOpValue = client.getStringValue("val", defaultValue);
- assertEquals(noOpValue, defaultValue);
+ client.getStringValue("val", defaultValue);
+ assertEquals(1, provider2.getFlagEvaluations().size());
}
@Test
void setProviderAndWaitShouldPutTheProviderInReadyState() {
String domain = "domain";
- api.setProviderAndWait(domain, new TestEventsProvider());
+ api.setProviderAndWait(domain, TestProvider.builder().initsToReady());
Client client = api.getClient(domain);
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
}
@@ -146,7 +148,7 @@ void setProviderAndWaitShouldPutTheProviderInReadyState() {
@Test
void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() {
String domain = "domain";
- TestEventsProvider provider = new TestEventsProvider();
+ var provider = TestProvider.builder().initsToReady();
api.setProviderAndWait(domain, provider);
Client client = api.getClient(domain);
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
@@ -161,7 +163,7 @@ void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() {
@Test
void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() {
String domain = "domain";
- TestEventsProvider provider = new TestEventsProvider();
+ var provider = TestProvider.builder().initsToReady();
api.setProviderAndWait(domain, provider);
Client client = api.getClient(domain);
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
@@ -176,7 +178,7 @@ void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() {
@Test
void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() {
String domain = "domain";
- TestEventsProvider provider = new TestEventsProvider();
+ var provider = TestProvider.builder().initsToReady();
api.setProviderAndWait(domain, provider);
Client client = api.getClient(domain);
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
diff --git a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java
deleted file mode 100644
index 0477a725b..000000000
--- a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package dev.openfeature.sdk;
-
-class DoSomethingProvider implements FeatureProvider {
-
- static final String name = "Something";
- // Flag evaluation metadata
- static final ImmutableMetadata DEFAULT_METADATA =
- ImmutableMetadata.builder().build();
- private ImmutableMetadata flagMetadata;
-
- public DoSomethingProvider() {
- this.flagMetadata = DEFAULT_METADATA;
- }
-
- public DoSomethingProvider(ImmutableMetadata flagMetadata) {
- this.flagMetadata = flagMetadata;
- }
-
- @Override
- public Metadata getMetadata() {
- return () -> name;
- }
-
- @Override
- public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(!defaultValue)
- .flagMetadata(flagMetadata)
- .build();
- }
-
- @Override
- public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(new StringBuilder(defaultValue).reverse().toString())
- .flagMetadata(flagMetadata)
- .build();
- }
-
- @Override
- public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue * 100)
- .flagMetadata(flagMetadata)
- .build();
- }
-
- @Override
- public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue * 100)
- .flagMetadata(flagMetadata)
- .build();
- }
-
- @Override
- public ProviderEvaluation getObjectEvaluation(
- String key, Value defaultValue, EvaluationContext invocationContext) {
- return ProviderEvaluation.builder()
- .value(null)
- .flagMetadata(flagMetadata)
- .build();
- }
-}
diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java
index b232f1177..b3cd2a05d 100644
--- a/src/test/java/dev/openfeature/sdk/EventsTest.java
+++ b/src/test/java/dev/openfeature/sdk/EventsTest.java
@@ -4,9 +4,14 @@
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.*;
-
-import dev.openfeature.sdk.testutils.TestEventsProvider;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@@ -49,7 +54,8 @@ void apiInitReady() {
final Consumer handler = mockHandler();
final String name = "apiInitReady";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider =
+ TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.onProviderReady(handler);
api.setProviderAndWait(name, provider);
verify(handler, timeout(TIMEOUT).atLeastOnce()).accept(any());
@@ -64,14 +70,15 @@ void apiInitReady() {
void apiInitError() {
final Consumer handler = mockHandler();
final String name = "apiInitError";
- final String errMessage = "oh no!";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ var provider = TestProvider.builder()
+ .withName(name)
+ .initWaitsFor(INIT_DELAY)
+ .initsToError();
api.onProviderError(handler);
api.setProvider(name, provider);
- verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
- return errMessage.equals(details.getMessage());
- }));
+ verify(handler, timeout(TIMEOUT))
+ .accept(argThat(details -> name.equals(details.getProviderName())));
}
}
@@ -89,11 +96,12 @@ void apiShouldPropagateEvents() {
final Consumer handler = mockHandler();
final String name = "apiShouldPropagateEvents";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider =
+ TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
api.onProviderConfigurationChanged(handler);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
EventDetails.builder().build());
verify(handler, timeout(TIMEOUT)).accept(any());
@@ -118,7 +126,8 @@ void apiShouldSupportAllEventTypes() {
final Consumer handler3 = mockHandler();
final Consumer handler4 = mockHandler();
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider =
+ TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
api.onProviderReady(handler1);
@@ -127,8 +136,7 @@ void apiShouldSupportAllEventTypes() {
api.onProviderError(handler4);
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
- provider.mockEvent(
- eventType, ProviderEventDetails.builder().build());
+ provider.emit(eventType, ProviderEventDetails.builder().build());
});
verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any());
@@ -161,13 +169,14 @@ class ProviderEvents {
void shouldPropagateDefaultAndAnon() {
final Consumer handler = mockHandler();
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider =
+ TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
// set provider before getting a client
api.setProviderAndWait(provider);
Client client = api.getClient();
client.onProviderStale(handler);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
verify(handler, timeout(TIMEOUT)).accept(any());
}
@@ -182,13 +191,14 @@ void shouldPropagateDefaultAndNamed() {
final Consumer handler = mockHandler();
final String name = "shouldPropagateDefaultAndNamed";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider =
+ TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
// set provider before getting a client
api.setProviderAndWait(provider);
Client client = api.getClient(name);
client.onProviderStale(handler);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
verify(handler, timeout(TIMEOUT)).accept(any());
}
@@ -213,7 +223,7 @@ void initReadyProviderBefore() {
final Consumer handler = mockHandler();
final String name = "initReadyProviderBefore";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
Client client = api.getClient(name);
client.onProviderReady(handler);
// set provider after getting a client
@@ -232,7 +242,7 @@ void initReadyProviderAfter() {
final Consumer handler = mockHandler();
final String name = "initReadyProviderAfter";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
// set provider before getting a client
api.setProviderAndWait(name, provider);
Client client = api.getClient(name);
@@ -250,16 +260,13 @@ void initReadyProviderAfter() {
void initErrorProviderAfter() {
final Consumer handler = mockHandler();
final String name = "initErrorProviderAfter";
- final String errMessage = "oh no!";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToError();
Client client = api.getClient(name);
client.onProviderError(handler);
// set provider after getting a client
api.setProvider(name, provider);
- verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
- return name.equals(details.getDomain()) && errMessage.equals(details.getMessage());
- }));
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> name.equals(details.getDomain())));
}
@Test
@@ -271,15 +278,14 @@ void initErrorProviderAfter() {
void initErrorProviderBefore() {
final Consumer handler = mockHandler();
final String name = "initErrorProviderBefore";
- final String errMessage = "oh no!";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToError();
// set provider after getting a client
api.setProvider(name, provider);
Client client = api.getClient(name);
client.onProviderError(handler);
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
- return name.equals(details.getDomain()) && errMessage.equals(details.getMessage());
+ return name.equals(details.getDomain());
}));
}
}
@@ -298,13 +304,13 @@ void shouldPropagateBefore() {
final Consumer handler = mockHandler();
final String name = "shouldPropagateBefore";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
// set provider before getting a client
api.setProviderAndWait(name, provider);
Client client = api.getClient(name);
client.onProviderConfigurationChanged(handler);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
EventDetails.builder().build());
verify(handler, timeout(TIMEOUT))
@@ -322,13 +328,13 @@ void shouldPropagateAfter() {
final Consumer handler = mockHandler();
final String name = "shouldPropagateAfter";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
Client client = api.getClient(name);
client.onProviderConfigurationChanged(handler);
// set provider after getting a client
api.setProviderAndWait(name, provider);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
EventDetails.builder().build());
verify(handler, timeout(TIMEOUT))
@@ -354,7 +360,7 @@ void shouldSupportAllEventTypes() {
final Consumer handler3 = mockHandler();
final Consumer handler4 = mockHandler();
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
Client client = api.getClient(name);
@@ -364,7 +370,7 @@ void shouldSupportAllEventTypes() {
client.onProviderError(handler4);
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
- provider.mockEvent(eventType, ProviderEventDetails.builder().build());
+ provider.emit(eventType, ProviderEventDetails.builder().build());
});
ArgumentMatcher nameMatches =
(EventDetails details) -> details.getDomain().equals(name);
@@ -383,8 +389,8 @@ void shouldNotRunHandlers() {
final Consumer handler2 = mockHandler();
final String name = "shouldNotRunHandlers";
- TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
- TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+ var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
+ var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider1);
Client client = api.getClient(name);
@@ -395,12 +401,17 @@ void shouldNotRunHandlers() {
api.setProviderAndWait(name, provider2);
// wait for the new provider to be ready and make sure things are cleaned up.
- await().until(() -> provider1.isShutDown());
+ await().until(provider1::isShutdown);
// fire old event
- provider1.mockEvent(
- ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
- EventDetails.builder().build());
+ try {
+ provider1.emit(
+ ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
+ EventDetails.builder().build());
+ } catch (Exception e) {
+ // ignore this exception. When the provider is shutdown, so is the underlying ExecutorService. If new tasks
+ // are scheduled, they will be rejected and an exception will be thrown.
+ }
// a bit of waiting here, but we want to make sure these are indeed never
// called.
@@ -420,8 +431,9 @@ void otherClientHandlersShouldNotRun() {
final Consumer handlerToRun = mockHandler();
final Consumer handlerNotToRun = mockHandler();
- TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
- TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+ var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
+ var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
+
api.setProviderAndWait(name1, provider1);
api.setProviderAndWait(name2, provider2);
@@ -431,7 +443,7 @@ void otherClientHandlersShouldNotRun() {
client1.onProviderConfigurationChanged(handlerToRun);
client2.onProviderConfigurationChanged(handlerNotToRun);
- provider1.mockEvent(
+ provider1.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
@@ -449,8 +461,8 @@ void boundShouldNotRunWithDefault() {
final String name = "boundShouldNotRunWithDefault";
final Consumer handlerNotToRun = mockHandler();
- TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY);
- TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY);
+ var namedProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
+ var defaultProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(defaultProvider);
Client client = api.getClient(name);
@@ -458,10 +470,10 @@ void boundShouldNotRunWithDefault() {
api.setProviderAndWait(name, namedProvider);
// await the new provider to make sure the old one is shut down
- await().until(() -> namedProvider.getState().equals(ProviderState.READY));
+ await().until(() -> ProviderState.READY.equals(client.getProviderState()));
// fire event on default provider
- defaultProvider.mockEvent(
+ defaultProvider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
@@ -479,17 +491,17 @@ void unboundShouldRunWithDefault() {
final String name = "unboundShouldRunWithDefault";
final Consumer handlerToRun = mockHandler();
- TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY);
+ var defaultProvider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(defaultProvider);
Client client = api.getClient(name);
client.onProviderConfigurationChanged(handlerToRun);
// await the new provider to make sure the old one is shut down
- await().until(() -> defaultProvider.getState().equals(ProviderState.READY));
+ await().until(() -> ProviderState.READY.equals(client.getProviderState()));
// fire event on default provider
- defaultProvider.mockEvent(
+ defaultProvider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
@@ -509,7 +521,7 @@ void handlersRunIfOneThrows() {
final Consumer nextHandler = mockHandler();
final Consumer lastHandler = mockHandler();
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
Client client1 = api.getClient(name);
@@ -518,7 +530,7 @@ void handlersRunIfOneThrows() {
client1.onProviderConfigurationChanged(nextHandler);
client1.onProviderConfigurationChanged(lastHandler);
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
verify(errorHandler, timeout(TIMEOUT)).accept(any());
@@ -537,7 +549,7 @@ void shouldHaveAllProperties() {
final Consumer handler2 = mockHandler();
final String name = "shouldHaveAllProperties";
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
Client client = api.getClient(name);
@@ -555,7 +567,7 @@ void shouldHaveAllProperties() {
.message(message)
.build();
- provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
+ provider.emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
// both global and client handler should have all the fields.
verify(handler1, timeout(TIMEOUT)).accept(argThat((EventDetails eventDetails) -> {
@@ -582,7 +594,7 @@ void matchingReadyEventsMustRunImmediately() {
final Consumer handler = mockHandler();
// provider which is already ready
- TestEventsProvider provider = new TestEventsProvider();
+ var provider = TestProvider.builder().initsToReady();
api.setProviderAndWait(name, provider);
// should run even thought handler was added after ready
@@ -601,7 +613,7 @@ void matchingStaleEventsMustRunImmediately() {
final Consumer handler = mockHandler();
// provider which is already stale
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
Client client = api.getClient(name);
api.setProviderAndWait(name, provider);
provider.emitProviderStale(ProviderEventDetails.builder().build()).await();
@@ -622,7 +634,7 @@ void matchingErrorEventsMustRunImmediately() {
final Consumer handler = mockHandler();
// provider which is already in error
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
Client client = api.getClient(name);
api.setProviderAndWait(name, provider);
provider.emitProviderError(ProviderEventDetails.builder().build()).await();
@@ -641,14 +653,14 @@ void mustPersistAcrossChanges() {
final String name = "mustPersistAcrossChanges";
final Consumer handler = mockHandler();
- TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
- TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+ var provider1 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
+ var provider2 = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider1);
Client client = api.getClient(name);
client.onProviderConfigurationChanged(handler);
- provider1.mockEvent(
+ provider1.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
ArgumentMatcher nameMatches =
@@ -661,7 +673,7 @@ void mustPersistAcrossChanges() {
// verify that with the new provider under the same name, the handler is called
// again.
- provider2.mockEvent(
+ provider2.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
verify(handler, timeout(TIMEOUT).times(2)).accept(argThat(nameMatches));
@@ -680,7 +692,7 @@ void removedEventsShouldNotRun() {
final Consumer handler1 = mockHandler();
final Consumer handler2 = mockHandler();
- TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ var provider = TestProvider.builder().initWaitsFor(INIT_DELAY).initsToReady();
api.setProviderAndWait(name, provider);
Client client = api.getClient(name);
@@ -692,7 +704,7 @@ void removedEventsShouldNotRun() {
client.removeHandler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler2);
// emit event
- provider.mockEvent(
+ provider.emit(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
ProviderEventDetails.builder().build());
diff --git a/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java b/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java
deleted file mode 100644
index 9ebd24758..000000000
--- a/src/test/java/dev/openfeature/sdk/FatalErrorProvider.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package dev.openfeature.sdk;
-
-import dev.openfeature.sdk.exceptions.FatalError;
-import dev.openfeature.sdk.exceptions.GeneralError;
-
-public class FatalErrorProvider implements FeatureProvider {
-
- private final String name = "fatal";
-
- @Override
- public Metadata getMetadata() {
- return () -> name;
- }
-
- @Override
- public void initialize(EvaluationContext evaluationContext) throws Exception {
- throw new FatalError(); // throw a fatal error on startup (this will cause the SDK to short circuit evaluations)
- }
-
- @Override
- public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- throw new GeneralError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- throw new GeneralError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- throw new GeneralError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- throw new GeneralError(TestConstants.BROKEN_MESSAGE);
- }
-
- @Override
- public ProviderEvaluation getObjectEvaluation(
- String key, Value defaultValue, EvaluationContext invocationContext) {
- throw new GeneralError(TestConstants.BROKEN_MESSAGE);
- }
-}
diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
index 3b02b172d..82aa4e3cc 100644
--- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
@@ -1,15 +1,24 @@
package dev.openfeature.sdk;
-import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import dev.openfeature.sdk.e2e.Flag;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.fixtures.HookFixtures;
-import dev.openfeature.sdk.testutils.TestEventsProvider;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -34,7 +43,7 @@ private Client _client() {
@SneakyThrows
private Client _initializedClient() {
- TestEventsProvider provider = new TestEventsProvider();
+ var provider = TestProvider.builder().initsToReady();
provider.initialize(null);
api.setProviderAndWait(provider);
return api.getClient();
@@ -74,12 +83,12 @@ void provider() {
"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);
+ var provider = TestProvider.builder().initWaitsFor(500).initsToReady();
api.setProviderAndWait(provider);
Client client = api.getClient();
assertThat(client.getProviderState()).isEqualTo(ProviderState.READY);
- provider = new TestEventsProvider(500);
+ provider = TestProvider.builder().initWaitsFor(500).initsToReady();
String providerName = "providerAndWait";
api.setProviderAndWait(providerName, provider);
Client client2 = api.getClient(providerName);
@@ -93,10 +102,10 @@ void providerAndWait() {
"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");
+ var provider1 = TestProvider.builder().initWaitsFor(500).initsToError();
assertThrows(GeneralError.class, () -> api.setProviderAndWait(provider1));
- FeatureProvider provider2 = new TestEventsProvider(500, true, "fake error");
+ var provider2 = TestProvider.builder().initWaitsFor(500).initsToError();
String providerName = "providerAndWaitError";
assertThrows(GeneralError.class, () -> api.setProviderAndWait(providerName, provider2));
}
@@ -107,7 +116,7 @@ void providerAndWaitError() {
"The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.")
@Test
void shouldReturnNotReadyIfNotInitialized() {
- FeatureProvider provider = new TestEventsProvider(100);
+ var provider = TestProvider.builder().initWaitsFor(500).initsToReady();
String providerName = "shouldReturnNotReadyIfNotInitialized";
api.setProvider(providerName, provider);
Client client = api.getClient(providerName);
@@ -121,8 +130,9 @@ void shouldReturnNotReadyIfNotInitialized() {
text = "The API MUST provide a function for retrieving the metadata field of the configured provider.")
@Test
void provider_metadata() {
- api.setProviderAndWait(new DoSomethingProvider());
- assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name);
+ var name = "name";
+ api.setProviderAndWait(TestProvider.builder().withName(name).initsToReady());
+ assertThat(api.getProviderMetadata().getName()).isEqualTo(name);
}
@Specification(
@@ -183,57 +193,63 @@ void hookRegistration() {
"The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")
@Test
void value_flags() {
- api.setProviderAndWait(new DoSomethingProvider());
+ api.setProviderAndWait(TestProvider.builder()
+ .withFlags(
+ new Flag(FlagValueType.BOOLEAN.name(), "boolean", true),
+ new Flag(FlagValueType.STRING.name(), "string", "default"),
+ new Flag(FlagValueType.INTEGER.name(), "int", 400),
+ new Flag(FlagValueType.DOUBLE.name(), "double", 40.0),
+ new Flag(FlagValueType.OBJECT.name(), "obj", new Value()))
+ .initsToReady());
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("boolean", false));
+ assertEquals(true, c.getBooleanValue("boolean", false, new ImmutableContext()));
assertEquals(
true,
c.getBooleanValue(
- key,
+ "boolean",
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("default", c.getStringValue("string", "my-string"));
+ assertEquals("default", c.getStringValue("string", "my-string", new ImmutableContext()));
assertEquals(
- "gnirts-ym",
+ "default",
c.getStringValue(
- key,
+ "string",
"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("int", 3));
+ assertEquals(400, c.getIntegerValue("int", 3, new ImmutableContext()));
assertEquals(
400,
c.getIntegerValue(
- key,
+ "int",
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("double", .4));
+ assertEquals(40.0, c.getDoubleValue("double", .4, new ImmutableContext()));
assertEquals(
40.0,
c.getDoubleValue(
- key,
+ "double",
.4,
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
- assertEquals(null, c.getObjectValue(key, new Value()));
- assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext()));
+ assertEquals(new Value(), c.getObjectValue("obj", new Value()));
+ assertEquals(new Value(), c.getObjectValue("obj", new Value(), new ImmutableContext()));
assertEquals(
- null,
+ new Value(),
c.getObjectValue(
- key,
+ "obj",
new Value(),
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
@@ -264,68 +280,80 @@ void value_flags() {
"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() {
- api.setProviderAndWait(new DoSomethingProvider());
+ api.setProviderAndWait(TestProvider.builder()
+ .withFlags(
+ new Flag(FlagValueType.BOOLEAN.name(), "boolean", true),
+ new Flag(FlagValueType.STRING.name(), "string", "default"),
+ new Flag(FlagValueType.INTEGER.name(), "int", 400),
+ new Flag(FlagValueType.DOUBLE.name(), "double", 40.0),
+ new Flag(FlagValueType.OBJECT.name(), "obj", new Value()))
+ .initsToReady());
Client c = api.getClient();
- String key = "key";
FlagEvaluationDetails bd = FlagEvaluationDetails.builder()
- .flagKey(key)
- .value(false)
- .variant(null)
- .flagMetadata(DEFAULT_METADATA)
+ .flagKey("boolean")
+ .value(true)
+ .variant(TestProvider.DEFAULT_VARIANT)
+ .flagMetadata(ImmutableMetadata.EMPTY)
+ .reason(Reason.STATIC.name())
.build();
- assertEquals(bd, c.getBooleanDetails(key, true));
- assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext()));
+ assertEquals(bd, c.getBooleanDetails("boolean", false));
+ assertEquals(bd, c.getBooleanDetails("boolean", false, new ImmutableContext()));
assertEquals(
bd,
c.getBooleanDetails(
- key,
- true,
+ "boolean",
+ false,
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails sd = FlagEvaluationDetails.builder()
- .flagKey(key)
- .value("tset")
- .variant(null)
- .flagMetadata(DEFAULT_METADATA)
+ .flagKey("string")
+ .value("default")
+ .variant(TestProvider.DEFAULT_VARIANT)
+ .flagMetadata(ImmutableMetadata.EMPTY)
+ .reason(Reason.STATIC.name())
.build();
- assertEquals(sd, c.getStringDetails(key, "test"));
- assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext()));
+ assertEquals(sd, c.getStringDetails("string", "test"));
+ assertEquals(sd, c.getStringDetails("string", "test", new ImmutableContext()));
assertEquals(
sd,
c.getStringDetails(
- key,
+ "string",
"test",
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails id = FlagEvaluationDetails.builder()
- .flagKey(key)
+ .flagKey("int")
.value(400)
- .flagMetadata(DEFAULT_METADATA)
+ .flagMetadata(ImmutableMetadata.EMPTY)
+ .reason(Reason.STATIC.name())
+ .variant(TestProvider.DEFAULT_VARIANT)
.build();
- assertEquals(id, c.getIntegerDetails(key, 4));
- assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext()));
+ assertEquals(id, c.getIntegerDetails("int", 4));
+ assertEquals(id, c.getIntegerDetails("int", 4, new ImmutableContext()));
assertEquals(
id,
c.getIntegerDetails(
- key,
+ "int",
4,
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
FlagEvaluationDetails dd = FlagEvaluationDetails.builder()
- .flagKey(key)
+ .flagKey("double")
.value(40.0)
- .flagMetadata(DEFAULT_METADATA)
+ .flagMetadata(ImmutableMetadata.EMPTY)
+ .reason(Reason.STATIC.name())
+ .variant(TestProvider.DEFAULT_VARIANT)
.build();
- assertEquals(dd, c.getDoubleDetails(key, .4));
- assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext()));
+ assertEquals(dd, c.getDoubleDetails("double", .4));
+ assertEquals(dd, c.getDoubleDetails("double", .4, new ImmutableContext()));
assertEquals(
dd,
c.getDoubleDetails(
- key,
+ "double",
.4,
new ImmutableContext(),
FlagEvaluationOptions.builder().build()));
@@ -371,7 +399,7 @@ void hooks() {
"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() {
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c = api.getClient();
boolean defaultValue = false;
assertFalse(c.getBooleanValue("key", defaultValue));
@@ -400,7 +428,7 @@ void broken_provider() {
"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_withDetails() throws InterruptedException {
- api.setProviderAndWait(new AlwaysBrokenWithDetailsProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c = api.getClient();
boolean defaultValue = false;
assertFalse(c.getBooleanValue("key", defaultValue));
@@ -416,7 +444,7 @@ void broken_provider_withDetails() throws InterruptedException {
text = "Methods, functions, or operations on the client SHOULD NOT write log messages.")
@Test
void log_on_error() throws NotImplementedException {
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c = api.getClient();
FlagEvaluationDetails result = c.getBooleanDetails("test", false);
@@ -435,7 +463,7 @@ void clientMetadata() {
assertNull(c.getMetadata().getDomain());
String domainName = "test domain";
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c2 = api.getClient(domainName);
assertEquals(domainName, c2.getMetadata().getName());
@@ -448,7 +476,7 @@ void clientMetadata() {
"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() {
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c = api.getClient();
FlagEvaluationDetails result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR.toString(), result.getReason());
@@ -460,7 +488,7 @@ void reason_is_error_when_there_are_errors() {
"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() {
- api.setProviderAndWait(new DoSomethingProvider(null));
+ api.setProviderAndWait(TestProvider.builder().allowUnknownFlags().initsToReady());
Client c = api.getClient();
FlagEvaluationDetails result = c.getBooleanDetails("test", false);
assertNotNull(result.getFlagMetadata());
@@ -471,7 +499,7 @@ void flag_metadata_passed() {
void api_context() {
String contextKey = "some-key";
String contextValue = "some-value";
- DoSomethingProvider provider = spy(new DoSomethingProvider());
+ var provider = spy(TestProvider.builder().allowUnknownFlags().initsToReady());
api.setProviderAndWait(provider);
Map attributes = new HashMap<>();
@@ -498,7 +526,7 @@ void api_context() {
"Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.")
@Test
void multi_layer_context_merges_correctly() {
- DoSomethingProvider provider = spy(new DoSomethingProvider());
+ var provider = spy(TestProvider.builder().allowUnknownFlags().initsToReady());
api.setProviderAndWait(provider);
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
api.setTransactionContextPropagator(transactionContextPropagator);
@@ -686,7 +714,7 @@ public void after(
text = "The API SHOULD have a method for setting a transaction context propagator.")
@Test
void setting_transaction_context_propagator() {
- DoSomethingProvider provider = new DoSomethingProvider();
+ var provider = spy(TestProvider.builder().initsToReady());
api.setProviderAndWait(provider);
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
@@ -700,7 +728,7 @@ void setting_transaction_context_propagator() {
"The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.")
@Test
void setting_transaction_context() {
- DoSomethingProvider provider = new DoSomethingProvider();
+ var provider = spy(TestProvider.builder().initsToReady());
api.setProviderAndWait(provider);
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java
index 56d88dfba..163007120 100644
--- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java
@@ -19,7 +19,7 @@
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.fixtures.HookFixtures;
-import dev.openfeature.sdk.testutils.TestEventsProvider;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -176,7 +176,7 @@ void optional_properties() {
@Test
void before_runs_ahead_of_evaluation() {
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client client = api.getClient();
Hook evalHook = mockBooleanHook();
@@ -261,39 +261,41 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() {
void hook_eval_order() {
List evalOrder = new ArrayList<>();
- api.setProviderAndWait("evalOrder", new TestEventsProvider() {
- public List getProviderHooks() {
- return Collections.singletonList(new BooleanHook() {
-
- @Override
- public Optional before(HookContext ctx, Map hints) {
- evalOrder.add("provider before");
- return null;
- }
-
- @Override
- public void after(
- HookContext ctx,
- FlagEvaluationDetails details,
- Map hints) {
- evalOrder.add("provider after");
- }
-
- @Override
- public void error(HookContext ctx, Exception error, Map hints) {
- evalOrder.add("provider error");
- }
-
- @Override
- public void finallyAfter(
- HookContext ctx,
- FlagEvaluationDetails details,
- Map hints) {
- evalOrder.add("provider finally");
- }
- });
- }
- });
+ api.setProviderAndWait(
+ "evalOrder",
+ TestProvider.builder()
+ .withHook(new BooleanHook() {
+
+ @Override
+ public Optional before(
+ HookContext ctx, Map hints) {
+ evalOrder.add("provider before");
+ return null;
+ }
+
+ @Override
+ public void after(
+ HookContext ctx,
+ FlagEvaluationDetails details,
+ Map hints) {
+ evalOrder.add("provider after");
+ }
+
+ @Override
+ public void error(HookContext ctx, Exception error, Map hints) {
+ evalOrder.add("provider error");
+ }
+
+ @Override
+ public void finallyAfter(
+ HookContext ctx,
+ FlagEvaluationDetails details,
+ Map hints) {
+ evalOrder.add("provider finally");
+ }
+ })
+ .allowUnknownFlags()
+ .initsToReady());
api.addHooks(new BooleanHook() {
@Override
public Optional before(HookContext ctx, Map hints) {
@@ -412,7 +414,7 @@ void error_stops_before() {
doThrow(RuntimeException.class).when(h).before(any(), any());
Hook h2 = mockBooleanHook();
- api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider());
+ api.setProviderAndWait(TestProvider.builder().withExceptionOnFlagEvaluation());
Client c = api.getClient();
c.getBooleanDetails(
@@ -435,7 +437,7 @@ void error_stops_after() {
doThrow(RuntimeException.class).when(h).after(any(), any(), any());
Hook h2 = mockBooleanHook();
- Client c = getClient(TestEventsProvider.newInitializedTestEventsProvider());
+ Client c = getClient(TestProvider.builder().allowUnknownFlags().initsToReady());
c.getBooleanDetails(
"key",
@@ -540,7 +542,7 @@ void flag_eval_hook_order() {
void error_hooks__before() {
Hook hook = mockBooleanHook();
doThrow(RuntimeException.class).when(hook).before(any(), any());
- Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
+ Client client = getClient(TestProvider.builder().initsToReady());
Boolean value = client.getBooleanValue(
"key",
false,
@@ -558,7 +560,7 @@ void error_hooks__before() {
void error_hooks__after() {
Hook hook = mockBooleanHook();
doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
- Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
+ Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady());
client.getBooleanValue(
"key",
false,
@@ -573,7 +575,7 @@ void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
Hook hook = mockBooleanHook();
doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
String flagKey = "test-flag-key";
- Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
+ Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady());
client.getBooleanValue(
flagKey,
true,
@@ -598,7 +600,7 @@ void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
@Test
void shortCircuit_flagResolution_runsHooksWithAllFields() {
String domain = "shortCircuit_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails";
- api.setProvider(domain, new FatalErrorProvider());
+ api.setProvider(domain, TestProvider.builder().initsToFatal());
Hook hook = mockBooleanHook();
String flagKey = "test-flag-key";
@@ -618,7 +620,7 @@ void shortCircuit_flagResolution_runsHooksWithAllFields() {
void successful_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
Hook hook = mockBooleanHook();
String flagKey = "test-flag-key";
- Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
+ Client client = getClient(TestProvider.builder().allowUnknownFlags().initsToReady());
client.getBooleanValue(
flagKey,
true,
@@ -631,7 +633,7 @@ void successful_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
FlagEvaluationDetails evaluationDetails = captor.getValue();
assertThat(evaluationDetails).isNotNull();
assertThat(evaluationDetails.getErrorCode()).isNull();
- assertThat(evaluationDetails.getReason()).isEqualTo("DEFAULT");
+ assertThat(evaluationDetails.getReason()).isEqualTo(Reason.STATIC.name());
assertThat(evaluationDetails.getVariant()).isEqualTo("Passed in default");
assertThat(evaluationDetails.getFlagKey()).isEqualTo(flagKey);
assertThat(evaluationDetails.getFlagMetadata())
@@ -779,7 +781,7 @@ void first_error_broken() {
private Client getClient(FeatureProvider provider) {
if (provider == null) {
- api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider());
+ api.setProviderAndWait(TestProvider.builder().initsToReady());
} else {
api.setProviderAndWait(provider);
}
diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
index 66fd06d55..2fdb4e3f0 100644
--- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
+++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
@@ -8,9 +8,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import dev.openfeature.sdk.providers.memory.InMemoryProvider;
-import dev.openfeature.sdk.testutils.TestEventsProvider;
-import java.util.Collections;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.HashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -28,7 +26,7 @@ void setupTest() {
@Test
void namedProviderTest() {
- FeatureProvider provider = new NoOpProvider();
+ var provider = TestProvider.builder().initsToReady();
api.setProviderAndWait("namedProviderTest", provider);
assertThat(provider.getMetadata().getName())
@@ -42,18 +40,19 @@ void namedProviderTest() {
@Test
void namedProviderOverwrittenTest() {
String domain = "namedProviderOverwrittenTest";
- FeatureProvider provider1 = new NoOpProvider();
- FeatureProvider provider2 = new DoSomethingProvider();
+ var provider1 = TestProvider.builder().withName("provider1").initsToReady();
+ var provider2 = TestProvider.builder().withName("provider2").initsToReady();
api.setProviderAndWait(domain, provider1);
api.setProviderAndWait(domain, provider2);
- assertThat(api.getProvider(domain).getMetadata().getName()).isEqualTo(DoSomethingProvider.name);
+ assertThat(api.getProvider(domain).getMetadata().getName())
+ .isEqualTo(provider2.getMetadata().getName());
}
@Test
void providerToMultipleNames() throws Exception {
- FeatureProvider inMemAsEventingProvider = new InMemoryProvider(Collections.EMPTY_MAP);
- FeatureProvider noOpAsNonEventingProvider = new NoOpProvider();
+ var inMemAsEventingProvider = TestProvider.builder().initsToReady();
+ var noOpAsNonEventingProvider = TestProvider.builder().initsToReady();
// register same provider for multiple names & as default provider
api.setProviderAndWait(inMemAsEventingProvider);
@@ -95,8 +94,8 @@ void setEvaluationContextShouldAllowChaining() {
@Test
void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception {
String domain = "namedProviderOverwrittenTest";
- FeatureProvider provider1 = new NoOpProvider();
- FeatureProvider provider2 = new TestEventsProvider();
+ var provider1 = TestProvider.builder().initsToReady();
+ var provider2 = TestProvider.builder().initsToReady();
api.setProviderAndWait(domain, provider1);
api.setProviderAndWait(domain, provider2);
diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
index 88ebfaf9d..91509bd45 100644
--- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
+++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
@@ -10,7 +10,7 @@
import dev.openfeature.sdk.exceptions.FatalError;
import dev.openfeature.sdk.fixtures.HookFixtures;
-import dev.openfeature.sdk.testutils.TestEventsProvider;
+import dev.openfeature.sdk.testutils.testProvider.TestProvider;
import java.util.HashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -42,10 +42,11 @@ void reset_logs() {
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
OpenFeatureAPI api = new OpenFeatureAPI();
api.setProviderAndWait(
- "shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider());
+ "shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext",
+ TestProvider.builder().initsToReady());
Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext");
client.addHooks(mockBooleanHook(), mockStringHook());
- FlagEvaluationDetails actual = client.getBooleanDetails("feature key", Boolean.FALSE);
+ FlagEvaluationDetails actual = client.getBooleanDetails("feature key", Boolean.TRUE);
assertThat(actual.getValue()).isTrue();
// I dislike this, but given the mocking tools available, there's no way that I know of to say "no errors were
@@ -83,7 +84,7 @@ void setEvaluationContextShouldAllowChaining() {
@Test
@DisplayName("Should not call evaluation methods when the provider has state FATAL")
void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() {
- FeatureProvider provider = new TestEventsProvider(100, true, "fake fatal", true);
+ var provider = TestProvider.builder().initWaitsFor(100).initsToFatal();
OpenFeatureAPI api = new OpenFeatureAPI();
Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState");
@@ -98,13 +99,15 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() {
@Test
@DisplayName("Should not call evaluation methods when the provider has state NOT_READY")
void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() {
- FeatureProvider provider = new TestEventsProvider(5000);
+ var awaitable = new Awaitable();
+ var provider = TestProvider.builder().initWaitsFor(awaitable).initsToReady();
OpenFeatureAPI api = new OpenFeatureAPI();
api.setProvider("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState", provider);
Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState");
FlagEvaluationDetails details = client.getBooleanDetails("key", true);
assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_NOT_READY);
+ awaitable.wakeup();
}
@ParameterizedTest
@@ -112,13 +115,9 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() {
@DisplayName("Should support usage of HookData with/without error")
void shouldSupportUsageOfHookData(boolean isError) {
OpenFeatureAPI api = new OpenFeatureAPI();
- FeatureProvider provider;
- if (isError) {
- provider = new AlwaysBrokenWithExceptionProvider();
- } else {
- provider = new DoSomethingProvider();
- }
- api.setProviderAndWait("shouldSupportUsageOfHookData", provider);
+ api.setProviderAndWait(
+ "shouldSupportUsageOfHookData",
+ TestProvider.builder().allowUnknownFlags(!isError).initsToReady());
var testHook = new TestHookWithData("test-data");
api.addHooks(testHook);
diff --git a/src/test/java/dev/openfeature/sdk/e2e/Flag.java b/src/test/java/dev/openfeature/sdk/e2e/Flag.java
index 2c4ffdb57..7e3a11c90 100644
--- a/src/test/java/dev/openfeature/sdk/e2e/Flag.java
+++ b/src/test/java/dev/openfeature/sdk/e2e/Flag.java
@@ -1,13 +1,21 @@
package dev.openfeature.sdk.e2e;
+import dev.openfeature.sdk.ImmutableMetadata;
+
public class Flag {
- public String name;
- public Object defaultValue;
- public String type;
+ public final String name;
+ public final Object defaultValue;
+ public final String type;
+ public final ImmutableMetadata flagMetadata;
public Flag(String type, String name, Object defaultValue) {
+ this(type, name, defaultValue, ImmutableMetadata.EMPTY);
+ }
+
+ public Flag(String type, String name, Object defaultValue, ImmutableMetadata flagMetadata) {
this.name = name;
this.defaultValue = defaultValue;
this.type = type;
+ this.flagMetadata = flagMetadata;
}
}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java
deleted file mode 100644
index 7cd2ea318..000000000
--- a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package dev.openfeature.sdk.testutils;
-
-import dev.openfeature.sdk.EvaluationContext;
-import dev.openfeature.sdk.EventProvider;
-import dev.openfeature.sdk.Metadata;
-import dev.openfeature.sdk.ProviderEvaluation;
-import dev.openfeature.sdk.ProviderEvent;
-import dev.openfeature.sdk.ProviderEventDetails;
-import dev.openfeature.sdk.Reason;
-import dev.openfeature.sdk.Value;
-import dev.openfeature.sdk.exceptions.FatalError;
-import dev.openfeature.sdk.exceptions.GeneralError;
-import lombok.SneakyThrows;
-
-public class TestEventsProvider extends EventProvider {
- public static final String PASSED_IN_DEFAULT = "Passed in default";
-
- private boolean initError = false;
- private String initErrorMessage;
- private boolean shutDown = false;
- private int initTimeoutMs = 0;
- private String name = "test";
- private Metadata metadata = () -> name;
- private boolean isFatalInitError = false;
-
- public TestEventsProvider() {}
-
- public TestEventsProvider(int initTimeoutMs) {
- this.initTimeoutMs = initTimeoutMs;
- }
-
- public TestEventsProvider(int initTimeoutMs, boolean initError, String initErrorMessage) {
- this.initTimeoutMs = initTimeoutMs;
- this.initError = initError;
- this.initErrorMessage = initErrorMessage;
- }
-
- public TestEventsProvider(int initTimeoutMs, boolean initError, String initErrorMessage, boolean fatal) {
- this.initTimeoutMs = initTimeoutMs;
- this.initError = initError;
- this.initErrorMessage = initErrorMessage;
- this.isFatalInitError = fatal;
- }
-
- @SneakyThrows
- public static TestEventsProvider newInitializedTestEventsProvider() {
- TestEventsProvider provider = new TestEventsProvider();
- provider.initialize(null);
- return provider;
- }
-
- public void mockEvent(ProviderEvent event, ProviderEventDetails details) {
- emit(event, details);
- }
-
- public boolean isShutDown() {
- return this.shutDown;
- }
-
- @Override
- public void shutdown() {
- this.shutDown = true;
- }
-
- @Override
- public void initialize(EvaluationContext evaluationContext) throws Exception {
- // wait half the TIMEOUT, otherwise some init/errors can be fired before we add handlers
- Thread.sleep(initTimeoutMs);
- if (this.initError) {
- if (this.isFatalInitError) {
- throw new FatalError(initErrorMessage);
- }
- throw new GeneralError(initErrorMessage);
- }
- }
-
- @Override
- public Metadata getMetadata() {
- return this.metadata;
- }
-
- @Override
- public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue)
- .variant(PASSED_IN_DEFAULT)
- .reason(Reason.DEFAULT.toString())
- .build();
- }
-
- @Override
- public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue)
- .variant(PASSED_IN_DEFAULT)
- .reason(Reason.DEFAULT.toString())
- .build();
- }
-
- @Override
- public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue)
- .variant(PASSED_IN_DEFAULT)
- .reason(Reason.DEFAULT.toString())
- .build();
- }
-
- @Override
- public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- return ProviderEvaluation.builder()
- .value(defaultValue)
- .variant(PASSED_IN_DEFAULT)
- .reason(Reason.DEFAULT.toString())
- .build();
- }
-
- @Override
- public ProviderEvaluation getObjectEvaluation(
- String key, Value defaultValue, EvaluationContext invocationContext) {
- return ProviderEvaluation.builder()
- .value(defaultValue)
- .variant(PASSED_IN_DEFAULT)
- .reason(Reason.DEFAULT.toString())
- .build();
- }
-}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java b/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java
new file mode 100644
index 000000000..61860a050
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/testProvider/FlagEvaluation.java
@@ -0,0 +1,16 @@
+package dev.openfeature.sdk.testutils.testProvider;
+
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.FlagValueType;
+
+public class FlagEvaluation {
+ public final String flagKey;
+ public final FlagValueType flagType;
+ public final EvaluationContext evaluationContext;
+
+ public FlagEvaluation(String flagKey, FlagValueType flagType, EvaluationContext evaluationContext) {
+ this.flagKey = flagKey;
+ this.flagType = flagType;
+ this.evaluationContext = evaluationContext;
+ }
+}
diff --git a/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java b/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java
new file mode 100644
index 000000000..383ade483
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/testProvider/TestProvider.java
@@ -0,0 +1,396 @@
+package dev.openfeature.sdk.testutils.testProvider;
+
+import dev.openfeature.sdk.*;
+import dev.openfeature.sdk.e2e.Flag;
+import dev.openfeature.sdk.exceptions.FatalError;
+import dev.openfeature.sdk.exceptions.FlagNotFoundError;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class TestProvider extends EventProvider {
+ public static final String DEFAULT_VARIANT = "Passed in default";
+
+ private final String name;
+ private final List hooks;
+ private final int initDelay;
+ private final Awaitable initWaitsFor;
+ private final Map flags;
+ private final boolean fatalOnInit;
+ private final boolean errorOnInit;
+ private final boolean errorsOnFlagEvaluation;
+ private final ErrorCode errorCode;
+ private final String errorMessage;
+ private final RuntimeException throwable;
+ private final ConcurrentLinkedQueue flagEvaluations = new ConcurrentLinkedQueue<>();
+ private final AtomicBoolean isShutdown = new AtomicBoolean(false);
+ private final boolean allowAnyFlag;
+
+ private TestProvider(
+ String name,
+ List hooks,
+ int initDelay,
+ Awaitable initWaitsFor,
+ boolean allowAnyFlag,
+ Map flags,
+ boolean errorsOnFlagEvaluation,
+ boolean errorOnInit,
+ ErrorCode errorCode,
+ String errorMessage,
+ RuntimeException throwable,
+ boolean fatalOnInit) {
+ this.name = name == null ? "TestProvider" : name;
+ this.hooks = hooks;
+ this.initDelay = initDelay;
+ this.initWaitsFor = initWaitsFor;
+ this.allowAnyFlag = allowAnyFlag;
+ this.flags = flags;
+ this.errorsOnFlagEvaluation = errorsOnFlagEvaluation;
+ this.errorOnInit = errorOnInit;
+ this.errorCode = errorCode == null ? ErrorCode.GENERAL : errorCode;
+ this.errorMessage = errorMessage == null ? "Test error" : errorMessage;
+ this.throwable = throwable;
+ this.fatalOnInit = fatalOnInit;
+ }
+
+ public List getFlagEvaluations() {
+ return new ArrayList<>(flagEvaluations);
+ }
+
+ @Override
+ public List getProviderHooks() {
+ return hooks;
+ }
+
+ @Override
+ public void initialize(EvaluationContext evaluationContext) throws Exception {
+ if (initWaitsFor != null) {
+ initWaitsFor.await();
+ } else if (initDelay > 0) {
+ var end = System.currentTimeMillis() + initDelay;
+ long delta = initDelay;
+ while (delta > 0) {
+ try {
+ Thread.sleep(delta);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ delta = end - System.currentTimeMillis();
+ }
+ }
+
+ if (fatalOnInit) {
+ throw new FatalError("TestProvider is set to fatal state, thus will throw on init");
+ }
+ if (errorOnInit) {
+ throw new RuntimeException("TestProvider is set to error state, thus will throw on init");
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ super.shutdown();
+ isShutdown.set(true);
+ }
+
+ public boolean isShutdown() {
+ return isShutdown.get();
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return () -> name;
+ }
+
+ private ProviderEvaluation getEvaluation(
+ String key, T defaultValue, FlagValueType flagType, Class clazz, EvaluationContext evaluationContext) {
+ flagEvaluations.add(new FlagEvaluation(key, flagType, evaluationContext));
+ if (throwable != null) {
+ throw throwable;
+ }
+ var builder = ProviderEvaluation.builder();
+ if (errorsOnFlagEvaluation) {
+ return builder.errorMessage(errorMessage).errorCode(errorCode).build();
+ }
+ if (allowAnyFlag) {
+ return builder.reason(Reason.STATIC.name())
+ .value(clazz.cast(defaultValue))
+ .flagMetadata(ImmutableMetadata.EMPTY)
+ .variant(DEFAULT_VARIANT)
+ .build();
+ }
+ var flag = flags.get(key);
+ if (flag == null) {
+ return builder.errorCode(ErrorCode.FLAG_NOT_FOUND)
+ .errorMessage("Flag not found")
+ .build();
+ }
+ if (flagType.name().equals(flag.type)) {
+ return builder.reason(Reason.STATIC.name())
+ .value(clazz.cast(flag.defaultValue))
+ .flagMetadata(flag.flagMetadata)
+ .variant(DEFAULT_VARIANT)
+ .build();
+ }
+ return builder.errorCode(ErrorCode.TYPE_MISMATCH)
+ .errorMessage("Flag type mismatch")
+ .flagMetadata(flag.flagMetadata)
+ .build();
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return getEvaluation(key, defaultValue, FlagValueType.BOOLEAN, Boolean.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
+ return getEvaluation(key, defaultValue, FlagValueType.STRING, String.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
+ return getEvaluation(key, defaultValue, FlagValueType.INTEGER, Integer.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
+ return getEvaluation(key, defaultValue, FlagValueType.DOUBLE, Double.class, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
+ return getEvaluation(key, defaultValue, FlagValueType.OBJECT, Value.class, ctx);
+ }
+
+ public interface PostInit {
+ TestProvider initsToReady();
+
+ TestProvider initsToError(boolean isError);
+
+ default TestProvider initsToError() {
+ return initsToError(true);
+ }
+
+ TestProvider initsToFatal(boolean isFatal);
+
+ default TestProvider initsToFatal() {
+ return initsToFatal(true);
+ }
+
+ TestProvider withExceptionOnFlagEvaluation(RuntimeException runtimeException);
+
+ default TestProvider withExceptionOnFlagEvaluation() {
+ return withExceptionOnFlagEvaluation(new FlagNotFoundError(TestConstants.BROKEN_MESSAGE));
+ }
+ }
+
+ public interface InitConfig {
+ PostInit initWaitsFor(int millis);
+
+ PostInit initWaitsFor(Awaitable awaitable);
+ }
+
+ public interface WithFlags extends InitConfig, PostInit {}
+
+ public interface FlagConfig {
+ FlagConfig withFlag(Flag flag);
+
+ WithFlags withFlags(Flag... flags);
+
+ WithFlags withFlags(Iterable flags);
+
+ default WithFlags errorsOnFlagEvaluation() {
+ return errorsOnFlagEvaluation(true);
+ }
+
+ WithFlags errorsOnFlagEvaluation(boolean error);
+
+ WithFlags errorsOnFlagEvaluation(ErrorCode errorCode);
+
+ WithFlags errorsOnFlagEvaluation(ErrorCode errorCode, String errorMessage);
+
+ default WithFlags allowUnknownFlags() {
+ return allowUnknownFlags(true);
+ }
+
+ WithFlags allowUnknownFlags(boolean allowEveryRequestedFlag);
+ }
+
+ public interface WithHooks extends FlagConfig, WithFlags {}
+
+ public interface HookConfig {
+ Builder withHook(Hook hook);
+
+ WithHooks withHooks(Hook... hooks);
+
+ WithHooks withHooks(Iterable hooks);
+ }
+
+ public interface WithName extends WithHooks, HookConfig {}
+
+ public interface NameConfig {
+ WithName withName(String name);
+ }
+
+ public interface Builder extends WithName, NameConfig {}
+
+ public static Builder builder() {
+ return new TestProviderBuilder();
+ }
+
+ public static class TestProviderBuilder implements Builder {
+ private final List hooks = new ArrayList<>();
+ private final Map flags = new HashMap<>();
+ private Awaitable initWaitsFor;
+ private int initDelay = 0;
+ private boolean errorsOnFlagEvaluation;
+ private ErrorCode errorCode;
+ private String errorMessage;
+ private RuntimeException runtimeException;
+ private boolean errorOnInit = false;
+ private boolean fatalOnInit = false;
+ private boolean allowAnyFlag = false;
+ private String name;
+
+ @Override
+ public WithName withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public Builder withHook(Hook hook) {
+ this.hooks.add(hook);
+ return this;
+ }
+
+ @Override
+ public WithHooks withHooks(Hook... hooks) {
+ for (int i = 0; i < hooks.length; i++) {
+ this.hooks.add(hooks[i]);
+ }
+ return this;
+ }
+
+ @Override
+ public WithHooks withHooks(Iterable hooks) {
+ for (Hook hook : hooks) {
+ this.hooks.add(hook);
+ }
+ return this;
+ }
+
+ @Override
+ public WithFlags errorsOnFlagEvaluation(boolean error) {
+ this.errorsOnFlagEvaluation = error;
+ return this;
+ }
+
+ @Override
+ public WithFlags errorsOnFlagEvaluation(ErrorCode errorCode) {
+ this.errorsOnFlagEvaluation = true;
+ this.errorCode = errorCode;
+ return this;
+ }
+
+ @Override
+ public WithFlags errorsOnFlagEvaluation(ErrorCode errorCode, String errorMessage) {
+ this.errorsOnFlagEvaluation = true;
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ return this;
+ }
+
+ @Override
+ public WithFlags allowUnknownFlags(boolean allowEveryRequestedFlag) {
+ this.allowAnyFlag = allowEveryRequestedFlag;
+ return this;
+ }
+
+ @Override
+ public FlagConfig withFlag(Flag flag) {
+ flags.put(flag.name, flag);
+ return this;
+ }
+
+ @Override
+ public WithFlags withFlags(Flag... flags) {
+ for (Flag flag : flags) {
+ this.flags.put(flag.name, flag);
+ }
+ return this;
+ }
+
+ @Override
+ public WithFlags withFlags(Iterable flags) {
+ for (Flag flag : flags) {
+ this.flags.put(flag.name, flag);
+ }
+ return this;
+ }
+
+ @Override
+ public PostInit initWaitsFor(int millis) {
+ initDelay = millis;
+ initWaitsFor = null;
+ return this;
+ }
+
+ @Override
+ public PostInit initWaitsFor(Awaitable awaitable) {
+ initDelay = 0;
+ initWaitsFor = awaitable;
+ return this;
+ }
+
+ @Override
+ public TestProvider initsToReady() {
+ errorOnInit = false;
+ return build();
+ }
+
+ @Override
+ public TestProvider initsToError(boolean isError) {
+ this.errorOnInit = isError;
+ return build();
+ }
+
+ @Override
+ public TestProvider initsToError() {
+ errorOnInit = true;
+ return build();
+ }
+
+ @Override
+ public TestProvider initsToFatal(boolean isFatal) {
+ this.fatalOnInit = isFatal;
+ return build();
+ }
+
+ @Override
+ public TestProvider withExceptionOnFlagEvaluation(RuntimeException runtimeException) {
+ this.runtimeException = runtimeException;
+ return build();
+ }
+
+ private TestProvider build() {
+ return new TestProvider(
+ name,
+ hooks,
+ initDelay,
+ initWaitsFor,
+ allowAnyFlag,
+ flags,
+ errorsOnFlagEvaluation,
+ errorOnInit,
+ errorCode,
+ errorMessage,
+ runtimeException,
+ fatalOnInit);
+ }
+ }
+}