diff --git a/pom.xml b/pom.xml index a6a54e5ae..bbc1b8db7 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,13 @@ + + com.tngtech.archunit + archunit-junit5 + 1.4.0 + test + + org.mockito mockito-core @@ -242,12 +249,14 @@ com.github.spotbugs:* org.junit* + com.tngtech.archunit* org.simplify4u:slf4j2-mock* com.google.guava* io.cucumber* org.junit* + com.tngtech.archunit* com.google.code.findbugs* com.github.spotbugs* org.simplify4u:slf4j-mock-common:* diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 9175a7cd7..bd60cc78a 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -29,7 +29,7 @@ public class OpenFeatureAPI implements EventBus { protected OpenFeatureAPI() { apiHooks = new ArrayList<>(); - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); transactionContextPropagator = new NoOpTransactionContextPropagator(); } @@ -333,7 +333,7 @@ public void shutdown() { providerRepository.shutdown(); eventSupport.shutdown(); - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); } } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 66f25f60a..a393d83e8 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -507,7 +507,7 @@ public Client onProviderStale(Consumer handler) { */ @Override public Client on(ProviderEvent event, Consumer handler) { - OpenFeatureAPI.getInstance().addHandler(domain, event, handler); + openfeatureApi.addHandler(domain, event, handler); return this; } @@ -516,7 +516,7 @@ public Client on(ProviderEvent event, Consumer handler) { */ @Override public Client removeHandler(ProviderEvent event, Consumer handler) { - OpenFeatureAPI.getInstance().removeHandler(domain, event, handler); + openfeatureApi.removeHandler(domain, event, handler); return this; } } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index bec866820..ab024a750 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -28,6 +28,11 @@ class ProviderRepository { return thread; }); private final Object registerStateManagerLock = new Object(); + private final OpenFeatureAPI openFeatureAPI; + + public ProviderRepository(OpenFeatureAPI openFeatureAPI) { + this.openFeatureAPI = openFeatureAPI; + } FeatureProviderStateManager getFeatureProviderStateManager() { return defaultStateManger.get(); @@ -205,7 +210,7 @@ private void initializeProvider( FeatureProviderStateManager oldManager) { try { if (ProviderState.NOT_READY.equals(newManager.getState())) { - newManager.initialize(OpenFeatureAPI.getInstance().getEvaluationContext()); + newManager.initialize(openFeatureAPI.getEvaluationContext()); afterInit.accept(newManager.getProvider()); } shutDownOld(oldManager, afterShutdown); diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java index 8f304eaac..bd0ac2c21 100644 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithDetailsProvider.java @@ -1,14 +1,12 @@ package dev.openfeature.sdk; -import dev.openfeature.sdk.exceptions.FlagNotFoundError; - public class AlwaysBrokenWithDetailsProvider implements FeatureProvider { + private final String name = "always broken with details"; + @Override public Metadata getMetadata() { - return () -> { - throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE); - }; + return () -> name; } @Override diff --git a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java similarity index 94% rename from src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java rename to src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java index 2f214d8ac..0ad09db29 100644 --- a/src/test/java/dev/openfeature/sdk/AlwaysBrokenProvider.java +++ b/src/test/java/dev/openfeature/sdk/AlwaysBrokenWithExceptionProvider.java @@ -2,7 +2,7 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; -public class AlwaysBrokenProvider implements FeatureProvider { +public class AlwaysBrokenWithExceptionProvider implements FeatureProvider { private final String name = "always broken"; diff --git a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java index cd7e8b295..beadf7aad 100644 --- a/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java +++ b/src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java @@ -2,17 +2,16 @@ import static org.junit.jupiter.api.Assertions.*; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import org.junit.jupiter.api.Test; class ClientProviderMappingTest { @Test void clientProviderTest() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); - FeatureProviderTestUtils.setFeatureProvider("client1", new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider("client2", new NoOpProvider()); + api.setProviderAndWait("client1", new DoSomethingProvider()); + api.setProviderAndWait("client2", new NoOpProvider()); Client c1 = api.getClient("client1"); Client c2 = api.getClient("client2"); diff --git a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java index aacf09169..32fa605c2 100644 --- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java +++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.verify; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.Arrays; import java.util.HashMap; @@ -16,14 +15,20 @@ import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class DeveloperExperienceTest implements HookFixtures { transient String flagKey = "mykey"; + private OpenFeatureAPI api; + + @BeforeEach + public void setUp() throws Exception { + api = new OpenFeatureAPI(); + } @Test void simpleBooleanFlag() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Boolean retval = client.getBooleanValue(flagKey, false); @@ -34,7 +39,6 @@ void simpleBooleanFlag() { void clientHooks() { Hook exampleHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(exampleHook); @@ -48,7 +52,6 @@ void evalHooks() { Hook clientHook = mockBooleanHook(); Hook evalHook = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); client.addHooks(clientHook); @@ -69,7 +72,6 @@ void evalHooks() { @Test void providingContext() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new TestEventsProvider()); Client client = api.getClient(); Map attributes = new HashMap<>(); @@ -86,8 +88,7 @@ void providingContext() { @Test void brokenProvider() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client client = api.getClient(); FlagEvaluationDetails retval = client.getBooleanDetails(flagKey, false); assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode()); @@ -99,6 +100,9 @@ void brokenProvider() { @Test void providerLockedPerTransaction() { + final String defaultValue = "string-value"; + final OpenFeatureAPI api = new OpenFeatureAPI(); + class MutatingHook implements Hook { @Override @@ -106,16 +110,14 @@ 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) { - FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider()); return Optional.empty(); } } - final String defaultValue = "string-value"; - final OpenFeatureAPI api = OpenFeatureAPI.getInstance(); final Client client = api.getClient(); - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); api.addHooks(new MutatingHook()); // if provider is changed during an evaluation transaction it should proceed with the original provider @@ -132,7 +134,6 @@ public Optional before(HookContext ctx, Map hints) { @Test void setProviderAndWaitShouldPutTheProviderInReadyState() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(domain, new TestEventsProvider()); Client client = api.getClient(domain); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); @@ -145,7 +146,6 @@ void setProviderAndWaitShouldPutTheProviderInReadyState() { @Test void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); @@ -161,7 +161,6 @@ void shouldPutTheProviderInStateErrorAfterEmittingErrorEvent() { @Test void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); @@ -177,7 +176,6 @@ void shouldPutTheProviderInStateStaleAfterEmittingStaleEvent() { @Test void shouldPutTheProviderInStateReadyAfterEmittingReadyEvent() { String domain = "domain"; - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); TestEventsProvider provider = new TestEventsProvider(); api.setProviderAndWait(domain, provider); Client client = api.getClient(domain); diff --git a/src/test/java/dev/openfeature/sdk/EventProviderTest.java b/src/test/java/dev/openfeature/sdk/EventProviderTest.java index a159877f0..ebf8901cb 100644 --- a/src/test/java/dev/openfeature/sdk/EventProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/EventProviderTest.java @@ -28,7 +28,7 @@ void setup() { @AfterAll public static void resetDefaultProvider() { - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + new OpenFeatureAPI().setProviderAndWait(new NoOpProvider()); } @Test @@ -91,7 +91,7 @@ void doesNotThrowWhenOnEmitSame() { @DisplayName("should not deadlock on emit called during emit") void doesNotDeadlockOnEmitStackedCalls() { TestStackedEmitCallsProvider provider = new TestStackedEmitCallsProvider(); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + new OpenFeatureAPI().setProviderAndWait(provider); } static class TestEventProvider extends EventProvider { diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java index e59024652..157c0bafe 100644 --- a/src/test/java/dev/openfeature/sdk/EventsTest.java +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -7,11 +7,11 @@ import static org.mockito.Mockito.*; import dev.openfeature.sdk.testutils.TestEventsProvider; -import io.cucumber.java.AfterAll; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -21,10 +21,11 @@ class EventsTest { private static final int TIMEOUT = 500; private static final int INIT_DELAY = TIMEOUT / 2; + private OpenFeatureAPI api; - @AfterAll - public static void resetDefaultProvider() { - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + @BeforeEach + public void setUp() throws Exception { + api = new OpenFeatureAPI(); } @Nested @@ -49,8 +50,8 @@ void apiInitReady() { final String name = "apiInitReady"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().onProviderReady(handler); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.onProviderReady(handler); + api.setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()).accept(any()); } @@ -66,8 +67,8 @@ void apiInitError() { final String errMessage = "oh no!"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - OpenFeatureAPI.getInstance().onProviderError(handler); - OpenFeatureAPI.getInstance().setProvider(name, provider); + api.onProviderError(handler); + api.setProvider(name, provider); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { return errMessage.equals(details.getMessage()); })); @@ -89,8 +90,8 @@ void apiShouldPropagateEvents() { final String name = "apiShouldPropagateEvents"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); + api.setProviderAndWait(name, provider); + api.onProviderConfigurationChanged(handler); provider.mockEvent( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, @@ -118,12 +119,12 @@ void apiShouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); - OpenFeatureAPI.getInstance().onProviderReady(handler1); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().onProviderStale(handler3); - OpenFeatureAPI.getInstance().onProviderError(handler4); + api.onProviderReady(handler1); + api.onProviderConfigurationChanged(handler2); + api.onProviderStale(handler3); + api.onProviderError(handler4); Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> { provider.mockEvent( @@ -162,8 +163,8 @@ void shouldPropagateDefaultAndAnon() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - Client client = OpenFeatureAPI.getInstance().getClient(); + api.setProviderAndWait(provider); + Client client = api.getClient(); client.onProviderStale(handler); provider.mockEvent( @@ -183,8 +184,8 @@ void shouldPropagateDefaultAndNamed() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(provider); + Client client = api.getClient(name); client.onProviderStale(handler); provider.mockEvent( @@ -213,10 +214,10 @@ void initReadyProviderBefore() { final String name = "initReadyProviderBefore"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderReady(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(argThat(details -> details.getDomain().equals(name))); } @@ -233,8 +234,8 @@ void initReadyProviderAfter() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderReady(handler); verify(handler, timeout(TIMEOUT).atLeastOnce()) .accept(argThat(details -> details.getDomain().equals(name))); @@ -252,10 +253,10 @@ void initErrorProviderAfter() { final String errMessage = "oh no!"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderError(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); + api.setProvider(name, provider); verify(handler, timeout(TIMEOUT)).accept(argThat(details -> { return name.equals(details.getDomain()) && errMessage.equals(details.getMessage()); })); @@ -274,8 +275,8 @@ void initErrorProviderBefore() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage); // set provider after getting a client - OpenFeatureAPI.getInstance().setProvider(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + 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()); @@ -299,8 +300,8 @@ void shouldPropagateBefore() { TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); // set provider before getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); provider.mockEvent( @@ -322,10 +323,10 @@ void shouldPropagateAfter() { final String name = "shouldPropagateAfter"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); // set provider after getting a client - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); provider.mockEvent( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, @@ -354,8 +355,8 @@ void shouldSupportAllEventTypes() { final Consumer handler4 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); client.onProviderReady(handler1); client.onProviderConfigurationChanged(handler2); @@ -384,14 +385,14 @@ void shouldNotRunHandlers() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider1); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + api.onProviderConfigurationChanged(handler1); client.onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); + api.setProviderAndWait(name, provider2); // wait for the new provider to be ready and make sure things are cleaned up. await().until(() -> provider1.isShutDown()); @@ -421,11 +422,11 @@ void otherClientHandlersShouldNotRun() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name1, provider1); - OpenFeatureAPI.getInstance().setProviderAndWait(name2, provider2); + api.setProviderAndWait(name1, provider1); + api.setProviderAndWait(name2, provider2); - Client client1 = OpenFeatureAPI.getInstance().getClient(name1); - Client client2 = OpenFeatureAPI.getInstance().getClient(name2); + Client client1 = api.getClient(name1); + Client client2 = api.getClient(name2); client1.onProviderConfigurationChanged(handlerToRun); client2.onProviderConfigurationChanged(handlerNotToRun); @@ -450,11 +451,11 @@ void boundShouldNotRunWithDefault() { TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + api.setProviderAndWait(defaultProvider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handlerNotToRun); - OpenFeatureAPI.getInstance().setProviderAndWait(name, namedProvider); + api.setProviderAndWait(name, namedProvider); // await the new provider to make sure the old one is shut down await().until(() -> namedProvider.getState().equals(ProviderState.READY)); @@ -465,7 +466,7 @@ void boundShouldNotRunWithDefault() { ProviderEventDetails.builder().build()); verify(handlerNotToRun, after(TIMEOUT).never()).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -479,9 +480,9 @@ void unboundShouldRunWithDefault() { final Consumer handlerToRun = mockHandler(); TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(defaultProvider); + api.setProviderAndWait(defaultProvider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handlerToRun); // await the new provider to make sure the old one is shut down @@ -493,7 +494,7 @@ void unboundShouldRunWithDefault() { ProviderEventDetails.builder().build()); verify(handlerToRun, timeout(TIMEOUT)).accept(any()); - OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); } @Test @@ -509,9 +510,9 @@ void handlersRunIfOneThrows() { final Consumer lastHandler = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); - Client client1 = OpenFeatureAPI.getInstance().getClient(name); + Client client1 = api.getClient(name); client1.onProviderConfigurationChanged(errorHandler); client1.onProviderConfigurationChanged(nextHandler); @@ -537,11 +538,11 @@ void shouldHaveAllProperties() { final String name = "shouldHaveAllProperties"; TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1); + api.onProviderConfigurationChanged(handler1); client.onProviderConfigurationChanged(handler2); List flagsChanged = Arrays.asList("flag"); @@ -582,10 +583,10 @@ void matchingReadyEventsMustRunImmediately() { // provider which is already ready TestEventsProvider provider = new TestEventsProvider(); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); + api.setProviderAndWait(name, provider); // should run even thought handler was added after ready - Client client = OpenFeatureAPI.getInstance().getClient(name); + Client client = api.getClient(name); client.onProviderReady(handler); verify(handler, timeout(TIMEOUT)).accept(any()); } @@ -598,7 +599,6 @@ void matchingReadyEventsMustRunImmediately() { void matchingStaleEventsMustRunImmediately() { final String name = "matchingEventsMustRunImmediately"; final Consumer handler = mockHandler(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already stale TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); @@ -620,7 +620,6 @@ void matchingStaleEventsMustRunImmediately() { void matchingErrorEventsMustRunImmediately() { final String name = "matchingEventsMustRunImmediately"; final Consumer handler = mockHandler(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); // provider which is already in error TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); @@ -629,6 +628,7 @@ void matchingErrorEventsMustRunImmediately() { provider.emitProviderError(ProviderEventDetails.builder().build()); assertThat(client.getProviderState()).isEqualTo(ProviderState.ERROR); + verify(handler, never()).accept(any()); // should run even though handler was added after error client.onProviderError(handler); verify(handler, timeout(TIMEOUT)).accept(any()); @@ -644,8 +644,8 @@ void mustPersistAcrossChanges() { TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY); TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider1); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider1); + Client client = api.getClient(name); client.onProviderConfigurationChanged(handler); provider1.mockEvent( @@ -657,7 +657,7 @@ void mustPersistAcrossChanges() { verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches)); // wait for the new provider to be ready. - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider2); + api.setProviderAndWait(name, provider2); // verify that with the new provider under the same name, the handler is called // again. @@ -681,14 +681,14 @@ void removedEventsShouldNotRun() { final Consumer handler2 = mockHandler(); TestEventsProvider provider = new TestEventsProvider(INIT_DELAY); - OpenFeatureAPI.getInstance().setProviderAndWait(name, provider); - Client client = OpenFeatureAPI.getInstance().getClient(name); + api.setProviderAndWait(name, provider); + Client client = api.getClient(name); // attached handlers - OpenFeatureAPI.getInstance().onProviderStale(handler1); + api.onProviderStale(handler1); client.onProviderConfigurationChanged(handler2); - OpenFeatureAPI.getInstance().removeHandler(ProviderEvent.PROVIDER_STALE, handler1); + api.removeHandler(ProviderEvent.PROVIDER_STALE, handler1); client.removeHandler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler2); // emit event diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index 2ad88d328..3b02b172d 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -9,7 +9,6 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.HashMap; import java.util.List; @@ -29,7 +28,7 @@ class FlagEvaluationSpecTest implements HookFixtures { private OpenFeatureAPI api; private Client _client() { - FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); + api.setProviderAndWait(new NoOpProvider()); return api.getClient(); } @@ -37,18 +36,13 @@ private Client _client() { private Client _initializedClient() { TestEventsProvider provider = new TestEventsProvider(); provider.initialize(null); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); return api.getClient(); } @BeforeEach void getApiInstance() { - api = OpenFeatureAPI.getInstance(); - } - - @AfterEach - void reset_ctx() { - api.setEvaluationContext(null); + api = new OpenFeatureAPI(); } @BeforeEach @@ -62,15 +56,6 @@ 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 = @@ -78,7 +63,7 @@ void global_singleton() { @Test void provider() { FeatureProvider mockProvider = mock(FeatureProvider.class); - FeatureProviderTestUtils.setFeatureProvider(mockProvider); + api.setProviderAndWait(mockProvider); assertThat(api.getProvider()).isEqualTo(mockProvider); } @@ -90,13 +75,13 @@ void provider() { @Test void providerAndWait() { FeatureProvider provider = new TestEventsProvider(500); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); provider = new TestEventsProvider(500); String providerName = "providerAndWait"; - OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider); + api.setProviderAndWait(providerName, provider); Client client2 = api.getClient(providerName); assertThat(client2.getProviderState()).isEqualTo(ProviderState.READY); } @@ -124,8 +109,8 @@ void providerAndWaitError() { void shouldReturnNotReadyIfNotInitialized() { FeatureProvider provider = new TestEventsProvider(100); String providerName = "shouldReturnNotReadyIfNotInitialized"; - OpenFeatureAPI.getInstance().setProvider(providerName, provider); - Client client = OpenFeatureAPI.getInstance().getClient(providerName); + api.setProvider(providerName, provider); + Client client = api.getClient(providerName); FlagEvaluationDetails details = client.getBooleanDetails("return_error_when_not_initialized", false); assertEquals(ErrorCode.PROVIDER_NOT_READY, details.getErrorCode()); assertEquals(Reason.ERROR.toString(), details.getReason()); @@ -136,7 +121,7 @@ void shouldReturnNotReadyIfNotInitialized() { text = "The API MUST provide a function for retrieving the metadata field of the configured provider.") @Test void provider_metadata() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name); } @@ -198,7 +183,7 @@ void hookRegistration() { "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.") @Test void value_flags() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; @@ -279,7 +264,7 @@ 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() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); + api.setProviderAndWait(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; @@ -386,7 +371,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() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -414,8 +399,8 @@ void broken_provider() { 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_withDetails() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenWithDetailsProvider()); + void broken_provider_withDetails() throws InterruptedException { + api.setProviderAndWait(new AlwaysBrokenWithDetailsProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); @@ -431,7 +416,7 @@ void broken_provider_withDetails() { text = "Methods, functions, or operations on the client SHOULD NOT write log messages.") @Test void log_on_error() throws NotImplementedException { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); @@ -450,7 +435,7 @@ void clientMetadata() { assertNull(c.getMetadata().getDomain()); String domainName = "test domain"; - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c2 = api.getClient(domainName); assertEquals(domainName, c2.getMetadata().getName()); @@ -463,7 +448,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() { - FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertEquals(Reason.ERROR.toString(), result.getReason()); @@ -475,7 +460,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() { - FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null)); + api.setProviderAndWait(new DoSomethingProvider(null)); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertNotNull(result.getFlagMetadata()); @@ -487,7 +472,7 @@ void api_context() { String contextKey = "some-key"; String contextValue = "some-value"; DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Map attributes = new HashMap<>(); attributes.put(contextKey, new Value(contextValue)); @@ -514,7 +499,7 @@ void api_context() { @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = spy(new DoSomethingProvider()); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); Hook hook = spy(new Hook() { @@ -702,7 +687,7 @@ public void after( @Test void setting_transaction_context_propagator() { DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); @@ -716,7 +701,7 @@ void setting_transaction_context_propagator() { @Test void setting_transaction_context() { DoSomethingProvider provider = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index d6247c649..4b08510dd 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -18,7 +18,6 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.fixtures.HookFixtures; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.ArrayList; import java.util.Arrays; @@ -28,16 +27,18 @@ import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; class HookSpecTest implements HookFixtures { - @AfterEach - void emptyApiHooks() { - // it's a singleton. Don't pollute each test. - OpenFeatureAPI.getInstance().clearHooks(); + + private OpenFeatureAPI api; + + @BeforeEach + void setUp() { + this.api = new OpenFeatureAPI(); } @Specification( @@ -163,7 +164,7 @@ void optional_properties() { .type(FlagValueType.INTEGER) .ctx(new ImmutableContext()) .defaultValue(1) - .clientMetadata(OpenFeatureAPI.getInstance().getClient().getMetadata()) + .clientMetadata(api.getClient().getMetadata()) .build(); } @@ -173,8 +174,8 @@ void optional_properties() { "The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.") @Test void before_runs_ahead_of_evaluation() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new AlwaysBrokenProvider()); + + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client client = api.getClient(); Hook evalHook = mockBooleanHook(); @@ -216,8 +217,7 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() { .errorMessage(errorMessage) .build()); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider("errorHookMustRun", provider); + api.setProviderAndWait("errorHookMustRun", provider); Client client = api.getClient("errorHookMustRun"); client.getBooleanValue( "key", @@ -259,7 +259,7 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() { @Test void hook_eval_order() { List evalOrder = new ArrayList<>(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait("evalOrder", new TestEventsProvider() { public List getProviderHooks() { return Collections.singletonList(new BooleanHook() { @@ -411,8 +411,7 @@ void error_stops_before() { doThrow(RuntimeException.class).when(h).before(any(), any()); Hook h2 = mockBooleanHook(); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProviderAndWait(new AlwaysBrokenProvider()); + api.setProviderAndWait(new AlwaysBrokenWithExceptionProvider()); Client c = api.getClient(); c.getBooleanDetails( @@ -516,8 +515,7 @@ void flag_eval_hook_order() { .thenReturn(ProviderEvaluation.builder().value(true).build()); InOrder order = inOrder(hook, provider); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); client.getBooleanValue( "key", @@ -695,8 +693,7 @@ void mergeHappensCorrectly() { when(provider.getBooleanEvaluation(any(), any(), any())) .thenReturn(ProviderEvaluation.builder().value(true).build()); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); Client client = api.getClient(); client.getBooleanValue( "key", @@ -761,11 +758,10 @@ void first_error_broken() { } private Client getClient(FeatureProvider provider) { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); if (provider == null) { - FeatureProviderTestUtils.setFeatureProvider(TestEventsProvider.newInitializedTestEventsProvider()); + api.setProviderAndWait(TestEventsProvider.newInitializedTestEventsProvider()); } else { - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); } return api.getClient(); } diff --git a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java index 3353f5644..4bcd73127 100644 --- a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java @@ -17,10 +17,12 @@ class InitializeBehaviorSpecTest { private static final String DOMAIN_NAME = "mydomain"; + private OpenFeatureAPI api; @BeforeEach void setupTest() { - OpenFeatureAPI.getInstance().setProvider(new NoOpProvider()); + this.api = new OpenFeatureAPI(); + api.setProvider(new NoOpProvider()); } @Nested @@ -37,7 +39,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagE FeatureProvider featureProvider = mock(FeatureProvider.class); doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); - OpenFeatureAPI.getInstance().setProvider(featureProvider); + api.setProvider(featureProvider); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -55,8 +57,7 @@ void shouldCatchExceptionThrownByTheProviderOnInitialization() throws Exception doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); doThrow(TestException.class).when(featureProvider).initialize(any()); - assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(featureProvider)) - .doesNotThrowAnyException(); + assertThatCode(() -> api.setProvider(featureProvider)).doesNotThrowAnyException(); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -77,7 +78,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItFor FeatureProvider featureProvider = mock(FeatureProvider.class); doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); - OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider); + api.setProvider(DOMAIN_NAME, featureProvider); verify(featureProvider, timeout(1000)).initialize(any()); } @@ -95,8 +96,7 @@ void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() throws doReturn(ProviderState.NOT_READY).when(featureProvider).getState(); doThrow(TestException.class).when(featureProvider).initialize(any()); - assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider)) - .doesNotThrowAnyException(); + assertThatCode(() -> api.setProvider(DOMAIN_NAME, featureProvider)).doesNotThrowAnyException(); verify(featureProvider, timeout(1000)).initialize(any()); } diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java similarity index 99% rename from src/test/java/dev/openfeature/sdk/LockingTest.java rename to src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java index 4b7af5530..ad86f4bc3 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.parallel.Isolated; @Isolated() -class LockingTest { +class LockingSingeltonTest { private static OpenFeatureAPI api; private OpenFeatureClient client; diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java new file mode 100644 index 000000000..dd9916eed --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPISingeltonTest.java @@ -0,0 +1,17 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +class OpenFeatureAPISingeltonTest { + + @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()); + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index 63145ecb6..e8e8b27b0 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.verify; import dev.openfeature.sdk.providers.memory.InMemoryProvider; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.Collections; import java.util.HashMap; @@ -23,13 +22,13 @@ class OpenFeatureAPITest { @BeforeEach void setupTest() { - api = OpenFeatureAPI.getInstance(); + api = new OpenFeatureAPI(); } @Test void namedProviderTest() { FeatureProvider provider = new NoOpProvider(); - FeatureProviderTestUtils.setFeatureProvider("namedProviderTest", provider); + api.setProviderAndWait("namedProviderTest", provider); assertThat(provider.getMetadata().getName()) .isEqualTo(api.getProviderMetadata("namedProviderTest").getName()); @@ -44,14 +43,10 @@ void namedProviderOverwrittenTest() { String domain = "namedProviderOverwrittenTest"; FeatureProvider provider1 = new NoOpProvider(); FeatureProvider provider2 = new DoSomethingProvider(); - FeatureProviderTestUtils.setFeatureProvider(domain, provider1); - FeatureProviderTestUtils.setFeatureProvider(domain, provider2); - - assertThat(OpenFeatureAPI.getInstance() - .getProvider(domain) - .getMetadata() - .getName()) - .isEqualTo(DoSomethingProvider.name); + api.setProviderAndWait(domain, provider1); + api.setProviderAndWait(domain, provider2); + + assertThat(api.getProvider(domain).getMetadata().getName()).isEqualTo(DoSomethingProvider.name); } @Test @@ -60,17 +55,17 @@ void providerToMultipleNames() throws Exception { FeatureProvider noOpAsNonEventingProvider = new NoOpProvider(); // register same provider for multiple names & as default provider - OpenFeatureAPI.getInstance().setProviderAndWait(inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientA", inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientB", inMemAsEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientC", noOpAsNonEventingProvider); - OpenFeatureAPI.getInstance().setProviderAndWait("clientD", noOpAsNonEventingProvider); - - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider()); - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientA")); - assertEquals(inMemAsEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientB")); - assertEquals(noOpAsNonEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientC")); - assertEquals(noOpAsNonEventingProvider, OpenFeatureAPI.getInstance().getProvider("clientD")); + api.setProviderAndWait(inMemAsEventingProvider); + api.setProviderAndWait("clientA", inMemAsEventingProvider); + api.setProviderAndWait("clientB", inMemAsEventingProvider); + api.setProviderAndWait("clientC", noOpAsNonEventingProvider); + api.setProviderAndWait("clientD", noOpAsNonEventingProvider); + + assertEquals(inMemAsEventingProvider, api.getProvider()); + assertEquals(inMemAsEventingProvider, api.getProvider("clientA")); + assertEquals(inMemAsEventingProvider, api.getProvider("clientB")); + assertEquals(noOpAsNonEventingProvider, api.getProvider("clientC")); + assertEquals(noOpAsNonEventingProvider, api.getProvider("clientD")); } @Test @@ -101,23 +96,20 @@ void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception { String domain = "namedProviderOverwrittenTest"; FeatureProvider provider1 = new NoOpProvider(); FeatureProvider provider2 = new TestEventsProvider(); - FeatureProviderTestUtils.setFeatureProvider(domain, provider1); - FeatureProviderTestUtils.setFeatureProvider(domain, provider2); + api.setProviderAndWait(domain, provider1); + api.setProviderAndWait(domain, provider2); provider2.initialize(null); - assertThat(OpenFeatureAPI.getInstance().getClient(domain).getProviderState()) - .isEqualTo(ProviderState.READY); + assertThat(api.getClient(domain).getProviderState()).isEqualTo(ProviderState.READY); } @Test void featureProviderTrackIsCalled() throws Exception { FeatureProvider featureProvider = mock(FeatureProvider.class); - FeatureProviderTestUtils.setFeatureProvider(featureProvider); + api.setProviderAndWait(featureProvider); - OpenFeatureAPI.getInstance() - .getClient() - .track("track-event", new ImmutableContext(), new MutableTrackingEventDetails(22.2f)); + api.getClient().track("track-event", new ImmutableContext(), new MutableTrackingEventDetails(22.2f)); verify(featureProvider).initialize(any()); verify(featureProvider).getMetadata(); diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java new file mode 100644 index 000000000..f33c5b4d7 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITestUtil.java @@ -0,0 +1,10 @@ +package dev.openfeature.sdk; + +public class OpenFeatureAPITestUtil { + + private OpenFeatureAPITestUtil() {} + + public static OpenFeatureAPI createAPI() { + return new OpenFeatureAPI(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index 4f4d32004..97a1417a1 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -38,7 +38,7 @@ void reset_logs() { @Test @DisplayName("should not throw exception if hook has different type argument than hookContext") void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); api.setProviderAndWait( "shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext", new DoSomethingProvider()); Client client = api.getClient("shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext"); @@ -82,7 +82,7 @@ void setEvaluationContextShouldAllowChaining() { @DisplayName("Should not call evaluation methods when the provider has state FATAL") void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { FeatureProvider provider = new TestEventsProvider(100, true, "fake fatal", true); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState"); assertThrows( @@ -97,7 +97,7 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { @DisplayName("Should not call evaluation methods when the provider has state NOT_READY") void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() { FeatureProvider provider = new TestEventsProvider(5000); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI api = new OpenFeatureAPI(); api.setProvider("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState", provider); Client client = api.getClient("shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState"); FlagEvaluationDetails details = client.getBooleanDetails("key", true); diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java index 98652635d..7041df5c1 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java @@ -35,7 +35,7 @@ class ProviderRepositoryTest { @BeforeEach void setupTest() { - providerRepository = new ProviderRepository(); + providerRepository = new ProviderRepository(new OpenFeatureAPI()); } @Nested diff --git a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java index e7caf9274..1bb7d4b62 100644 --- a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java @@ -1,6 +1,5 @@ package dev.openfeature.sdk; -import static dev.openfeature.sdk.testutils.FeatureProviderTestUtils.setFeatureProvider; import static org.mockito.Mockito.*; import dev.openfeature.sdk.fixtures.ProviderFixture; @@ -15,9 +14,19 @@ class ShutdownBehaviorSpecTest { private String DOMAIN = "myDomain"; + private OpenFeatureAPI api; + + void setFeatureProvider(FeatureProvider featureProvider) { + api.setProviderAndWait(featureProvider); + } + + void setFeatureProvider(String domain, FeatureProvider featureProvider) { + api.setProviderAndWait(domain, featureProvider); + } @BeforeEach void resetFeatureProvider() { + api = new OpenFeatureAPI(); setFeatureProvider(new NoOpProvider()); } @@ -110,7 +119,6 @@ void mustShutdownAllProvidersOnShuttingDownApi() { FeatureProvider namedProvider = ProviderFixture.createMockedProvider(); setFeatureProvider(defaultProvider); setFeatureProvider(DOMAIN, namedProvider); - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); synchronized (OpenFeatureAPI.class) { api.shutdown(); @@ -125,15 +133,14 @@ void mustShutdownAllProvidersOnShuttingDownApi() { @Test @DisplayName("once shutdown is complete, api must be ready to use again") void apiIsReadyToUseAfterShutdown() { - final OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance(); NoOpProvider p1 = new NoOpProvider(); - openFeatureAPI.setProvider(p1); + api.setProvider(p1); - openFeatureAPI.shutdown(); + api.shutdown(); NoOpProvider p2 = new NoOpProvider(); - openFeatureAPI.setProvider(p2); + api.setProvider(p2); } } } diff --git a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java index a8f6e30f3..ba3543745 100644 --- a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java @@ -15,7 +15,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import dev.openfeature.sdk.fixtures.ProviderFixture; -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import java.util.HashMap; import java.util.Map; import lombok.SneakyThrows; @@ -29,7 +28,7 @@ class TrackingSpecTest { @BeforeEach void getApiInstance() { - api = OpenFeatureAPI.getInstance(); + api = new OpenFeatureAPI(); client = api.getClient(); } @@ -116,7 +115,7 @@ void contextsGetMerged() { client.setEvaluationContext(clCtx); FeatureProvider provider = ProviderFixture.createMockedProvider(); - FeatureProviderTestUtils.setFeatureProvider(provider); + api.setProviderAndWait(provider); client.track("event", new MutableContext().add("my-key", "final"), new MutableTrackingEventDetails(0.0f)); @@ -170,8 +169,7 @@ void eventDetails() { .add("my-struct", new Value(new MutableTrackingEventDetails())); assertEquals(expectedMap, details.asMap()); - assertThatCode(() -> OpenFeatureAPI.getInstance() - .getClient() + assertThatCode(() -> api.getClient() .track("tracking-event-name", new ImmutableContext(), new MutableTrackingEventDetails())) .doesNotThrowAnyException(); @@ -188,8 +186,7 @@ void eventDetails() { ImmutableTrackingEventDetails immutableDetails = new ImmutableTrackingEventDetails(2, expectedMap); assertEquals(expectedImmutable, immutableDetails.asMap()); - assertThatCode(() -> OpenFeatureAPI.getInstance() - .getClient() + assertThatCode(() -> api.getClient() .track("tracking-event-name", new ImmutableContext(), new ImmutableTrackingEventDetails())) .doesNotThrowAnyException(); } diff --git a/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java b/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java new file mode 100644 index 000000000..8bf8b2888 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/arch/ArchitectureTest.java @@ -0,0 +1,27 @@ +package dev.openfeature.sdk.arch; + +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses(packages = "dev.openfeature.sdk") +public class ArchitectureTest { + + @ArchTest + public static final ArchRule avoidGetInstances = noClasses() + .that() + .resideOutsideOfPackages("..benchmark", "..e2e.*") + .and() + .haveSimpleNameNotEndingWith("SingeltonTest") + .should() + .callMethodWhere(describe( + "Avoid Internal usage of OpenFeatureAPI.GetInstances", + // Target method may not reside in class annotated with BusinessException + methodCall -> + methodCall.getTarget().getOwner().getFullName().equals("dev.openfeature.sdk.OpenFeatureAPI") + // And target method may not have the static modifier + && methodCall.getTarget().getName().equals("getInstance"))); +} diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index 86782b397..4d2a8b287 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -12,6 +12,7 @@ import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.OpenFeatureAPITestUtil; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; @@ -25,18 +26,20 @@ class InMemoryProviderTest { - private static Client client; + private Client client; - private static InMemoryProvider provider; + private InMemoryProvider provider; + private OpenFeatureAPI api; @SneakyThrows @BeforeEach void beforeEach() { Map> flags = buildFlags(); provider = spy(new InMemoryProvider(flags)); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> {}); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - client = OpenFeatureAPI.getInstance().getClient(); + api = OpenFeatureAPITestUtil.createAPI(); + api.onProviderConfigurationChanged(eventDetails -> {}); + api.setProviderAndWait(provider); + client = api.getClient(); provider.updateFlags(flags); provider.updateFlag( "addedFlag", @@ -107,8 +110,8 @@ void emitChangedFlagsOnlyIfThereAreChangedFlags() { Consumer handler = mock(Consumer.class); Map> flags = buildFlags(); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + api.onProviderConfigurationChanged(handler); + api.setProviderAndWait(provider); provider.updateFlags(flags); diff --git a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java b/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java deleted file mode 100644 index c9ad77d89..000000000 --- a/src/test/java/dev/openfeature/sdk/testutils/FeatureProviderTestUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import static org.awaitility.Awaitility.await; - -import dev.openfeature.sdk.FeatureProvider; -import dev.openfeature.sdk.OpenFeatureAPI; -import java.time.Duration; -import java.util.function.Function; -import lombok.experimental.UtilityClass; - -// todo check the need of this utility class as we now have setProviderAndWait capability -@UtilityClass -public class FeatureProviderTestUtils { - - public static void setFeatureProvider(FeatureProvider provider) { - OpenFeatureAPI.getInstance().setProvider(provider); - waitForProviderInitializationComplete(OpenFeatureAPI::getProvider, provider); - } - - private static void waitForProviderInitializationComplete( - Function extractor, FeatureProvider provider) { - await().pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(1)) - .until(() -> extractor.apply(OpenFeatureAPI.getInstance()).equals(provider)); - } - - public static void setFeatureProvider(String domain, FeatureProvider provider) { - OpenFeatureAPI.getInstance().setProvider(domain, provider); - waitForProviderInitializationComplete(api -> api.getProvider(domain), provider); - } -}