From 96d1264477bfe948818daa1459f0c7cc95dd95ae Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 30 Sep 2024 16:09:49 -0400 Subject: [PATCH 1/5] feat!: context enrichment via contextEnricher func Signed-off-by: Todd Baert --- providers/flagd/README.md | 8 +- .../contrib/providers/flagd/FlagdOptions.java | 24 +- .../providers/flagd/FlagdProvider.java | 48 +- .../providers/flagd/SyncMetadataHook.java | 26 + .../resolver/common/ConnectionEvent.java | 19 +- .../flagd/resolver/common/Convert.java | 14 + .../resolver/process/storage/FlagStore.java | 12 +- .../process/storage/StorageStateChange.java | 13 +- .../providers/flagd/FlagdProviderTest.java | 1827 +++++++++-------- .../providers/flagd/SyncMetadataHookTest.java | 36 + .../process/InProcessResolverTest.java | 69 +- 11 files changed, 1103 insertions(+), 993 deletions(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHookTest.java diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 4c6786efb..fece0f9af 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -96,7 +96,7 @@ FlagdProvider flagdProvider = new FlagdProvider(options); ### Configuration options -Options can be defined in the constructor or as environment variables, with constructor options having the highest +Most options can be defined in the constructor or as environment variables, with constructor options having the highest precedence. Default options can be overridden through a `FlagdOptions` based constructor or set to be picked up from the environment variables. @@ -177,6 +177,12 @@ By default, the provider is configured to use [least recently used (lru)](https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/LRUMap.html) caching with up to 1000 entries. +##### Context enrichment + +The `contextEnricher` option is a function which provides a context to be added to each evaluation. +This function runs on the initial provider connection and ever reconnection, and is passed the [sync-metadata](#sync-metadata). +By default, a simple implementation which uses the sync-metadata payload its entirety is used. + ### OpenTelemetry tracing (RPC only) flagd provider support OpenTelemetry traces for gRPC-backed remote evaluations. diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java index c88709a32..dc0349aa6 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdOptions.java @@ -1,14 +1,19 @@ package dev.openfeature.contrib.providers.flagd; +import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault; +import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider; + +import java.util.function.Function; + import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.Structure; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import lombok.Builder; import lombok.Getter; -import static dev.openfeature.contrib.providers.flagd.Config.fallBackToEnvOrDefault; -import static dev.openfeature.contrib.providers.flagd.Config.fromValueProvider; - /** * FlagdOptions is a builder to build flagd provider options. */ @@ -109,6 +114,19 @@ public class FlagdOptions { @Builder.Default private String offlineFlagSourcePath = fallBackToEnvOrDefault(Config.OFFLINE_SOURCE_PATH, null); + /** + * Function providing an EvaluationContext to mix into every evaluations. + * The sync-metadata response + * (https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.GetMetadataResponse), + * represented as a {@link dev.openfeature.sdk.Structure}, is passed as an + * argument. + * This function runs every time the provider (re)connects, and its result is cached and used in every evaluation. + * By default, the entire sync response (converted to a Structure) is used. + */ + @Builder.Default + private Function contextEnricher = (syncMetadata) -> new ImmutableContext( + syncMetadata.asMap()); + /** * Inject a Custom Connector for fetching flags. */ diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java index 15bfb5cbb..4346d6ae9 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java @@ -1,7 +1,9 @@ package dev.openfeature.contrib.providers.flagd; +import java.util.ArrayList; import java.util.Collections; -import java.util.Map; +import java.util.List; +import java.util.function.Function; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -10,9 +12,13 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; import lombok.extern.slf4j.Slf4j; @@ -22,13 +28,14 @@ @Slf4j @SuppressWarnings({ "PMD.TooManyStaticImports", "checkstyle:NoFinalizer" }) public class FlagdProvider extends EventProvider { + private Function contextEnricher; private static final String FLAGD_PROVIDER = "flagd"; private final Resolver flagResolver; private volatile boolean initialized = false; private volatile boolean connected = false; - private volatile Map syncMetadata = Collections.emptyMap(); - - private EvaluationContext evaluationContext; + private volatile Structure syncMetadata = new ImmutableStructure(); + private volatile EvaluationContext enrichedContext = new ImmutableContext(); + private final List hooks = new ArrayList<>(); protected final void finalize() { // DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW @@ -62,6 +69,13 @@ public FlagdProvider(final FlagdOptions options) { throw new IllegalStateException( String.format("Requested unsupported resolver type of %s", options.getResolverType())); } + hooks.add(new SyncMetadataHook(this::getEnrichedContext)); + contextEnricher = options.getContextEnricher(); + } + + @Override + public List getProviderHooks() { + return Collections.unmodifiableList(hooks); } @Override @@ -70,7 +84,6 @@ public synchronized void initialize(EvaluationContext evaluationContext) throws return; } - this.evaluationContext = evaluationContext; this.flagResolver.init(); this.initialized = true; } @@ -97,31 +110,31 @@ public Metadata getMetadata() { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - return this.flagResolver.booleanEvaluation(key, defaultValue, mergeContext(ctx)); + return this.flagResolver.booleanEvaluation(key, defaultValue, ctx); } @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - return this.flagResolver.stringEvaluation(key, defaultValue, mergeContext(ctx)); + return this.flagResolver.stringEvaluation(key, defaultValue, ctx); } @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - return this.flagResolver.doubleEvaluation(key, defaultValue, mergeContext(ctx)); + return this.flagResolver.doubleEvaluation(key, defaultValue, ctx); } @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - return this.flagResolver.integerEvaluation(key, defaultValue, mergeContext(ctx)); + return this.flagResolver.integerEvaluation(key, defaultValue, ctx); } @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { - return this.flagResolver.objectEvaluation(key, defaultValue, mergeContext(ctx)); + return this.flagResolver.objectEvaluation(key, defaultValue, ctx); } /** - * An unmodifiable view of an object map representing the latest result of the + * An unmodifiable view of a Structure representing the latest result of the * SyncMetadata. * Set on initial connection and updated with every reconnection. * see: @@ -129,16 +142,12 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa * * @return Object map representing sync metadata */ - protected Map getSyncMetadata() { - return Collections.unmodifiableMap(syncMetadata); + protected Structure getSyncMetadata() { + return new ImmutableStructure(syncMetadata.asMap()); } - private EvaluationContext mergeContext(final EvaluationContext clientCallCtx) { - if (this.evaluationContext != null) { - return evaluationContext.merge(clientCallCtx); - } - - return clientCallCtx; + EvaluationContext getEnrichedContext() { + return enrichedContext; } private boolean isConnected() { @@ -149,6 +158,7 @@ private void onConnectionEvent(ConnectionEvent connectionEvent) { boolean previous = connected; boolean current = connected = connectionEvent.isConnected(); syncMetadata = connectionEvent.getSyncMetadata(); + enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata()); // configuration changed if (initialized && previous && current) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java new file mode 100644 index 000000000..77c3e03e4 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java @@ -0,0 +1,26 @@ +package dev.openfeature.contrib.providers.flagd; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; + +class SyncMetadataHook implements Hook { + + private Supplier contextSupplier; + + SyncMetadataHook(Supplier contextSupplier) { + this.contextSupplier = contextSupplier; + } + + /** + * Return the enriched context, including the additional attributes from the sync-metadata. + */ + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.ofNullable(contextSupplier.get()); + } +} \ No newline at end of file diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java index cc8a7a0b0..d48b9e49e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java @@ -2,8 +2,9 @@ import java.util.Collections; import java.util.List; -import java.util.Map; +import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.Structure; import lombok.AllArgsConstructor; import lombok.Getter; @@ -17,7 +18,7 @@ public class ConnectionEvent { @Getter private final boolean connected; private final List flagsChanged; - private final Map syncMetadata; + private final Structure syncMetadata; /** * Construct a new ConnectionEvent. @@ -25,7 +26,7 @@ public class ConnectionEvent { * @param connected status of the connection */ public ConnectionEvent(boolean connected) { - this(connected, Collections.emptyList(), Collections.emptyMap()); + this(connected, Collections.emptyList(), new ImmutableStructure()); } /** @@ -35,7 +36,7 @@ public ConnectionEvent(boolean connected) { * @param flagsChanged list of flags changed */ public ConnectionEvent(boolean connected, List flagsChanged) { - this(connected, flagsChanged, Collections.emptyMap()); + this(connected, flagsChanged, new ImmutableStructure()); } /** @@ -44,8 +45,8 @@ public ConnectionEvent(boolean connected, List flagsChanged) { * @param connected status of the connection * @param syncMetadata sync.getMetadata */ - public ConnectionEvent(boolean connected, Map syncMetadata) { - this(connected, Collections.emptyList(), syncMetadata); + public ConnectionEvent(boolean connected, Structure syncMetadata) { + this(connected, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap())); } /** @@ -58,11 +59,11 @@ public List getFlagsChanged() { } /** - * Get changed sync metadata. + * Get changed sync metadata represented as SDK structure type. * * @return an unmodifiable view of the sync metadata */ - public Map getSyncMetadata() { - return Collections.unmodifiableMap(syncMetadata); + public Structure getSyncMetadata() { + return new ImmutableStructure(syncMetadata.asMap()); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java index 939b614d3..e33f59736 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/Convert.java @@ -12,6 +12,7 @@ import com.google.protobuf.Struct; import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.MutableStructure; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; @@ -20,6 +21,19 @@ * gRPC type conversion utils. */ public class Convert { + + /** + * Converts a protobuf struct to EvaluationContext. + * + * @param struct profobuf struct to convert + * @return a context + */ + public static EvaluationContext convertProtobufStructToContext(final Struct struct) { + final HashMap values = new HashMap<>(); + struct.getFieldsMap().forEach((key, value) -> values.put(key, convertAny(value))); + return new ImmutableContext(values); + } + /** * Recursively convert protobuf structure to openfeature value. */ diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java index a4581f338..e4add48e9 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java @@ -2,7 +2,6 @@ import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.convertProtobufMapToStructure; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,6 +18,8 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse; +import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.Structure; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.extern.slf4j.Slf4j; @@ -109,7 +110,7 @@ private void streamerListener(final Connector connector) throws InterruptedExcep List changedFlagsKeys; Map flagMap = FlagParser.parseString(payload.getFlagData(), throwIfInvalid); - Map metadata = parseSyncMetadata(payload.getMetadataResponse()); + Structure metadata = parseSyncMetadata(payload.getMetadataResponse()); writeLock.lock(); try { changedFlagsKeys = getChangedFlagsKeys(flagMap); @@ -143,14 +144,13 @@ private void streamerListener(final Connector connector) throws InterruptedExcep log.info("Shutting down store stream listener"); } - private Map parseSyncMetadata(GetMetadataResponse metadataResponse) { + private Structure parseSyncMetadata(GetMetadataResponse metadataResponse) { try { - return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap()) - .asObjectMap(); + return convertProtobufMapToStructure(metadataResponse.getMetadata().getFieldsMap()); } catch (Exception exception) { log.error("Failed to parse metadataResponse, provider metadata may not be up-to-date"); } - return Collections.emptyMap(); + return new ImmutableStructure(); } private List getChangedFlagsKeys(Map newFlags) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java index 042bd082b..a4db81553 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java @@ -2,8 +2,9 @@ import java.util.Collections; import java.util.List; -import java.util.Map; +import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.Structure; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -17,7 +18,7 @@ public class StorageStateChange { private final StorageState storageState; private final List changedFlagsKeys; - private final Map syncMetadata; + private final Structure syncMetadata; /** * Construct a new StorageStateChange. @@ -26,10 +27,10 @@ public class StorageStateChange { * @param syncMetadata possibly updated metadata */ public StorageStateChange(StorageState storageState, List changedFlagsKeys, - Map syncMetadata) { + Structure syncMetadata) { this.storageState = storageState; this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = Collections.unmodifiableMap(syncMetadata); + this.syncMetadata = new ImmutableStructure(syncMetadata.asMap()); } /** @@ -40,7 +41,7 @@ public StorageStateChange(StorageState storageState, List changedFlagsKe public StorageStateChange(StorageState storageState, List changedFlagsKeys) { this.storageState = storageState; this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = Collections.emptyMap(); + this.syncMetadata = new ImmutableStructure(); } /** @@ -50,6 +51,6 @@ public StorageStateChange(StorageState storageState, List changedFlagsKe public StorageStateChange(StorageState storageState) { this.storageState = storageState; this.changedFlagsKeys = Collections.emptyList(); - this.syncMetadata = Collections.emptyMap(); + this.syncMetadata = new ImmutableStructure(); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java index 269c14d9d..bf179b5bd 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java @@ -13,7 +13,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -21,22 +20,19 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; -import java.util.concurrent.LinkedBlockingQueue; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; -import org.mockito.Mockito; import com.google.protobuf.Struct; @@ -49,13 +45,14 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.MockStorage; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; -import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveFloatResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveIntResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveObjectResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveStringResponse; +import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceBlockingStub; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceStub; import dev.openfeature.sdk.EvaluationContext; @@ -65,947 +62,951 @@ import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.MutableStructure; import dev.openfeature.sdk.OpenFeatureAPI; -import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; import io.cucumber.java.AfterAll; import io.grpc.Channel; import io.grpc.Deadline; -import lombok.val; class FlagdProviderTest { - private static final String FLAG_KEY = "some-key"; - private static final String FLAG_KEY_BOOLEAN = "some-key-boolean"; - private static final String FLAG_KEY_INTEGER = "some-key-integer"; - private static final String FLAG_KEY_DOUBLE = "some-key-double"; - private static final String FLAG_KEY_STRING = "some-key-string"; - private static final String FLAG_KEY_OBJECT = "some-key-object"; - private static final String BOOL_VARIANT = "on"; - private static final String DOUBLE_VARIANT = "half"; - private static final String INT_VARIANT = "one-hundred"; - private static final String STRING_VARIANT = "greeting"; - private static final String OBJECT_VARIANT = "obj"; - private static final Reason DEFAULT = Reason.DEFAULT; - private static final Integer INT_VALUE = 100; - private static final Double DOUBLE_VALUE = .5d; - private static final String INNER_STRUCT_KEY = "inner_key"; - private static final String INNER_STRUCT_VALUE = "inner_value"; - private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = Struct.newBuilder() - .putFields(INNER_STRUCT_KEY, - com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE) - .build()) - .build(); - private static final String STRING_VALUE = "hi!"; - - private static OpenFeatureAPI api; - - @BeforeAll - public static void init() { - api = OpenFeatureAPI.getInstance(); - } - - @AfterAll - public static void cleanUp() { - api.shutdown(); - } - - @Test - void resolvers_call_grpc_service_and_return_details() { - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() - .setValue(STRING_VALUE) - .setVariant(STRING_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() - .setValue(INT_VALUE) - .setVariant(INT_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() - .setValue(DOUBLE_VALUE) - .setVariant(DOUBLE_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() - .setValue(PROTOBUF_STRUCTURE_VALUE) - .setVariant(OBJECT_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - when(serviceBlockingStubMock - .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) - .thenReturn(floatResponse); - when(serviceBlockingStubMock - .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) - .thenReturn(intResponse); - when(serviceBlockingStubMock - .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) - .thenReturn(stringResponse); - when(serviceBlockingStubMock - .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) - .thenReturn(objectResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, - false); - assertTrue(booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(DEFAULT.toString(), booleanDetails.getReason()); - - FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, - "wrong"); - assertEquals(STRING_VALUE, stringDetails.getValue()); - assertEquals(STRING_VARIANT, stringDetails.getVariant()); - assertEquals(DEFAULT.toString(), stringDetails.getReason()); - - FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - assertEquals(INT_VALUE, intDetails.getValue()); - assertEquals(INT_VARIANT, intDetails.getVariant()); - assertEquals(DEFAULT.toString(), intDetails.getReason()); - - FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - assertEquals(DOUBLE_VALUE, floatDetails.getValue()); - assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); - assertEquals(DEFAULT.toString(), floatDetails.getReason()); - - FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, - new Value()); - assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() - .asMap().get(INNER_STRUCT_KEY).asString()); - assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); - assertEquals(DEFAULT.toString(), objectDetails.getReason()); - } - - @Test - void zero_value() { - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setVariant(BOOL_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() - .setVariant(STRING_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() - .setVariant(INT_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() - .setVariant(DOUBLE_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() - .setVariant(OBJECT_VARIANT) - .setReason(DEFAULT.toString()) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - when(serviceBlockingStubMock - .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) - .thenReturn(floatResponse); - when(serviceBlockingStubMock - .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) - .thenReturn(intResponse); - when(serviceBlockingStubMock - .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) - .thenReturn(stringResponse); - when(serviceBlockingStubMock - .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) - .thenReturn(objectResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, - false); - assertEquals(false, booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(DEFAULT.toString(), booleanDetails.getReason()); - - FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, - "wrong"); - assertEquals("", stringDetails.getValue()); - assertEquals(STRING_VARIANT, stringDetails.getVariant()); - assertEquals(DEFAULT.toString(), stringDetails.getReason()); - - FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - assertEquals(0, intDetails.getValue()); - assertEquals(INT_VARIANT, intDetails.getVariant()); - assertEquals(DEFAULT.toString(), intDetails.getReason()); - - FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - assertEquals(0.0, floatDetails.getValue()); - assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); - assertEquals(DEFAULT.toString(), floatDetails.getReason()); - - FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, - new Value()); - assertEquals(new MutableStructure(), objectDetails.getValue().asObject()); - assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); - assertEquals(DEFAULT.toString(), objectDetails.getReason()); - } - - @Test - void test_metadata_from_grpc_response() { - // given - final Map metadataInput = new HashMap<>(); - - com.google.protobuf.Value scope = com.google.protobuf.Value.newBuilder().setStringValue("flagd-scope") - .build(); - metadataInput.put("scope", scope); - - com.google.protobuf.Value bool = com.google.protobuf.Value.newBuilder().setBoolValue(true).build(); - metadataInput.put("boolean", bool); - - com.google.protobuf.Value number = com.google.protobuf.Value.newBuilder().setNumberValue(1).build(); - metadataInput.put("number", number); - - final Struct metadataStruct = Struct.newBuilder().putAllFields(metadataInput).build(); - - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(DEFAULT.toString()) - .setMetadata(metadataStruct) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))).thenReturn( - serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - // when - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, - false); - - // then - final ImmutableMetadata metadata = booleanDetails.getFlagMetadata(); - - assertEquals("flagd-scope", metadata.getString("scope")); - assertEquals(true, metadata.getBoolean("boolean")); - assertEquals(1, metadata.getDouble("number")); - } - - @Test - void resolvers_cache_responses_if_static_and_event_stream_alive() { - do_resolvers_cache_responses(STATIC_REASON, true, true); - } - - @Test - void resolvers_should_not_cache_responses_if_not_static() { - do_resolvers_cache_responses(DEFAULT.toString(), true, false); - } - - @Test - void resolvers_should_not_cache_responses_if_event_stream_not_alive() { - do_resolvers_cache_responses(STATIC_REASON, false, false); - } + private static final String FLAG_KEY = "some-key"; + private static final String FLAG_KEY_BOOLEAN = "some-key-boolean"; + private static final String FLAG_KEY_INTEGER = "some-key-integer"; + private static final String FLAG_KEY_DOUBLE = "some-key-double"; + private static final String FLAG_KEY_STRING = "some-key-string"; + private static final String FLAG_KEY_OBJECT = "some-key-object"; + private static final String BOOL_VARIANT = "on"; + private static final String DOUBLE_VARIANT = "half"; + private static final String INT_VARIANT = "one-hundred"; + private static final String STRING_VARIANT = "greeting"; + private static final String OBJECT_VARIANT = "obj"; + private static final Reason DEFAULT = Reason.DEFAULT; + private static final Integer INT_VALUE = 100; + private static final Double DOUBLE_VALUE = .5d; + private static final String INNER_STRUCT_KEY = "inner_key"; + private static final String INNER_STRUCT_VALUE = "inner_value"; + private static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = Struct.newBuilder() + .putFields(INNER_STRUCT_KEY, + com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE) + .build()) + .build(); + private static final String STRING_VALUE = "hi!"; + + private static OpenFeatureAPI api; + + @BeforeAll + public static void init() { + api = OpenFeatureAPI.getInstance(); + } + + @AfterAll + public static void cleanUp() { + api.shutdown(); + } + + @Test + void resolvers_call_grpc_service_and_return_details() { + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() + .setValue(STRING_VALUE) + .setVariant(STRING_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() + .setValue(INT_VALUE) + .setVariant(INT_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() + .setValue(DOUBLE_VALUE) + .setVariant(DOUBLE_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() + .setValue(PROTOBUF_STRUCTURE_VALUE) + .setVariant(OBJECT_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + when(serviceBlockingStubMock + .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) + .thenReturn(floatResponse); + when(serviceBlockingStubMock + .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) + .thenReturn(intResponse); + when(serviceBlockingStubMock + .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) + .thenReturn(stringResponse); + when(serviceBlockingStubMock + .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) + .thenReturn(objectResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, + false); + assertTrue(booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(DEFAULT.toString(), booleanDetails.getReason()); + + FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, + "wrong"); + assertEquals(STRING_VALUE, stringDetails.getValue()); + assertEquals(STRING_VARIANT, stringDetails.getVariant()); + assertEquals(DEFAULT.toString(), stringDetails.getReason()); + + FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + assertEquals(INT_VALUE, intDetails.getValue()); + assertEquals(INT_VARIANT, intDetails.getVariant()); + assertEquals(DEFAULT.toString(), intDetails.getReason()); + + FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + assertEquals(DOUBLE_VALUE, floatDetails.getValue()); + assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); + assertEquals(DEFAULT.toString(), floatDetails.getReason()); + + FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, + new Value()); + assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() + .asMap().get(INNER_STRUCT_KEY).asString()); + assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); + assertEquals(DEFAULT.toString(), objectDetails.getReason()); + } + + @Test + void zero_value() { + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setVariant(BOOL_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() + .setVariant(STRING_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() + .setVariant(INT_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() + .setVariant(DOUBLE_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() + .setVariant(OBJECT_VARIANT) + .setReason(DEFAULT.toString()) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + when(serviceBlockingStubMock + .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) + .thenReturn(floatResponse); + when(serviceBlockingStubMock + .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) + .thenReturn(intResponse); + when(serviceBlockingStubMock + .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) + .thenReturn(stringResponse); + when(serviceBlockingStubMock + .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) + .thenReturn(objectResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, + false); + assertEquals(false, booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(DEFAULT.toString(), booleanDetails.getReason()); + + FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, + "wrong"); + assertEquals("", stringDetails.getValue()); + assertEquals(STRING_VARIANT, stringDetails.getVariant()); + assertEquals(DEFAULT.toString(), stringDetails.getReason()); + + FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + assertEquals(0, intDetails.getValue()); + assertEquals(INT_VARIANT, intDetails.getVariant()); + assertEquals(DEFAULT.toString(), intDetails.getReason()); + + FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + assertEquals(0.0, floatDetails.getValue()); + assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); + assertEquals(DEFAULT.toString(), floatDetails.getReason()); + + FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, + new Value()); + assertEquals(new MutableStructure(), objectDetails.getValue().asObject()); + assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); + assertEquals(DEFAULT.toString(), objectDetails.getReason()); + } + + @Test + void test_metadata_from_grpc_response() { + // given + final Map metadataInput = new HashMap<>(); + + com.google.protobuf.Value scope = com.google.protobuf.Value.newBuilder().setStringValue("flagd-scope") + .build(); + metadataInput.put("scope", scope); + + com.google.protobuf.Value bool = com.google.protobuf.Value.newBuilder().setBoolValue(true).build(); + metadataInput.put("boolean", bool); + + com.google.protobuf.Value number = com.google.protobuf.Value.newBuilder().setNumberValue(1).build(); + metadataInput.put("number", number); + + final Struct metadataStruct = Struct.newBuilder().putAllFields(metadataInput).build(); + + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(DEFAULT.toString()) + .setMetadata(metadataStruct) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))).thenReturn( + serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + // when + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, + false); + + // then + final ImmutableMetadata metadata = booleanDetails.getFlagMetadata(); + + assertEquals("flagd-scope", metadata.getString("scope")); + assertEquals(true, metadata.getBoolean("boolean")); + assertEquals(1, metadata.getDouble("number")); + } + + @Test + void resolvers_cache_responses_if_static_and_event_stream_alive() { + do_resolvers_cache_responses(STATIC_REASON, true, true); + } + + @Test + void resolvers_should_not_cache_responses_if_not_static() { + do_resolvers_cache_responses(DEFAULT.toString(), true, false); + } + + @Test + void resolvers_should_not_cache_responses_if_event_stream_not_alive() { + do_resolvers_cache_responses(STATIC_REASON, false, false); + } + + @Test + void context_is_parsed_and_passed_to_grpc_service() { + final String BOOLEAN_ATTR_KEY = "bool-attr"; + final String INT_ATTR_KEY = "int-attr"; + final String STRING_ATTR_KEY = "string-attr"; + final String STRUCT_ATTR_KEY = "struct-attr"; + final String DOUBLE_ATTR_KEY = "double-attr"; + final String LIST_ATTR_KEY = "list-attr"; + final String STRUCT_ATTR_INNER_KEY = "struct-inner-key"; + + final Boolean BOOLEAN_ATTR_VALUE = true; + final int INT_ATTR_VALUE = 1; + final String STRING_ATTR_VALUE = "str"; + final double DOUBLE_ATTR_VALUE = 0.5d; + final List LIST_ATTR_VALUE = new ArrayList() { + { + add(new Value(1)); + } + }; + final String STRUCT_ATTR_INNER_VALUE = "struct-inner-value"; + final Structure STRUCT_ATTR_VALUE = new MutableStructure().add(STRUCT_ATTR_INNER_KEY, + STRUCT_ATTR_INNER_VALUE); + final String DEFAULT_STRING = "DEFAULT"; + + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(DEFAULT_STRING.toString()) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.resolveBoolean(argThat( + x -> { + final Struct struct = x.getContext(); + final Map valueMap = struct.getFieldsMap(); + + return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue()) + && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue() + && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY) + .getNumberValue() + && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue() + && "MY_TARGETING_KEY".equals( + valueMap.get("targetingKey").getStringValue()) + && LIST_ATTR_VALUE.get(0).asInteger() == valueMap + .get(LIST_ATTR_KEY).getListValue() + .getValuesList().get(0).getNumberValue() + && STRUCT_ATTR_INNER_VALUE.equals( + valueMap.get(STRUCT_ATTR_KEY).getStructValue() + .getFieldsMap() + .get(STRUCT_ATTR_INNER_KEY) + .getStringValue()); + }))).thenReturn(booleanResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + final MutableContext context = new MutableContext("MY_TARGETING_KEY"); + context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); + context.add(INT_ATTR_KEY, INT_ATTR_VALUE); + context.add(DOUBLE_ATTR_KEY, DOUBLE_ATTR_VALUE); + context.add(LIST_ATTR_KEY, LIST_ATTR_VALUE); + context.add(STRING_ATTR_KEY, STRING_ATTR_VALUE); + context.add(STRUCT_ATTR_KEY, STRUCT_ATTR_VALUE); + + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, + context); + assertTrue(booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(DEFAULT.toString(), booleanDetails.getReason()); + } + + // Validates null handling - + // https://github.com/open-feature/java-sdk-contrib/issues/258 + @Test + void null_context_handling() { + // given + final String flagA = "flagA"; + final boolean defaultVariant = false; + final boolean expectedVariant = true; + + final MutableContext context = new MutableContext(); + context.add("key", (String) null); + + final ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + + // when + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.resolveBoolean(any())) + .thenReturn(ResolveBooleanResponse.newBuilder().setValue(expectedVariant).build()); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + // then + final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); + + assertNotEquals(evaluation, defaultVariant); + assertEquals(evaluation, expectedVariant); + } + + @Test + void reason_mapped_correctly_if_unknown() { + ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason("UNKNOWN") // set an invalid reason string + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.resolveBoolean(any(ResolveBooleanRequest.class))) + .thenReturn(badReasonResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + + OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); + + FlagEvaluationDetails booleanDetails = api.getClient() + .getBooleanDetails(FLAG_KEY, false, new MutableContext()); + assertEquals(Reason.UNKNOWN.toString(), booleanDetails.getReason()); // reason should be converted to + // UNKNOWN + } + + @Test + void invalidate_cache() { + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() + .setValue(STRING_VALUE) + .setVariant(STRING_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() + .setValue(INT_VALUE) + .setVariant(INT_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() + .setValue(DOUBLE_VALUE) + .setVariant(DOUBLE_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() + .setValue(PROTOBUF_STRUCTURE_VALUE) + .setVariant(OBJECT_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + ServiceStub serviceStubMock = mock(ServiceStub.class); + when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); + doNothing().when(serviceStubMock).eventStream(any(), any()); + when(serviceStubMock.withDeadline(any(Deadline.class))) + .thenReturn(serviceStubMock); + when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .withDeadline(any(Deadline.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + when(serviceBlockingStubMock + .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) + .thenReturn(floatResponse); + when(serviceBlockingStubMock + .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) + .thenReturn(intResponse); + when(serviceBlockingStubMock + .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) + .thenReturn(stringResponse); + when(serviceBlockingStubMock + .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) + .thenReturn(objectResponse); + + GrpcConnector grpc; + try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) { + mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) + .thenReturn(serviceBlockingStubMock); + mockStaticService.when(() -> ServiceGrpc.newStub(any())) + .thenReturn(serviceStubMock); + + final Cache cache = new Cache("lru", 5); + + class NoopInitGrpcConnector extends GrpcConnector { + public NoopInitGrpcConnector(FlagdOptions options, Cache cache, + Supplier connectedSupplier, + Consumer onConnectionEvent) { + super(options, cache, connectedSupplier, onConnectionEvent); + } - @Test - void context_is_parsed_and_passed_to_grpc_service() { - final String BOOLEAN_ATTR_KEY = "bool-attr"; - final String INT_ATTR_KEY = "int-attr"; - final String STRING_ATTR_KEY = "string-attr"; - final String STRUCT_ATTR_KEY = "struct-attr"; - final String DOUBLE_ATTR_KEY = "double-attr"; - final String LIST_ATTR_KEY = "list-attr"; - final String STRUCT_ATTR_INNER_KEY = "struct-inner-key"; - - final Boolean BOOLEAN_ATTR_VALUE = true; - final int INT_ATTR_VALUE = 1; - final String STRING_ATTR_VALUE = "str"; - final double DOUBLE_ATTR_VALUE = 0.5d; - final List LIST_ATTR_VALUE = new ArrayList() { - { - add(new Value(1)); - } + public void initialize() throws Exception { }; - final String STRUCT_ATTR_INNER_VALUE = "struct-inner-value"; - final Structure STRUCT_ATTR_VALUE = new MutableStructure().add(STRUCT_ATTR_INNER_KEY, - STRUCT_ATTR_INNER_VALUE); - final String DEFAULT_STRING = "DEFAULT"; - - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(DEFAULT_STRING.toString()) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.resolveBoolean(argThat( - x -> { - final Struct struct = x.getContext(); - final Map valueMap = struct.getFieldsMap(); - - return STRING_ATTR_VALUE.equals(valueMap.get(STRING_ATTR_KEY).getStringValue()) - && INT_ATTR_VALUE == valueMap.get(INT_ATTR_KEY).getNumberValue() - && DOUBLE_ATTR_VALUE == valueMap.get(DOUBLE_ATTR_KEY) - .getNumberValue() - && valueMap.get(BOOLEAN_ATTR_KEY).getBoolValue() - && "MY_TARGETING_KEY".equals( - valueMap.get("targetingKey").getStringValue()) - && LIST_ATTR_VALUE.get(0).asInteger() == valueMap - .get(LIST_ATTR_KEY).getListValue() - .getValuesList().get(0).getNumberValue() - && STRUCT_ATTR_INNER_VALUE.equals( - valueMap.get(STRUCT_ATTR_KEY).getStructValue() - .getFieldsMap() - .get(STRUCT_ATTR_INNER_KEY) - .getStringValue()); - }))).thenReturn(booleanResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - final MutableContext context = new MutableContext("MY_TARGETING_KEY"); - context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); - context.add(INT_ATTR_KEY, INT_ATTR_VALUE); - context.add(DOUBLE_ATTR_KEY, DOUBLE_ATTR_VALUE); - context.add(LIST_ATTR_KEY, LIST_ATTR_VALUE); - context.add(STRING_ATTR_KEY, STRING_ATTR_VALUE); - context.add(STRUCT_ATTR_KEY, STRUCT_ATTR_VALUE); - - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, - context); - assertTrue(booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(DEFAULT.toString(), booleanDetails.getReason()); - } - - // Validates null handling - - // https://github.com/open-feature/java-sdk-contrib/issues/258 - @Test - void null_context_handling() { - // given - final String flagA = "flagA"; - final boolean defaultVariant = false; - final boolean expectedVariant = true; + } - final MutableContext context = new MutableContext(); - context.add("key", (String) null); - - final ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - - // when - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.resolveBoolean(any())) - .thenReturn(ResolveBooleanResponse.newBuilder().setValue(expectedVariant).build()); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - // then - final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); - - assertNotEquals(evaluation, defaultVariant); - assertEquals(evaluation, expectedVariant); + grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, + (connectionEvent) -> { + }); } - @Test - void reason_mapped_correctly_if_unknown() { - ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason("UNKNOWN") // set an invalid reason string - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.resolveBoolean(any(ResolveBooleanRequest.class))) - .thenReturn(badReasonResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc)); - - FlagEvaluationDetails booleanDetails = api.getClient() - .getBooleanDetails(FLAG_KEY, false, new MutableContext()); - assertEquals(Reason.UNKNOWN.toString(), booleanDetails.getReason()); // reason should be converted to - // UNKNOWN + FlagdProvider provider = createProvider(grpc); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + HashMap flagsMap = new HashMap(); + HashMap structMap = new HashMap(); + + flagsMap.put(FLAG_KEY_BOOLEAN, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); + flagsMap.put(FLAG_KEY_STRING, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); + flagsMap.put(FLAG_KEY_INTEGER, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); + flagsMap.put(FLAG_KEY_DOUBLE, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); + flagsMap.put(FLAG_KEY_OBJECT, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); + + structMap.put("flags", com.google.protobuf.Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); + + // should cache results + FlagEvaluationDetails booleanDetails; + FlagEvaluationDetails stringDetails; + FlagEvaluationDetails intDetails; + FlagEvaluationDetails floatDetails; + FlagEvaluationDetails objectDetails; + + // assert cache has been invalidated + booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); + assertTrue(booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(STATIC_REASON, booleanDetails.getReason()); + + stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); + assertEquals(STRING_VALUE, stringDetails.getValue()); + assertEquals(STRING_VARIANT, stringDetails.getVariant()); + assertEquals(STATIC_REASON, stringDetails.getReason()); + + intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + assertEquals(INT_VALUE, intDetails.getValue()); + assertEquals(INT_VARIANT, intDetails.getVariant()); + assertEquals(STATIC_REASON, intDetails.getReason()); + + floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + assertEquals(DOUBLE_VALUE, floatDetails.getValue()); + assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); + assertEquals(STATIC_REASON, floatDetails.getReason()); + + objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); + assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() + .asMap().get(INNER_STRUCT_KEY).asString()); + assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); + assertEquals(STATIC_REASON, objectDetails.getReason()); + } + + private void do_resolvers_cache_responses(String reason, Boolean eventStreamAlive, Boolean shouldCache) { + String expectedReason = CACHED_REASON; + if (!shouldCache) { + expectedReason = reason; } - @Test - void invalidate_cache() { - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() - .setValue(STRING_VALUE) - .setVariant(STRING_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() - .setValue(INT_VALUE) - .setVariant(INT_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() - .setValue(DOUBLE_VALUE) - .setVariant(DOUBLE_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() - .setValue(PROTOBUF_STRUCTURE_VALUE) - .setVariant(OBJECT_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - ServiceStub serviceStubMock = mock(ServiceStub.class); - when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); - doNothing().when(serviceStubMock).eventStream(any(), any()); - when(serviceStubMock.withDeadline(any(Deadline.class))) - .thenReturn(serviceStubMock); - when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .withDeadline(any(Deadline.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - when(serviceBlockingStubMock - .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) - .thenReturn(floatResponse); - when(serviceBlockingStubMock - .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) - .thenReturn(intResponse); - when(serviceBlockingStubMock - .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) - .thenReturn(stringResponse); - when(serviceBlockingStubMock - .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) - .thenReturn(objectResponse); - - GrpcConnector grpc; - try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) { - mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) - .thenReturn(serviceBlockingStubMock); - mockStaticService.when(() -> ServiceGrpc.newStub(any())) - .thenReturn(serviceStubMock); - - final Cache cache = new Cache("lru", 5); - - class NoopInitGrpcConnector extends GrpcConnector { - public NoopInitGrpcConnector(FlagdOptions options, Cache cache, - Supplier connectedSupplier, - Consumer onConnectionEvent) { - super(options, cache, connectedSupplier, onConnectionEvent); - } - - public void initialize() throws Exception { - }; - } - - grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, - (connectionEvent) -> { - }); + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(reason) + .build(); + + ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() + .setValue(STRING_VALUE) + .setVariant(STRING_VARIANT) + .setReason(reason) + .build(); + + ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() + .setValue(INT_VALUE) + .setVariant(INT_VARIANT) + .setReason(reason) + .build(); + + ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() + .setValue(DOUBLE_VALUE) + .setVariant(DOUBLE_VARIANT) + .setReason(reason) + .build(); + + ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() + .setValue(PROTOBUF_STRUCTURE_VALUE) + .setVariant(OBJECT_VARIANT) + .setReason(reason) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + when(serviceBlockingStubMock + .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) + .thenReturn(floatResponse); + when(serviceBlockingStubMock + .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) + .thenReturn(intResponse); + when(serviceBlockingStubMock + .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) + .thenReturn(stringResponse); + when(serviceBlockingStubMock + .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) + .thenReturn(objectResponse); + + GrpcConnector grpc = mock(GrpcConnector.class); + when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); + FlagdProvider provider = createProvider(grpc, () -> eventStreamAlive); + // provider.setState(eventStreamAlive); // caching only available when event + // stream is alive + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, + false); + booleanDetails = api.getClient() + .getBooleanDetails(FLAG_KEY_BOOLEAN, false); // should retrieve from cache on second + // invocation + assertTrue(booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(expectedReason, booleanDetails.getReason()); + + FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, + "wrong"); + stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); + assertEquals(STRING_VALUE, stringDetails.getValue()); + assertEquals(STRING_VARIANT, stringDetails.getVariant()); + assertEquals(expectedReason, stringDetails.getReason()); + + FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + assertEquals(INT_VALUE, intDetails.getValue()); + assertEquals(INT_VARIANT, intDetails.getVariant()); + assertEquals(expectedReason, intDetails.getReason()); + + FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + assertEquals(DOUBLE_VALUE, floatDetails.getValue()); + assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); + assertEquals(expectedReason, floatDetails.getReason()); + + FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, + new Value()); + objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); + assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() + .asMap().get(INNER_STRUCT_KEY).asString()); + assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); + assertEquals(expectedReason, objectDetails.getReason()); + } + + @Test + void disabled_cache() { + ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() + .setValue(true) + .setVariant(BOOL_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() + .setValue(STRING_VALUE) + .setVariant(STRING_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() + .setValue(INT_VALUE) + .setVariant(INT_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() + .setValue(DOUBLE_VALUE) + .setVariant(DOUBLE_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() + .setValue(PROTOBUF_STRUCTURE_VALUE) + .setVariant(OBJECT_VARIANT) + .setReason(STATIC_REASON) + .build(); + + ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); + ServiceStub serviceStubMock = mock(ServiceStub.class); + when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); + when(serviceStubMock.withDeadline(any(Deadline.class))) + .thenReturn(serviceStubMock); + when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.withDeadline(any(Deadline.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) + .thenReturn(serviceBlockingStubMock); + when(serviceBlockingStubMock + .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) + .thenReturn(booleanResponse); + when(serviceBlockingStubMock + .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) + .thenReturn(floatResponse); + when(serviceBlockingStubMock + .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) + .thenReturn(intResponse); + when(serviceBlockingStubMock + .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) + .thenReturn(stringResponse); + when(serviceBlockingStubMock + .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) + .thenReturn(objectResponse); + + // disabled cache + final Cache cache = new Cache("disabled", 0); + + GrpcConnector grpc; + try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) { + mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) + .thenReturn(serviceBlockingStubMock); + mockStaticService.when(() -> ServiceGrpc.newStub(any())) + .thenReturn(serviceStubMock); + + class NoopInitGrpcConnector extends GrpcConnector { + public NoopInitGrpcConnector(FlagdOptions options, Cache cache, + Supplier connectedSupplier, + Consumer onConnectionEvent) { + super(options, cache, connectedSupplier, onConnectionEvent); } - FlagdProvider provider = createProvider(grpc); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - - HashMap flagsMap = new HashMap(); - HashMap structMap = new HashMap(); - - flagsMap.put(FLAG_KEY_BOOLEAN, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - flagsMap.put(FLAG_KEY_STRING, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - flagsMap.put(FLAG_KEY_INTEGER, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - flagsMap.put(FLAG_KEY_DOUBLE, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - flagsMap.put(FLAG_KEY_OBJECT, com.google.protobuf.Value.newBuilder().setStringValue("foo").build()); - - structMap.put("flags", com.google.protobuf.Value.newBuilder() - .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); - - // should cache results - FlagEvaluationDetails booleanDetails; - FlagEvaluationDetails stringDetails; - FlagEvaluationDetails intDetails; - FlagEvaluationDetails floatDetails; - FlagEvaluationDetails objectDetails; - - // assert cache has been invalidated - booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); - assertTrue(booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(STATIC_REASON, booleanDetails.getReason()); - - stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); - assertEquals(STRING_VALUE, stringDetails.getValue()); - assertEquals(STRING_VARIANT, stringDetails.getVariant()); - assertEquals(STATIC_REASON, stringDetails.getReason()); - - intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - assertEquals(INT_VALUE, intDetails.getValue()); - assertEquals(INT_VARIANT, intDetails.getVariant()); - assertEquals(STATIC_REASON, intDetails.getReason()); - - floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - assertEquals(DOUBLE_VALUE, floatDetails.getValue()); - assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); - assertEquals(STATIC_REASON, floatDetails.getReason()); - - objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); - assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() - .asMap().get(INNER_STRUCT_KEY).asString()); - assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); - assertEquals(STATIC_REASON, objectDetails.getReason()); - } - - private void do_resolvers_cache_responses(String reason, Boolean eventStreamAlive, Boolean shouldCache) { - String expectedReason = CACHED_REASON; - if (!shouldCache) { - expectedReason = reason; - } + public void initialize() throws Exception { + }; + } - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(reason) - .build(); - - ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() - .setValue(STRING_VALUE) - .setVariant(STRING_VARIANT) - .setReason(reason) - .build(); - - ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() - .setValue(INT_VALUE) - .setVariant(INT_VARIANT) - .setReason(reason) - .build(); - - ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() - .setValue(DOUBLE_VALUE) - .setVariant(DOUBLE_VARIANT) - .setReason(reason) - .build(); - - ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() - .setValue(PROTOBUF_STRUCTURE_VALUE) - .setVariant(OBJECT_VARIANT) - .setReason(reason) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - when(serviceBlockingStubMock - .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) - .thenReturn(floatResponse); - when(serviceBlockingStubMock - .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) - .thenReturn(intResponse); - when(serviceBlockingStubMock - .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) - .thenReturn(stringResponse); - when(serviceBlockingStubMock - .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) - .thenReturn(objectResponse); - - GrpcConnector grpc = mock(GrpcConnector.class); - when(grpc.getResolver()).thenReturn(serviceBlockingStubMock); - FlagdProvider provider = createProvider(grpc, () -> eventStreamAlive); - // provider.setState(eventStreamAlive); // caching only available when event - // stream is alive - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, - false); - booleanDetails = api.getClient() - .getBooleanDetails(FLAG_KEY_BOOLEAN, false); // should retrieve from cache on second - // invocation - assertTrue(booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(expectedReason, booleanDetails.getReason()); - - FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, - "wrong"); - stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); - assertEquals(STRING_VALUE, stringDetails.getValue()); - assertEquals(STRING_VARIANT, stringDetails.getVariant()); - assertEquals(expectedReason, stringDetails.getReason()); - - FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - assertEquals(INT_VALUE, intDetails.getValue()); - assertEquals(INT_VARIANT, intDetails.getVariant()); - assertEquals(expectedReason, intDetails.getReason()); - - FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - assertEquals(DOUBLE_VALUE, floatDetails.getValue()); - assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); - assertEquals(expectedReason, floatDetails.getReason()); - - FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, - new Value()); - objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); - assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() - .asMap().get(INNER_STRUCT_KEY).asString()); - assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); - assertEquals(expectedReason, objectDetails.getReason()); + grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, + (connectionEvent) -> { + }); } - @Test - void disabled_cache() { - ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder() - .setValue(true) - .setVariant(BOOL_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder() - .setValue(STRING_VALUE) - .setVariant(STRING_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveIntResponse intResponse = ResolveIntResponse.newBuilder() - .setValue(INT_VALUE) - .setVariant(INT_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder() - .setValue(DOUBLE_VALUE) - .setVariant(DOUBLE_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder() - .setValue(PROTOBUF_STRUCTURE_VALUE) - .setVariant(OBJECT_VARIANT) - .setReason(STATIC_REASON) - .build(); - - ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class); - ServiceStub serviceStubMock = mock(ServiceStub.class); - when(serviceStubMock.withWaitForReady()).thenReturn(serviceStubMock); - when(serviceStubMock.withDeadline(any(Deadline.class))) - .thenReturn(serviceStubMock); - when(serviceBlockingStubMock.withWaitForReady()).thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.withDeadline(any(Deadline.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock.withDeadlineAfter(anyLong(), any(TimeUnit.class))) - .thenReturn(serviceBlockingStubMock); - when(serviceBlockingStubMock - .resolveBoolean(argThat(x -> FLAG_KEY_BOOLEAN.equals(x.getFlagKey())))) - .thenReturn(booleanResponse); - when(serviceBlockingStubMock - .resolveFloat(argThat(x -> FLAG_KEY_DOUBLE.equals(x.getFlagKey())))) - .thenReturn(floatResponse); - when(serviceBlockingStubMock - .resolveInt(argThat(x -> FLAG_KEY_INTEGER.equals(x.getFlagKey())))) - .thenReturn(intResponse); - when(serviceBlockingStubMock - .resolveString(argThat(x -> FLAG_KEY_STRING.equals(x.getFlagKey())))) - .thenReturn(stringResponse); - when(serviceBlockingStubMock - .resolveObject(argThat(x -> FLAG_KEY_OBJECT.equals(x.getFlagKey())))) - .thenReturn(objectResponse); - - // disabled cache - final Cache cache = new Cache("disabled", 0); - - GrpcConnector grpc; - try (MockedStatic mockStaticService = mockStatic(ServiceGrpc.class)) { - mockStaticService.when(() -> ServiceGrpc.newBlockingStub(any(Channel.class))) - .thenReturn(serviceBlockingStubMock); - mockStaticService.when(() -> ServiceGrpc.newStub(any())) - .thenReturn(serviceStubMock); - - class NoopInitGrpcConnector extends GrpcConnector { - public NoopInitGrpcConnector(FlagdOptions options, Cache cache, - Supplier connectedSupplier, - Consumer onConnectionEvent) { - super(options, cache, connectedSupplier, onConnectionEvent); - } - - public void initialize() throws Exception { - }; - } - - grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, - (connectionEvent) -> { - }); - } - - FlagdProvider provider = createProvider(grpc, cache, () -> true); - - try { - provider.initialize(null); - } catch (Exception e) { - // ignore exception if any - } + FlagdProvider provider = createProvider(grpc, cache, () -> true); - OpenFeatureAPI.getInstance().setProviderAndWait(provider); - - HashMap flagsMap = new HashMap<>(); - HashMap structMap = new HashMap<>(); - - flagsMap.put("foo", com.google.protobuf.Value.newBuilder().setStringValue("foo") - .build()); // assert that a configuration_change event works - - structMap.put("flags", com.google.protobuf.Value.newBuilder() - .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); - - // should not cache results - FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, - false); - FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, - "wrong"); - FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, - new Value()); - - // assert values are not cached - booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); - assertTrue(booleanDetails.getValue()); - assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); - assertEquals(STATIC_REASON, booleanDetails.getReason()); - - stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); - assertEquals(STRING_VALUE, stringDetails.getValue()); - assertEquals(STRING_VARIANT, stringDetails.getVariant()); - assertEquals(STATIC_REASON, stringDetails.getReason()); - - intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); - assertEquals(INT_VALUE, intDetails.getValue()); - assertEquals(INT_VARIANT, intDetails.getVariant()); - assertEquals(STATIC_REASON, intDetails.getReason()); - - floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); - assertEquals(DOUBLE_VALUE, floatDetails.getValue()); - assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); - assertEquals(STATIC_REASON, floatDetails.getReason()); - - objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); - assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() - .asMap().get(INNER_STRUCT_KEY).asString()); - assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); - assertEquals(STATIC_REASON, objectDetails.getReason()); + try { + provider.initialize(null); + } catch (Exception e) { + // ignore exception if any } - @Test - void contextMerging() throws Exception { - // given - final FlagdProvider provider = new FlagdProvider(); - - final Resolver resolverMock = mock(Resolver.class); - - Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - flagResolver.set(provider, resolverMock); - - final HashMap globalCtxMap = new HashMap<>(); - globalCtxMap.put("id", new Value("GlobalID")); - globalCtxMap.put("env", new Value("A")); - - final HashMap localCtxMap = new HashMap<>(); - localCtxMap.put("id", new Value("localID")); - localCtxMap.put("client", new Value("999")); - - final HashMap expectedCtx = new HashMap<>(); - expectedCtx.put("id", new Value("localID")); - expectedCtx.put("env", new Value("A")); - localCtxMap.put("client", new Value("999")); - - // when - provider.initialize(new ImmutableContext(globalCtxMap)); - provider.getBooleanEvaluation("ket", false, new ImmutableContext(localCtxMap)); - - // then - verify(resolverMock).booleanEvaluation(any(), any(), argThat( - ctx -> ctx.asMap().entrySet().containsAll(expectedCtx.entrySet()))); + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + HashMap flagsMap = new HashMap<>(); + HashMap structMap = new HashMap<>(); + + flagsMap.put("foo", com.google.protobuf.Value.newBuilder().setStringValue("foo") + .build()); // assert that a configuration_change event works + + structMap.put("flags", com.google.protobuf.Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(flagsMap)).build()); + + // should not cache results + FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, + false); + FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, + "wrong"); + FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, + new Value()); + + // assert values are not cached + booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); + assertTrue(booleanDetails.getValue()); + assertEquals(BOOL_VARIANT, booleanDetails.getVariant()); + assertEquals(STATIC_REASON, booleanDetails.getReason()); + + stringDetails = api.getClient().getStringDetails(FLAG_KEY_STRING, "wrong"); + assertEquals(STRING_VALUE, stringDetails.getValue()); + assertEquals(STRING_VARIANT, stringDetails.getVariant()); + assertEquals(STATIC_REASON, stringDetails.getReason()); + + intDetails = api.getClient().getIntegerDetails(FLAG_KEY_INTEGER, 0); + assertEquals(INT_VALUE, intDetails.getValue()); + assertEquals(INT_VARIANT, intDetails.getVariant()); + assertEquals(STATIC_REASON, intDetails.getReason()); + + floatDetails = api.getClient().getDoubleDetails(FLAG_KEY_DOUBLE, 0.1); + assertEquals(DOUBLE_VALUE, floatDetails.getValue()); + assertEquals(DOUBLE_VARIANT, floatDetails.getVariant()); + assertEquals(STATIC_REASON, floatDetails.getReason()); + + objectDetails = api.getClient().getObjectDetails(FLAG_KEY_OBJECT, new Value()); + assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asStructure() + .asMap().get(INNER_STRUCT_KEY).asString()); + assertEquals(OBJECT_VARIANT, objectDetails.getVariant()); + assertEquals(STATIC_REASON, objectDetails.getReason()); + } + + @Test + void initializationAndShutdown() throws Exception { + // given + final FlagdProvider provider = new FlagdProvider(); + final EvaluationContext ctx = new ImmutableContext(); + + final Resolver resolverMock = mock(Resolver.class); + + Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + flagResolver.set(provider, resolverMock); + + // when + + // validate multiple initialization + provider.initialize(ctx); + provider.initialize(ctx); + + // validate multiple shutdowns + provider.shutdown(); + provider.shutdown(); + + // then + verify(resolverMock, times(1)).init(); + verify(resolverMock, times(1)).shutdown(); + } + + @Test + void contextEnrichment() throws Exception { + + final EvaluationContext ctx = new ImmutableContext(); + String key = "key1"; + String val = "val1"; + MutableStructure metadata = new MutableStructure(); + metadata.add(key, val); + // given + final Function mockEnricher = mock(Function.class); + + // mock a resolver + try (MockedConstruction mockResolver = mockConstruction(InProcessResolver.class, + (mock, context) -> { + Consumer onConnectionEvent; + + // get a reference to the onConnectionEvent callback + onConnectionEvent = (Consumer) context + .arguments().get(2); + + // when our mock resolver initializes, it runs the passed onConnectionEvent + // callback + doAnswer(invocation -> { + onConnectionEvent.accept( + new ConnectionEvent(true, metadata)); + return null; + }).when(mock).init(); + })) { + + final FlagdProvider provider = new FlagdProvider( + FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).contextEnricher(mockEnricher).build()); + provider.initialize(ctx); + + // the enricher should run with init events, and be passed the metadata + verify(mockEnricher).apply(argThat(arg -> arg.getValue(key).asString().equals(val))); } + } + + @Test + void updatesSyncMetadataWithCallback() throws Exception { + + final EvaluationContext ctx = new ImmutableContext(); + String key = "key1"; + String val = "val1"; + // Map metadataMap = new HashMap<>(); + // metadataMap.put(key, val); + MutableStructure metadata = new MutableStructure(); + metadata.add(key, val); + + // mock a resolver + try (MockedConstruction mockResolver = mockConstruction(InProcessResolver.class, + (mock, context) -> { + Consumer onConnectionEvent; + + // get a reference to the onConnectionEvent callback + onConnectionEvent = (Consumer) context + .arguments().get(2); + + // when our mock resolver initializes, it runs the passed onConnectionEvent + // callback + doAnswer(invocation -> { + onConnectionEvent.accept( + new ConnectionEvent(true, metadata)); + return null; + }).when(mock).init(); + })) { + + FlagdProvider provider = new FlagdProvider(FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).build()); + provider.initialize(ctx); + + // the onConnectionEvent should have updated the sync metadata + assertEquals(val, provider.getSyncMetadata().getValue(key).asString()); + } + } - @Test - void initializationAndShutdown() throws Exception { - // given - final FlagdProvider provider = new FlagdProvider(); - final EvaluationContext ctx = new ImmutableContext(); + // test helper - final Resolver resolverMock = mock(Resolver.class); + // create provider with given grpc connector + private FlagdProvider createProvider(GrpcConnector grpc) { + return createProvider(grpc, () -> true); + } - Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - flagResolver.set(provider, resolverMock); + // create provider with given grpc provider and state supplier + private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) { + final Cache cache = new Cache("lru", 5); - // when + return createProvider(grpc, cache, getConnected); + } - // validate multiple initialization - provider.initialize(ctx); - provider.initialize(ctx); + // create provider with given grpc provider, cache and state supplier + private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getConnected) { + final FlagdOptions flagdOptions = FlagdOptions.builder().build(); + final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, getConnected, + (connectionEvent) -> { + }); - // validate multiple shutdowns - provider.shutdown(); - provider.shutdown(); + final FlagdProvider provider = new FlagdProvider(); - // then - verify(resolverMock, times(1)).init(); - verify(resolverMock, times(1)).shutdown(); - } + try { + Field connector = GrpcResolver.class.getDeclaredField("connector"); + connector.setAccessible(true); + connector.set(grpcResolver, grpc); - @Test - void updatesSyncMetadataWithCallback() throws Exception { - - final EvaluationContext ctx = new ImmutableContext(); - String key = "key1"; - String val = "val1"; - Map metadata = new HashMap<>(); - metadata.put(key, val); - - // mock a resolver - try (MockedConstruction mockResolver = mockConstruction(GrpcResolver.class, - (mock, context) -> { - Consumer onConnectionEvent; - - // get a reference to the onConnectionEvent callback - onConnectionEvent = (Consumer) context - .arguments().get(3); - - // when our mock resolver initializes, it runs the passed onConnectionEvent - // callback - doAnswer(invocation -> { - onConnectionEvent.accept( - new ConnectionEvent(true, metadata)); - return null; - }).when(mock).init(); - })) { - - FlagdProvider provider = new FlagdProvider(); - provider.initialize(ctx); - - // the onConnectionEvent should have updated the sync metadata - assertEquals(val, provider.getSyncMetadata().get(key)); - } + Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + flagResolver.set(provider, grpcResolver); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); } - // test helper - - // create provider with given grpc connector - private FlagdProvider createProvider(GrpcConnector grpc) { - return createProvider(grpc, () -> true); + return provider; + } + + // Create an in process provider + private FlagdProvider createInProcessProvider() { + + final FlagdOptions flagdOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .deadline(1000) + .build(); + final FlagdProvider provider = new FlagdProvider(flagdOptions); + final MockStorage mockStorage = new MockStorage(new HashMap(), + new LinkedBlockingQueue( + Arrays.asList(new StorageStateChange(StorageState.OK)))); + + try { + final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + final Resolver resolver = (Resolver) flagResolver.get(provider); + + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, mockStorage); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); } - // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) { - final Cache cache = new Cache("lru", 5); - - return createProvider(grpc, cache, getConnected); - } - - // create provider with given grpc provider, cache and state supplier - private FlagdProvider createProvider(GrpcConnector grpc, Cache cache, Supplier getConnected) { - final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, getConnected, - (connectionEvent) -> { - }); - - final FlagdProvider provider = new FlagdProvider(); - - try { - Field connector = GrpcResolver.class.getDeclaredField("connector"); - connector.setAccessible(true); - connector.set(grpcResolver, grpc); - - Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - flagResolver.set(provider, grpcResolver); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return provider; - } - - // Create an in process provider - private FlagdProvider createInProcessProvider() { - - final FlagdOptions flagdOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .deadline(1000) - .build(); - final FlagdProvider provider = new FlagdProvider(flagdOptions); - final MockStorage mockStorage = new MockStorage(new HashMap(), - new LinkedBlockingQueue( - Arrays.asList(new StorageStateChange(StorageState.OK)))); - - try { - final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - final Resolver resolver = (Resolver) flagResolver.get(provider); - - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, mockStorage); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return provider; - } + return provider; + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHookTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHookTest.java new file mode 100644 index 000000000..731164b2b --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHookTest.java @@ -0,0 +1,36 @@ +package dev.openfeature.contrib.providers.flagd; + +import static org.junit.Assert.assertEquals; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.junit.Test; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FlagValueType; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.MutableContext; + +/** + * SyncMetadataHookTest + */ +public class SyncMetadataHookTest { + + @Test + public void shouldCallContextSupplierAndReturnContext() { + MutableContext suppliedContext = new MutableContext(); + String key1 = "key1"; + String val1 = "val1"; + Supplier contextSupplier = () -> suppliedContext; + + suppliedContext.add(key1, val1); + + // when(contextSupplier.get()).thenReturn(new ImmutableContext("some-key")); + SyncMetadataHook hook = new SyncMetadataHook(contextSupplier); + Optional context = hook.before(HookContext.builder().flagKey("some-flag").defaultValue(false) + .type(FlagValueType.BOOLEAN).ctx(new ImmutableContext()).build(), null); + assertEquals(val1, context.get().getValue(key1).asString()); + } +} \ No newline at end of file diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index b66c65d3c..4b9bd824e 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -1,5 +1,34 @@ package dev.openfeature.contrib.providers.flagd.resolver.process; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.BOOLEAN_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DISABLED_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DOUBLE_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_IF_IN_TARGET; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_INVALID_TARGET; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_SHORTHAND_TARGETING; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WITH_TARGETING_KEY; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.INT_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -13,45 +42,13 @@ import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.MutableStructure; import dev.openfeature.sdk.ProviderEvaluation; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.BOOLEAN_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DISABLED_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.DOUBLE_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_IF_IN_TARGET; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_INVALID_TARGET; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WIH_SHORTHAND_TARGETING; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.FLAG_WITH_TARGETING_KEY; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.INT_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG; -import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; - class InProcessResolverTest { @Test @@ -79,8 +76,8 @@ public void eventHandling() throws Throwable { final BlockingQueue receiver = new LinkedBlockingQueue<>(5); final String key = "key1"; final String val = "val1"; - final Map syncMetadata = new HashMap<>(); - syncMetadata.put(key, val); + final MutableStructure syncMetadata = new MutableStructure(); + syncMetadata.add(key, val); InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), (connectionEvent) -> receiver.offer(new StorageStateChange( @@ -107,7 +104,7 @@ public void eventHandling() throws Throwable { assertTimeoutPreemptively(Duration.ofMillis(200), () -> { StorageStateChange storageState = receiver.take(); assertEquals(StorageState.OK, storageState.getStorageState()); - assertEquals(val, storageState.getSyncMetadata().get(key)); + assertEquals(val, storageState.getSyncMetadata().getValue(key).asString()); }); assertTimeoutPreemptively(Duration.ofMillis(200), () -> { From 90fa155761780d65883cfe7abb81bc5b70bbc7d9 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 30 Sep 2024 17:26:59 -0400 Subject: [PATCH 2/5] Update providers/flagd/README.md Signed-off-by: Todd Baert --- providers/flagd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index fece0f9af..d4c84fcc4 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -180,7 +180,7 @@ caching with up to 1000 entries. ##### Context enrichment The `contextEnricher` option is a function which provides a context to be added to each evaluation. -This function runs on the initial provider connection and ever reconnection, and is passed the [sync-metadata](#sync-metadata). +This function runs on the initial provider connection and every reconnection, and is passed the [sync-metadata](#sync-metadata). By default, a simple implementation which uses the sync-metadata payload its entirety is used. ### OpenTelemetry tracing (RPC only) From acef9760b97feb0429e377b1922ade6c11b16bae Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 30 Sep 2024 17:27:34 -0400 Subject: [PATCH 3/5] Update providers/flagd/README.md Signed-off-by: Todd Baert --- providers/flagd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index d4c84fcc4..c284305c8 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -181,7 +181,7 @@ caching with up to 1000 entries. The `contextEnricher` option is a function which provides a context to be added to each evaluation. This function runs on the initial provider connection and every reconnection, and is passed the [sync-metadata](#sync-metadata). -By default, a simple implementation which uses the sync-metadata payload its entirety is used. +By default, a simple implementation which uses the sync-metadata payload in its entirety is used. ### OpenTelemetry tracing (RPC only) From 20300abdd0c8fe91743151d6b72dff5dfdd925e3 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 1 Oct 2024 09:26:30 -0400 Subject: [PATCH 4/5] Update providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java Signed-off-by: Todd Baert --- .../openfeature/contrib/providers/flagd/SyncMetadataHook.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java index 77c3e03e4..a7ee75d0b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/SyncMetadataHook.java @@ -17,7 +17,7 @@ class SyncMetadataHook implements Hook { } /** - * Return the enriched context, including the additional attributes from the sync-metadata. + * Return the context adapted from the sync-metadata provided by the supplier. */ @Override public Optional before(HookContext ctx, Map hints) { From 6390421088ccfe4dcd9fbdcc0c0f56254856b6d9 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 2 Oct 2024 15:44:03 -0400 Subject: [PATCH 5/5] fixup: add more asserts Signed-off-by: Todd Baert --- .../providers/flagd/FlagdProvider.java | 4 ++++ .../providers/flagd/FlagdProviderTest.java | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java index 4346d6ae9..7b451ec91 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java @@ -146,6 +146,10 @@ protected Structure getSyncMetadata() { return new ImmutableStructure(syncMetadata.asMap()); } + /** + * The updated context mixed into all evaluations based on the sync-metadata. + * @return context + */ EvaluationContext getEnrichedContext() { return enrichedContext; } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java index bf179b5bd..2a5850172 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java @@ -28,6 +28,10 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicReference; +import java.util.Collections; +import java.util.Optional; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -57,6 +61,8 @@ import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceStub; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.FlagValueType; +import dev.openfeature.sdk.HookContext; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.ImmutableMetadata; import dev.openfeature.sdk.MutableContext; @@ -909,12 +915,10 @@ void contextEnrichment() throws Exception { @Test void updatesSyncMetadataWithCallback() throws Exception { - + // given final EvaluationContext ctx = new ImmutableContext(); String key = "key1"; String val = "val1"; - // Map metadataMap = new HashMap<>(); - // metadataMap.put(key, val); MutableStructure metadata = new MutableStructure(); metadata.add(key, val); @@ -936,11 +940,19 @@ void updatesSyncMetadataWithCallback() throws Exception { }).when(mock).init(); })) { - FlagdProvider provider = new FlagdProvider(FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).build()); + FlagdProvider provider = new FlagdProvider( + FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS).build()); provider.initialize(ctx); - // the onConnectionEvent should have updated the sync metadata + // the onConnectionEvent should have updated the sync metadata and the assertEquals(val, provider.getSyncMetadata().getValue(key).asString()); + assertEquals(val, provider.getEnrichedContext().getValue(key).asString()); + + // call the hook manually and make sure the enriched context is returned + Optional contextFromHook = provider.getProviderHooks().get(0) + .before(HookContext.builder().flagKey("some-flag").defaultValue(false) + .type(FlagValueType.BOOLEAN).ctx(new ImmutableContext()).build(), Collections.emptyMap()); + assertEquals(val, contextFromHook.get().getValue(key).asString()); } }