diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ba8048..631548b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,11 +7,6 @@ workflows: test: jobs: - build-linux - - test-linux: - name: Java 8 - Linux - OpenJDK - docker-image: cimg/openjdk:8.0 - requires: - - build-linux - test-linux: name: Java 11 - Linux - OpenJDK docker-image: cimg/openjdk:11.0 @@ -36,7 +31,7 @@ workflows: jobs: build-linux: docker: - - image: cimg/openjdk:8.0 + - image: cimg/openjdk:11.0 steps: - checkout - run: java -version @@ -81,7 +76,7 @@ jobs: packaging: docker: - - image: cimg/openjdk:8.0 + - image: cimg/openjdk:11.0 steps: - run: java -version - run: sudo apt-get install make -y -q diff --git a/README.md b/README.md index afed7b9..295f7cd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Your project will need compatible versions of the LaunchDarkly Server-Side SDK f Example gradle dependencies: ```groovy -implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)' -implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)' +implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)' +implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)' ``` ### Installation @@ -54,8 +54,7 @@ import com.launchdarkly.openfeature.serverprovider.Provider; public class Main { public static void main(String[] args) { - LDClient ldClient = new LDClient("my-sdk-key"); - OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient)); + OpenFeatureAPI.getInstance().setProvider(new Provider("my-sdk-key")); // Refer to OpenFeature documentation for getting a client and performing evaluations. } @@ -86,6 +85,16 @@ There are several other attributes which have special functionality within a sin - A key of `anonymous`. Must be a boolean value. [Equivalent to the 'anonymous' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#anonymous(boolean)) - A key of `name`. Must be a string. [Equivalent to the 'name' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#name(java.lang.String)) +### Initialization and Shutdown + +The LaunchDarkly supports Initialization and Shutdown using the OpenFeature API. The provider begins initialization as soon as it is constructed, and the underlying LaunchDarkly SDK will block execution based on the configured start wait time. If you wish to defer the blocking behavior, then you can use the `startWait` function when building the `LDConfig`. + +OpenFeature will report when the provider is ready, and additionally the `setProviderAndWait` function of the OpenFeature +API can be used to wait until the provider is ready, or it has encountered a permanent error. + +It the provider has been shutdown, because the OpenFeature API has been shutdown, or because the provider was no longer in use by the OpenFeature API, then the underlying LaunchDarkly SDK will be closed. +This is an important consideration if you are using the `getLdClient` method of the provider to access the underlying SDK instance. + ### Examples #### A single user context diff --git a/build.gradle b/build.gradle index 45da11f..3844789 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ java { repositories { mavenLocal() mavenCentral() + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: + maven { url "https://oss.sonatype.org/content/groups/public/" } } test { @@ -41,6 +43,15 @@ checkstyle { checkstyleTest.enabled = false } +task generateJava(type: Copy) { + // This updates Version.java + from 'src/templates/java' + into "src/main/java" + filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [VERSION: version.toString()]) +} + +compileJava.dependsOn 'generateJava' + publishing { publications { mavenJava(MavenPublication) { @@ -111,11 +122,19 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:23.0' - implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)' - implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)' + implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)' + + implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)' // Use JUnit test framework - testImplementation 'junit:junit:4.12' + testImplementation(platform('org.junit:junit-bom:5.10.0')) + testImplementation('org.junit.jupiter:junit-jupiter') testImplementation "org.mockito:mockito-core:3.+" } +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java index 4e913f8..68540d5 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java @@ -90,7 +90,7 @@ private String getTargetingKey(String targetingKey, Value keyAsValue) { targetingKey = !Objects.equals(targetingKey, "") ? targetingKey : keyAsValue.asString(); } - if (targetingKey == null || targetingKey.equals("")) { + if (targetingKey == null || targetingKey.isEmpty()) { logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + "must be a string."); } return targetingKey; diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 9d6cf53..df3af64 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -1,32 +1,36 @@ package com.launchdarkly.openfeature.serverprovider; -import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; -import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import dev.openfeature.sdk.*; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.concurrent.TimeoutException; + /** * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java * with OpenFeature. *

- *import dev.openfeature.sdk.OpenFeatureAPI;
- *import com.launchdarkly.sdk.server.LDClient;
+ * import dev.openfeature.sdk.OpenFeatureAPI;
  *
- *public class Main {
+ * public class Main {
  *  public static void main(String[] args) {
- *    LDClient ldClient = new LDClient("my-sdk-key");
- *    OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
+ *    OpenFeatureAPI.getInstance().setProvider(new Provider("fake-key"));
  *
  *    // Refer to OpenFeature documentation for getting a client and performing evaluations.
  *  }
- *}
+ * }
  * 
*/ -public class Provider implements FeatureProvider { +public class Provider extends EventProvider { private static final class ProviderMetaData implements Metadata { @Override public String getName() { @@ -43,39 +47,38 @@ public String getName() { private final LDClientInterface client; + private ProviderState state = ProviderState.NOT_READY; + /** - * Create a provider with the given LaunchDarkly client and provider configuration. - *

-     * // Using the provider with a custom log level.
-     * new Provider(ldclient, ProviderConfiguration
-     *     .builder()
-     *     .logging(Components.logging().level(LDLogLevel.INFO)
-     *     .build());
-     * 
+ * Create a provider with the specified SDK and default configuration. + *

+ * If you need to specify any configuration use {@link Provider#Provider(String, LDConfig)} instead. * - * @param client A {@link LDClient} instance. - * @param config Configuration for the provider. + * @param sdkKey the SDK key for your LaunchDarkly environment */ - public Provider(LDClientInterface client, ProviderConfiguration config) { - this.client = client; - LoggingConfiguration loggingConfig = config.getLoggingConfigurationFactory().build(null); - LDLogAdapter adapter = loggingConfig.getLogAdapter(); - logger = LDLogger.withAdapter(adapter, loggingConfig.getBaseLoggerName()); - - evaluationContextConverter = new EvaluationContextConverter(logger); - evaluationDetailConverter = new EvaluationDetailConverter(logger); - valueConverter = new ValueConverter(logger); + public Provider(String sdkKey) { + this(sdkKey, new LDConfig.Builder().build()); } /** - * Create a provider with the given LaunchDarkly client. - *

- * The provider will be created with default configuration. + * Crate a provider with the specified SDK key and configuration. * - * @param client A {@link LDClient} instance. + * @param sdkKey the SDK key for your LaunchDarkly environment + * @param config a client configuration object */ - public Provider(LDClientInterface client) { - this(client, ProviderConfiguration.builder().build()); + public Provider(String sdkKey, LDConfig config) { + this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config) + .wrapper(Components.wrapperInfo() + .wrapperName("open-feature-java-server") + .wrapperVersion(Version.SDK_VERSION)).build())); + } + + Provider(LDClientInterface client) { + this.client = client; + logger = client.getLogger(); + evaluationContextConverter = new EvaluationContextConverter(logger); + evaluationDetailConverter = new EvaluationDetailConverter(logger); + valueConverter = new ValueConverter(logger); } @Override @@ -86,7 +89,7 @@ public Metadata getMetadata() { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { EvaluationDetail detail - = this.client.boolVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + = this.client.boolVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); return evaluationDetailConverter.toEvaluationDetails(detail); } @@ -94,7 +97,7 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { EvaluationDetail detail - = this.client.stringVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + = this.client.stringVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); return evaluationDetailConverter.toEvaluationDetails(detail); } @@ -102,7 +105,7 @@ public ProviderEvaluation getStringEvaluation(String key, String default @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { EvaluationDetail detail - = this.client.intVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + = this.client.intVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); return evaluationDetailConverter.toEvaluationDetails(detail); } @@ -110,7 +113,7 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { EvaluationDetail detail - = this.client.doubleVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + = this.client.doubleVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); return evaluationDetailConverter.toEvaluationDetails(detail); } @@ -118,9 +121,89 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { EvaluationDetail detail - = this.client.jsonValueVariationDetail( - key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue)); + = this.client.jsonValueVariationDetail( + key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue)); return evaluationDetailConverter.toEvaluationDetailsLdValue(detail); } + + @Override + public ProviderState getState() { + return state; + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + // If we are ready, then set the state. Don't return, because we still need to listen for future + // changes. + if (client.isInitialized()) { + state = ProviderState.READY; + } + + client.getFlagTracker().addFlagChangeListener(detail -> { + emitProviderConfigurationChanged( + ProviderEventDetails.builder().flagsChanged(Collections.singletonList(detail.getKey())).build()); + }); + // Listen for future status changes. + client.getDataSourceStatusProvider().addStatusListener((res) -> { + switch (res.getState()) { + // We will not re-enter INITIALIZING, but it is here to make the switch exhaustive. + case INITIALIZING: { + } + break; + case INTERRUPTED: { + state = ProviderState.STALE; + var message = res.getLastError() != null ? res.getLastError().getMessage() : "encountered an unknown error"; + emitProviderStale(ProviderEventDetails.builder().message(message).build()); + } + break; + case VALID: { + // If we are ready, then we don't want to emit it again. Other conditions we may be updating the + // reason we are stale or interrupted, so we want to emit an event each time. + if (state != ProviderState.READY) { + state = ProviderState.READY; + emitProviderReady(ProviderEventDetails.builder().build()); + } + } + break; + case OFF: { + // Currently there is not a shutdown state. + // Our client/provider cannot be restarted, so we just go to error. + state = ProviderState.ERROR; + emitProviderError(ProviderEventDetails.builder().message("Provider shutdown").build()); + } + } + }); + if (state == ProviderState.READY) { + return; + } + + boolean initialized = client.getDataSourceStatusProvider().waitFor(DataSourceStatusProvider.State.VALID, + ChronoUnit.FOREVER.getDuration()); + + if (!initialized) { + // Here we throw an exception for the OpenFeature SDK, which will handle emitting an event. + throw new RuntimeException("Failed to initialize LaunchDarkly client."); + } + } + + @Override + public void shutdown() { + try { + client.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Get the LaunchDarkly client associated with this provider. + *

+ * This can be used to access LaunchDarkly features which are not available in OpenFeature. + * + * @return the launchdarkly client instance + */ + public LDClientInterface getLdClient() { + return client; + } } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java deleted file mode 100644 index b233bae..0000000 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.launchdarkly.openfeature.serverprovider; - -import com.launchdarkly.logging.LDLogAdapter; -import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; -import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; - -/** - * An immutable configuration for the provider. Must be created using a {@link ProviderConfigurationBuilder}. - *


- *     ProviderConfiguration.builder().build();
- * 
- */ -public final class ProviderConfiguration { - ProviderConfigurationBuilder builder; - - private ProviderConfiguration(ProviderConfigurationBuilder builder) { - this.builder = builder; - } - - /** - * A mutable object that uses the Builder pattern to specify properties for a {@link ProviderConfiguration} object. - */ - public static final class ProviderConfigurationBuilder { - private ComponentConfigurer loggingConfigurer; - - /** - * Build a provider configuration. - * - * @return And immutable provider configuration. - */ - public ProviderConfiguration build() { - if (this.loggingConfigurer == null) { - this.loggingConfigurer = Components.logging(); - } - return new ProviderConfiguration(this); - } - - /** - * Assign an existing logging configuration. - * - * @param config The logging configuration to use. - * @return This builder. - */ - public ProviderConfigurationBuilder logging(ComponentConfigurer config) { - this.loggingConfigurer = config; - return this; - } - - /** - * Create a logging configuration based on an {@link LDLogAdapter}. - * - * @param logAdapter The log adapter to use. - * @return This builder. - */ - public ProviderConfigurationBuilder logging(LDLogAdapter logAdapter) { - this.loggingConfigurer = Components.logging(logAdapter); - return this; - } - } - - /** - * Get a new builder instance. - * - * @return A provider configuration builder. - */ - public static ProviderConfigurationBuilder builder() { - return new ProviderConfigurationBuilder(); - } - - /** - * Get the logging factory to generate logging configuration. - * - * @return A logging factory. - */ - public ComponentConfigurer getLoggingConfigurationFactory() { - return builder.loggingConfigurer; - } -} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java new file mode 100644 index 0000000..b3945b3 --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Version.java @@ -0,0 +1,8 @@ +package com.launchdarkly.openfeature.serverprovider; + +abstract class Version { + private Version() {} + + // This constant is updated automatically by our Gradle script during a release, if the project version has changed + static final String SDK_VERSION = "0.1.0"; +} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/package-info.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/package-info.java index 54bfb29..88a6c04 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/package-info.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/package-info.java @@ -2,8 +2,6 @@ * Main package for the LaunchDarkly OpenFeature provider for the Server-Side SDK for Java, containing the provider * and configuration classes. *

- * You will most often use {@link com.launchdarkly.openfeature.serverprovider.Provider} (the provider) and - * {@link com.launchdarkly.openfeature.serverprovider.ProviderConfiguration} (configuration options for the provider). - *

+ * You will most often use {@link com.launchdarkly.openfeature.serverprovider.Provider} (the provider). */ package com.launchdarkly.openfeature.serverprovider; diff --git a/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java b/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java new file mode 100644 index 0000000..c4145cc --- /dev/null +++ b/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java @@ -0,0 +1,8 @@ +package com.launchdarkly.openfeature.serverprovider; + +abstract class Version { + private Version() {} + + // This constant is updated automatically by our Gradle script during a release, if the project version has changed + static final String SDK_VERSION = "@VERSION@"; +} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java index fe4d9f2..cebb687 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java @@ -8,12 +8,12 @@ import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Value; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.HashMap; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ContextConverterTest { TestLogger testLogger = new TestLogger(); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java index 21c147a..8cb0c3b 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java @@ -3,11 +3,11 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.*; import dev.openfeature.sdk.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class EvaluationDetailConverterTest { private final Double EPSILON = 0.00001; diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java new file mode 100644 index 0000000..be63d0f --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java @@ -0,0 +1,45 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.integrations.TestData; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEvent; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ArrayBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests in this suite use a real client instance and the public constructor. + *

+ * Detailed provider tests use a mock client to test specific result and context conversions. + */ +public class EventsTest { + @Test + public void emitsFlagChangeEvents() throws InterruptedException { + var td = TestData.dataSource(); + td.update(td.flag("flagA").valueForAll(LDValue.of("test"))); + + var provider = new Provider("fake-key", new LDConfig.Builder().dataSource(td) + .events(Components.noEvents()).build()); + + var changes = new ArrayBlockingQueue(1); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, eventDetails -> { + changes.add(eventDetails.getFlagsChanged().get(0)); + }); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + td.update(td.flag("flagA").valueForAll(LDValue.of("updated"))); + + var res = changes.take(); + assertEquals("flagA", res); + + td.update(td.flag("flagB").valueForAll(LDValue.of("new"))); + + var res2 = changes.take(); + assertEquals("flagB", res2); + } +} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java index e72950e..620a2c3 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java @@ -6,11 +6,11 @@ import com.launchdarkly.sdk.ObjectBuilder; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class LdValueConverterTest { private final LDValueConverter valueConverter = new LDValueConverter(LDLogger.none()); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java new file mode 100644 index 0000000..596f12a --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -0,0 +1,85 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEvent; +import dev.openfeature.sdk.ProviderState; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests in this suite use a real client instance and the public constructor. + *

+ * Detailed provider tests use a mock client to test specific result and context conversions. + */ +public class LifeCycleTest { + @Test + public void canCallThePublicConstructor() { + assertDoesNotThrow(() -> { + var provider = new Provider("fake-key", new LDConfig.Builder() + .offline(true).build()); + }); + } + + @Test + public void canInitializeAnOfflineClient() { + assertDoesNotThrow(() -> { + var provider = new Provider("fake-key", new LDConfig.Builder() + .offline(true).build()); + provider.initialize(new ImmutableContext("context-key")); + assertEquals(ProviderState.READY, provider.getState()); + var ldClient = provider.getLdClient(); + assertEquals(DataSourceStatusProvider.State.VALID, ldClient.getDataSourceStatusProvider().getStatus().getState()); + }); + } + + @Test + public void canShutdownAnOfflineClient() { + assertDoesNotThrow(() -> { + var provider = new Provider("fake-key", new LDConfig.Builder() + .offline(true).build()); + provider.initialize(new ImmutableContext("context-key")); + provider.shutdown(); + // Currently this does not check the provider state as the OF spec doesn't yet have a terminal + // shutdown state. + var ldClient = provider.getLdClient(); + assertEquals(DataSourceStatusProvider.State.OFF, ldClient.getDataSourceStatusProvider().getStatus().getState()); + }); + } + + @Test + public void itEmitsReadyEvents() { + var provider = new Provider("fake-key", new LDConfig.Builder() + .offline(true).build()); + + var readyCount = new AtomicInteger(); + var errorCount = new AtomicInteger(); + var staleCount = new AtomicInteger(); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> { + readyCount.getAndIncrement(); + }); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_STALE, (detail) -> { + staleCount.getAndIncrement(); + }); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> { + errorCount.getAndIncrement(); + }); + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + OpenFeatureAPI.getInstance().shutdown(); + + assertEquals(1, readyCount.get()); + assertEquals(0, staleCount.get()); + assertEquals(0, errorCount.get()); + } +} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java deleted file mode 100644 index e23ac67..0000000 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.launchdarkly.openfeature.serverprovider; - -import com.launchdarkly.logging.LDLogLevel; -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; -import org.junit.Test; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - - -public class ProviderConfigurationTests { - @Test - public void itCanBuildADefaultConfiguration() { - ProviderConfiguration defaultConfig = ProviderConfiguration.builder().build(); - assertNotNull(defaultConfig.getLoggingConfigurationFactory()); - } - - @Test - public void itCanBeUsedWithALogAdapter() { - TestLogger logAdapter = new TestLogger(); - ProviderConfiguration withLogAdapter = ProviderConfiguration.builder() - .logging(logAdapter).build(); - - LoggingConfiguration loggingConfig = withLogAdapter.getLoggingConfigurationFactory() - .build(null); - - LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name"); - logger.error("this is the error"); - - assertTrue(logAdapter - .getChannel("the-name") - .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error")); - } - - @Test - public void itCanBeUsedWithALoggingComponentConfigurer() { - TestLogger logAdapter = new TestLogger(); - ProviderConfiguration withConfigurer = ProviderConfiguration.builder() - .logging(Components.logging().adapter(logAdapter)).build(); - - LoggingConfiguration loggingConfig = withConfigurer.getLoggingConfigurationFactory() - .build(null); - - LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name"); - logger.error("this is the error"); - - assertTrue(logAdapter - .getChannel("the-name") - .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error")); - } -} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java index 7194764..757a4c5 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java @@ -6,13 +6,18 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; import dev.openfeature.sdk.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ProviderTest { LDClientInterface mockedLdClient = mock(LDClientInterface.class); + + /** + * This test uses the package private constructor, which means that it does not set + * wrapper information. + */ Provider ldProvider = new Provider(mockedLdClient); @Test @@ -26,19 +31,19 @@ public void itCanDoABooleanEvaluation() { EvaluationContext evaluationContext = new ImmutableContext("user-key"); when(mockedLdClient.boolVariationDetail("the-key", LDContext.create("user-key"), false)) - .thenReturn(EvaluationDetail.fromValue(true, 12, EvaluationReason.fallthrough())); + .thenReturn(EvaluationDetail.fromValue(true, 12, EvaluationReason.fallthrough())); OpenFeatureAPI.getInstance().setProvider(ldProvider); assertTrue(OpenFeatureAPI - .getInstance() - .getClient() - .getBooleanValue("the-key", false, evaluationContext)); + .getInstance() + .getClient() + .getBooleanValue("the-key", false, evaluationContext)); FlagEvaluationDetails detailed = OpenFeatureAPI - .getInstance() - .getClient() - .getBooleanDetails("the-key", false, evaluationContext); + .getInstance() + .getClient() + .getBooleanDetails("the-key", false, evaluationContext); assertEquals(true, detailed.getValue()); assertEquals("12", detailed.getVariant()); @@ -50,20 +55,20 @@ public void itCanDoAStringEvaluation() { EvaluationContext evaluationContext = new ImmutableContext("user-key"); when(mockedLdClient.stringVariationDetail("the-key", LDContext.create("user-key"), "default")) - .thenReturn(EvaluationDetail - .fromValue("evaluated", 17, EvaluationReason.off())); + .thenReturn(EvaluationDetail + .fromValue("evaluated", 17, EvaluationReason.off())); OpenFeatureAPI.getInstance().setProvider(ldProvider); assertEquals("evaluated", OpenFeatureAPI - .getInstance() - .getClient() - .getStringValue("the-key", "default", evaluationContext)); + .getInstance() + .getClient() + .getStringValue("the-key", "default", evaluationContext)); FlagEvaluationDetails detailed = OpenFeatureAPI - .getInstance() - .getClient() - .getStringDetails("the-key", "default", evaluationContext); + .getInstance() + .getClient() + .getStringDetails("the-key", "default", evaluationContext); assertEquals("evaluated", detailed.getValue()); assertEquals("17", detailed.getVariant()); @@ -75,18 +80,18 @@ public void itCanDoADoubleEvaluation() { EvaluationContext evaluationContext = new ImmutableContext("user-key"); when(mockedLdClient.doubleVariationDetail("the-key", LDContext.create("user-key"), 0.0)) - .thenReturn(EvaluationDetail.fromValue(1.0, 42, EvaluationReason.targetMatch())); + .thenReturn(EvaluationDetail.fromValue(1.0, 42, EvaluationReason.targetMatch())); OpenFeatureAPI.getInstance().setProvider(ldProvider); assertEquals(1.0, OpenFeatureAPI - .getInstance() - .getClient() - .getDoubleValue("the-key", 0.0, evaluationContext), 0.00001); + .getInstance() + .getClient() + .getDoubleValue("the-key", 0.0, evaluationContext), 0.00001); FlagEvaluationDetails detailed = OpenFeatureAPI - .getInstance() - .getClient() - .getDoubleDetails("the-key", 0.0, evaluationContext); + .getInstance() + .getClient() + .getDoubleDetails("the-key", 0.0, evaluationContext); assertEquals(1.0, detailed.getValue(), 0.00001); assertEquals("42", detailed.getVariant()); @@ -98,23 +103,23 @@ public void itCanDoAValueEvaluation() { EvaluationContext evaluationContext = new ImmutableContext("user-key"); EvaluationDetail evaluationDetail = EvaluationDetail - .fromValue(LDValue.buildObject().put("aKey", "aValue").build(), 84, EvaluationReason.targetMatch()); + .fromValue(LDValue.buildObject().put("aKey", "aValue").build(), 84, EvaluationReason.targetMatch()); when(mockedLdClient.jsonValueVariationDetail("the-key", LDContext.create("user-key"), LDValue.ofNull())) - .thenReturn(evaluationDetail); + .thenReturn(evaluationDetail); OpenFeatureAPI.getInstance().setProvider(ldProvider); Value ofValue = OpenFeatureAPI - .getInstance() - .getClient() - .getObjectValue("the-key", new Value(), evaluationContext); + .getInstance() + .getClient() + .getObjectValue("the-key", new Value(), evaluationContext); assertEquals("aValue", ofValue.asStructure().getValue("aKey").asString()); FlagEvaluationDetails detailed = OpenFeatureAPI - .getInstance() - .getClient() - .getObjectDetails("the-key", new Value(), evaluationContext); + .getInstance() + .getClient() + .getObjectDetails("the-key", new Value(), evaluationContext); assertEquals("aValue", detailed.getValue().asStructure().getValue("aKey").asString()); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java index a60189a..d1c84cc 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java @@ -5,14 +5,14 @@ import com.launchdarkly.sdk.LDValueType; import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Value; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class ValueConverterTest { private final ValueConverter valueConverter = new ValueConverter(LDLogger.none());