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 3532bf3eb..679168533 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,10 +1,10 @@ package dev.openfeature.contrib.providers.flagd; import java.util.Collections; -import java.util.List; import java.util.Map; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; @@ -145,17 +145,16 @@ private boolean isConnected() { return this.connected; } - private void onConnectionEvent(boolean newConnectedState, List changedFlagKeys, - Map syncMetadata) { + private void onConnectionEvent(ConnectionEvent connectionEvent) { boolean previous = connected; - boolean current = newConnectedState; - this.connected = newConnectedState; - this.syncMetadata = syncMetadata; + boolean current = connected = connectionEvent.isConnected(); + syncMetadata = connectionEvent.getSyncMetadata(); // configuration changed if (initialized && previous && current) { log.debug("Configuration changed"); - ProviderEventDetails details = ProviderEventDetails.builder().flagsChanged(changedFlagKeys) + ProviderEventDetails details = ProviderEventDetails.builder() + .flagsChanged(connectionEvent.getFlagsChanged()) .message("configuration changed").build(); this.emitProviderConfigurationChanged(details); return; 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 new file mode 100644 index 000000000..cc8a7a0b0 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java @@ -0,0 +1,68 @@ +package dev.openfeature.contrib.providers.flagd.resolver.common; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Event payload for a + * {@link dev.openfeature.contrib.providers.flagd.resolver.Resolver} connection + * state change event. + */ +@AllArgsConstructor +public class ConnectionEvent { + @Getter + private final boolean connected; + private final List flagsChanged; + private final Map syncMetadata; + + /** + * Construct a new ConnectionEvent. + * + * @param connected status of the connection + */ + public ConnectionEvent(boolean connected) { + this(connected, Collections.emptyList(), Collections.emptyMap()); + } + + /** + * Construct a new ConnectionEvent. + * + * @param connected status of the connection + * @param flagsChanged list of flags changed + */ + public ConnectionEvent(boolean connected, List flagsChanged) { + this(connected, flagsChanged, Collections.emptyMap()); + } + + /** + * Construct a new ConnectionEvent. + * + * @param connected status of the connection + * @param syncMetadata sync.getMetadata + */ + public ConnectionEvent(boolean connected, Map syncMetadata) { + this(connected, Collections.emptyList(), syncMetadata); + } + + /** + * Get changed flags. + * + * @return an unmodifiable view of the changed flags + */ + public List getFlagsChanged() { + return Collections.unmodifiableList(flagsChanged); + } + + /** + * Get changed sync metadata. + * + * @return an unmodifiable view of the sync metadata + */ + public Map getSyncMetadata() { + return Collections.unmodifiableMap(syncMetadata); + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java index 9f99bae1d..ed85e8010 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnector.java @@ -2,19 +2,19 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Supplier; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelBuilder; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.common.Util; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; -import dev.openfeature.sdk.internal.TriConsumer; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.grpc.ManagedChannel; import io.grpc.stub.StreamObserver; @@ -38,7 +38,7 @@ public class GrpcConnector { private final long deadline; private final Cache cache; - private final TriConsumer, Map> onConnectionEvent; + private final Consumer onConnectionEvent; private final Supplier connectedSupplier; private int eventStreamAttempt = 1; @@ -56,7 +56,7 @@ public class GrpcConnector { * @param onConnectionEvent lambda which handles changes in the connection/stream */ public GrpcConnector(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier, - TriConsumer, Map> onConnectionEvent) { + Consumer onConnectionEvent) { this.channel = ChannelBuilder.nettyChannel(options); this.serviceStub = ServiceGrpc.newStub(channel); this.serviceBlockingStub = ServiceGrpc.newBlockingStub(channel); @@ -105,7 +105,7 @@ public void shutdown() throws Exception { this.channel.awaitTermination(this.deadline, TimeUnit.MILLISECONDS); log.warn(String.format("Unable to shut down channel by %d deadline", this.deadline)); } - this.onConnectionEvent.accept(false, Collections.emptyList(), Collections.emptyMap()); + this.onConnectionEvent.accept(new ConnectionEvent(false, Collections.emptyList(), Collections.emptyMap())); } } @@ -166,6 +166,6 @@ private void onConnectionEvent(final boolean connected, final List chang this.eventStreamRetryBackoff = this.startEventStreamRetryBackoff; } // chain to initiator - this.onConnectionEvent.accept(connected, changedFlags, Collections.emptyMap()); + this.onConnectionEvent.accept(new ConnectionEvent(connected, changedFlags)); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java index e675f029c..9fcede67e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcResolver.java @@ -5,8 +5,8 @@ import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.getField; import static dev.openfeature.contrib.providers.flagd.resolver.common.Convert.getFieldDescriptor; -import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -16,6 +16,7 @@ import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveFactory; import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveStrategy; @@ -33,7 +34,6 @@ import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; -import dev.openfeature.sdk.internal.TriConsumer; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; @@ -61,7 +61,7 @@ public final class GrpcResolver implements Resolver { * @param onConnectionEvent lambda which handles changes in the connection/stream */ public GrpcResolver(final FlagdOptions options, final Cache cache, final Supplier connectedSupplier, - final TriConsumer, Map> onConnectionEvent) { + final Consumer onConnectionEvent) { this.cache = cache; this.connectedSupplier = connectedSupplier; this.strategy = ResolveFactory.getStrategy(options); diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 2affac522..39c77f01b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -2,13 +2,12 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.common.Util; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore; @@ -27,7 +26,6 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; -import dev.openfeature.sdk.internal.TriConsumer; import lombok.extern.slf4j.Slf4j; /** @@ -38,7 +36,7 @@ @Slf4j public class InProcessResolver implements Resolver { private final Storage flagStore; - private final TriConsumer, Map> onConnectionEvent; + private final Consumer onConnectionEvent; private final Operator operator; private final long deadline; private final ImmutableMetadata metadata; @@ -56,7 +54,7 @@ public class InProcessResolver implements Resolver { * connection/stream */ public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier, - TriConsumer, Map> onConnectionEvent) { + Consumer onConnectionEvent) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.onConnectionEvent = onConnectionEvent; @@ -79,11 +77,11 @@ public void init() throws Exception { final StorageStateChange storageStateChange = flagStore.getStateQueue().take(); switch (storageStateChange.getStorageState()) { case OK: - onConnectionEvent.accept(true, storageStateChange.getChangedFlagsKeys(), - storageStateChange.getSyncMetadata()); + onConnectionEvent.accept(new ConnectionEvent(true, storageStateChange.getChangedFlagsKeys(), + storageStateChange.getSyncMetadata())); break; case ERROR: - onConnectionEvent.accept(false, Collections.emptyList(), Collections.emptyMap()); + onConnectionEvent.accept(new ConnectionEvent(false)); break; default: log.info(String.format("Storage emitted unhandled status: %s", @@ -109,7 +107,7 @@ public void init() throws Exception { */ public void shutdown() throws InterruptedException { flagStore.shutdown(); - onConnectionEvent.accept(false, Collections.emptyList(), Collections.emptyMap()); + onConnectionEvent.accept(new ConnectionEvent(false)); } /** 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 14b7779b8..269c14d9d 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 @@ -27,6 +27,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.concurrent.LinkedBlockingQueue; @@ -40,6 +41,7 @@ import com.google.protobuf.Struct; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcConnector; import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; @@ -68,885 +70,942 @@ import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.internal.TriConsumer; 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); - } - - @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, TriConsumer, Map> onConnectionEvent) { - super(options, cache, connectedSupplier, onConnectionEvent); - } + 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); - public void initialize() throws Exception {}; - } + // then + final ImmutableMetadata metadata = booleanDetails.getFlagMetadata(); - grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys, syncMetadata) -> { - }); + assertEquals("flagd-scope", metadata.getString("scope")); + assertEquals(true, metadata.getBoolean("boolean")); + assertEquals(1, metadata.getDouble("number")); } - 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 resolvers_cache_responses_if_static_and_event_stream_alive() { + do_resolvers_cache_responses(STATIC_REASON, true, true); } - 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))) + @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); - mockStaticService.when(() -> ServiceGrpc.newStub(any())) - .thenReturn(serviceStubMock); + 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); - class NoopInitGrpcConnector extends GrpcConnector { - public NoopInitGrpcConnector(FlagdOptions options, Cache cache, - Supplier connectedSupplier, - TriConsumer, Map> onConnectionEvent) { - super(options, cache, connectedSupplier, onConnectionEvent); + 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); + } + + public void initialize() throws Exception { + }; } - public void initialize() throws Exception { - }; + grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, + (connectionEvent) -> { + }); + } + + 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; } - grpc = new NoopInitGrpcConnector(FlagdOptions.builder().build(), cache, () -> true, (state, changedFlagKeys, syncMetadata) -> { - }); + 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()); } - FlagdProvider provider = createProvider(grpc, cache, () -> true); + @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 + } - try { - provider.initialize(null); - } catch (Exception e) { - // ignore exception if any + 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()); } - OpenFeatureAPI.getInstance().setProviderAndWait(provider); + @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")); - 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 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()))); - } - - @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 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) -> { - TriConsumer, Map> onConnectionEvent; - - // get a reference to the onConnectionEvent callback - onConnectionEvent = (TriConsumer, Map>) context - .arguments().get(3); - - // when our mock resolver initializes, it runs the passed onConnectionEvent callback - doAnswer(invocation -> { - onConnectionEvent.accept(true, Collections.emptyList(), - 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)); + 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()))); } - } - // test helper + @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 - // create provider with given grpc connector - private FlagdProvider createProvider(GrpcConnector grpc) { - return createProvider(grpc, () -> true); - } + // validate multiple initialization + provider.initialize(ctx); + provider.initialize(ctx); - // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) { - final Cache cache = new Cache("lru", 5); + // validate multiple shutdowns + provider.shutdown(); + provider.shutdown(); - return createProvider(grpc, cache, getConnected); - } + // then + verify(resolverMock, times(1)).init(); + verify(resolverMock, times(1)).shutdown(); + } - // 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, - (providerState, changedFlagKeys, syncMetadata) -> { - }); + @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)); + } + } - final FlagdProvider provider = new FlagdProvider(); + // test helper + + // create provider with given grpc connector + private FlagdProvider createProvider(GrpcConnector grpc) { + return createProvider(grpc, () -> true); + } - try { - Field connector = GrpcResolver.class.getDeclaredField("connector"); - connector.setAccessible(true); - connector.set(grpcResolver, grpc); + // create provider with given grpc provider and state supplier + private FlagdProvider createProvider(GrpcConnector grpc, Supplier getConnected) { + final Cache cache = new Cache("lru", 5); - Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - flagResolver.set(provider, grpcResolver); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); + return createProvider(grpc, cache, getConnected); } - 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, 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; } - 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; + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java index 29051abe9..aeeb0246d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/GrpcConnectorTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; @@ -33,6 +34,7 @@ import org.mockito.invocation.InvocationOnMock; import dev.openfeature.contrib.providers.flagd.FlagdOptions; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; @@ -65,7 +67,7 @@ void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAcces doAnswer(invocation -> null).when(mockStub).eventStream(any(), any()); final GrpcConnector connector = new GrpcConnector(options, cache, () -> true, - (state, changedFlagKeys, syncMetadata) -> { + (connectionEvent) -> { }); Field serviceStubField = GrpcConnector.class.getDeclaredField("serviceStub"); @@ -98,7 +100,7 @@ void validate_retry_calls(int retries) throws NoSuchFieldException, IllegalAcces void initialization_succeed_with_connected_status() throws NoSuchFieldException, IllegalAccessException { final Cache cache = new Cache("disabled", 0); final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); - TriConsumer, Map> onConnectionEvent = mock(TriConsumer.class); + Consumer onConnectionEvent = mock(Consumer.class); doAnswer((InvocationOnMock invocation) -> { EventStreamObserver eventStreamObserver = (EventStreamObserver) invocation.getArgument(1); eventStreamObserver @@ -123,9 +125,8 @@ void initialization_succeed_with_connected_status() throws NoSuchFieldException, onConnectionEvent); assertDoesNotThrow(connector::initialize); - - // assert that onConnectionEvent was called with true - verify(onConnectionEvent).accept(argThat(arg -> arg), any(), any()); + // assert that onConnectionEvent is connected + verify(onConnectionEvent).accept(argThat(arg -> arg.isConnected())); } } @@ -133,7 +134,7 @@ void initialization_succeed_with_connected_status() throws NoSuchFieldException, void initialization_fail_with_timeout() throws Exception { final Cache cache = new Cache("disabled", 0); final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); - TriConsumer, Map> onConnectionEvent = mock(TriConsumer.class); + Consumer onConnectionEvent = mock(Consumer.class); doAnswer(invocation -> null).when(mockStub).eventStream(any(), any()); final GrpcConnector connector = new GrpcConnector(FlagdOptions.builder().build(), cache, () -> false, @@ -141,8 +142,8 @@ void initialization_fail_with_timeout() throws Exception { // assert throws assertThrows(RuntimeException.class, connector::initialize); - // assert that onConnectionEvent was called with false - verify(onConnectionEvent).accept(argThat(arg -> !arg), any(), any()); + // assert that onConnectionEvent is not connected + verify(onConnectionEvent).accept(argThat(arg -> !arg.isConnected())); } @Test 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 dfa2051f9..b66c65d3c 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 @@ -2,6 +2,7 @@ import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; +import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.MockConnector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; @@ -19,7 +20,6 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; -import dev.openfeature.sdk.internal.TriConsumer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -34,6 +34,7 @@ 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; @@ -53,404 +54,405 @@ class InProcessResolverTest { - @Test - public void connectorSetup() { - // given - FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .host("localhost") - .port(8080).build(); - FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .offlineFlagSourcePath("path").build(); - FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .customConnector(new MockConnector(null)).build(); - - // then - assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); - assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); - assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); - } - - @Test - public void eventHandling() throws Throwable { - // given - // note - queues with adequate capacity - final BlockingQueue sender = new LinkedBlockingQueue<>(5); - final BlockingQueue receiver = new LinkedBlockingQueue<>(5); - final String key = "key1"; - final String val = "val1"; - final Map syncMetadata = new HashMap<>(); - syncMetadata.put(key, val); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), - (connectedState, changedFlagKeys, sm) -> receiver.offer(new StorageStateChange( - connectedState ? StorageState.OK : StorageState.ERROR, changedFlagKeys, sm))); - - // when - init and emit events - Thread initThread = new Thread(() -> { - try { - inProcessResolver.init(); - } catch (Exception e) { - } - }); - initThread.start(); - if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), 100, - TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); - } - if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); + @Test + public void connectorSetup() { + // given + FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .host("localhost") + .port(8080).build(); + FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("path").build(); + FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .customConnector(new MockConnector(null)).build(); + + // then + assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); + assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); + assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); } - // then - receive events in order - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - StorageStateChange storageState = receiver.take(); - assertEquals(StorageState.OK, storageState.getStorageState()); - assertEquals(val, storageState.getSyncMetadata().get(key)); - }); - - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - assertEquals(StorageState.ERROR, receiver.take().getStorageState()); - }); - } - - @Test - public void simpleBooleanResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { + @Test + public void eventHandling() throws Throwable { + // given + // note - queues with adequate capacity + final BlockingQueue sender = new LinkedBlockingQueue<>(5); + final BlockingQueue receiver = new LinkedBlockingQueue<>(5); + final String key = "key1"; + final String val = "val1"; + final Map syncMetadata = new HashMap<>(); + syncMetadata.put(key, val); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), + (connectionEvent) -> receiver.offer(new StorageStateChange( + connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, + connectionEvent.getFlagsChanged(), connectionEvent.getSyncMetadata()))); + + // when - init and emit events + Thread initThread = new Thread(() -> { + try { + inProcessResolver.init(); + } catch (Exception e) { + } + }); + initThread.start(); + if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), 100, + TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + + // then - receive events in order + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + StorageStateChange storageState = receiver.take(); + assertEquals(StorageState.OK, storageState.getStorageState()); + assertEquals(val, storageState.getSyncMetadata().get(key)); }); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", - false, - new ImmutableContext()); - - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("on", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } - - @Test - public void simpleDoubleResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + assertEquals(StorageState.ERROR, receiver.take().getStorageState()); }); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, - new ImmutableContext()); + @Test + public void simpleBooleanResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("on", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(3.141d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void simpleDoubleResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - @Test - public void fetchIntegerAsDouble() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, - new ImmutableContext()); + // then + assertEquals(3.141d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(3, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void fetchIntegerAsDouble() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - @Test - public void fetchDoubleAsInt() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, + new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, - new ImmutableContext()); + // then + assertEquals(3, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // then - assertEquals(1d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + @Test + public void fetchDoubleAsInt() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - @Test - public void simpleIntResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, + new ImmutableContext()); + + // then + assertEquals(1d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, - new ImmutableContext()); + @Test + public void simpleIntResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - // then - assertEquals(1, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void simpleObjectResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("objectFlag", OBJECT_FLAG); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, + new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // then + assertEquals(1, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - Map typeDefault = new HashMap<>(); - typeDefault.put("key", "0164"); - typeDefault.put("date", "01.01.1990"); + @Test + public void simpleObjectResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("objectFlag", OBJECT_FLAG); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", - Value.objectToValue(typeDefault), new ImmutableContext()); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - // then - Value value = providerEvaluation.getValue(); - Map valueMap = value.asStructure().asMap(); + Map typeDefault = new HashMap<>(); + typeDefault.put("key", "0164"); + typeDefault.put("date", "01.01.1990"); - assertEquals("0165", valueMap.get("key").asString()); - assertEquals("01.01.2000", valueMap.get("date").asString()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - assertEquals("typeA", providerEvaluation.getVariant()); - } + // when + ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", + Value.objectToValue(typeDefault), new ImmutableContext()); - @Test - public void missingFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); + // then + Value value = providerEvaluation.getValue(); + Map valueMap = value.asStructure().asMap(); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + assertEquals("0165", valueMap.get("key").asString()); + assertEquals("01.01.2000", valueMap.get("date").asString()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + assertEquals("typeA", providerEvaluation.getVariant()); + } - // when/then - ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, - new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); - } + @Test + public void missingFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); - @Test - public void disabledFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("disabledFlag", DISABLED_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // when/then + ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); + } - // when/then - ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, - new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); - } + @Test + public void disabledFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("disabledFlag", DISABLED_FLAG); - @Test - public void variantMismatchFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // when/then + ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); - }); - } + @Test + public void variantMismatchFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - @Test - public void typeMismatchEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", BOOLEAN_FLAG); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); }); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); - }); - } + @Test + public void typeMismatchEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", BOOLEAN_FLAG); - @Test - public void booleanShorthandEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); }); + } - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, - new ImmutableContext()); + @Test + public void booleanShorthandEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("true", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void targetingMatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, + new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("true", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", - "loopAlg", - new MutableContext().add("email", "abc@faas.com")); - - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } - - @Test - public void targetingUnmatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + @Test + public void targetingMatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@faas.com")); + + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", - "loopAlg", - new MutableContext().add("email", "abc@abc.com")); - - // then - assertEquals("loopAlg", providerEvaluation.getValue()); - assertEquals("loop", providerEvaluation.getVariant()); - assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); - } - - @Test - public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + @Test + public void targetingUnmatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@abc.com")); + + // then + assertEquals("loopAlg", providerEvaluation.getValue()); + assertEquals("loop", providerEvaluation.getVariant()); + assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", - new MutableContext("xyz")); + @Test + public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void targetingErrorEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", + new MutableContext("xyz")); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), - (providerState, changedFlagKeys, syncMetadata) -> { - }); + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } + + @Test + public void targetingErrorEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - // when/then - assertThrows(ParseError.class, () -> { - inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); - }); - } - - @Test - public void validateMetadataInEvaluationResult() throws Exception { - // given - final String scope = "appName=myApp"; - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWth( - FlagdOptions.builder().selector(scope).build(), - new MockStorage(flagMap)); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", - false, - new ImmutableContext()); - - // then - final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); - assertNotNull(metadata); - assertEquals(scope, metadata.getString("scope")); - } - - private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { - - final InProcessResolver resolver = new InProcessResolver(options, () -> true, - (providerState, changedFlagKeys, syncMetadata) -> { + InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when/then + assertThrows(ParseError.class, () -> { + inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); }); - return injectFlagStore(resolver, storage); - } + } - private InProcessResolver getInProcessResolverWth(final MockStorage storage, - final TriConsumer, Map> onConnectionEvent) - throws NoSuchFieldException, IllegalAccessException { + @Test + public void validateMetadataInEvaluationResult() throws Exception { + // given + final String scope = "appName=myApp"; + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWth( + FlagdOptions.builder().selector(scope).build(), + new MockStorage(flagMap)); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); + assertNotNull(metadata); + assertEquals(scope, metadata.getString("scope")); + } - final InProcessResolver resolver = new InProcessResolver( - FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); - return injectFlagStore(resolver, storage); - } + private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - // helper to inject flagStore override - private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { + final InProcessResolver resolver = new InProcessResolver(options, () -> true, + (connectionEvent) -> { + }); + return injectFlagStore(resolver, storage); + } - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, storage); + private InProcessResolver getInProcessResolverWth(final MockStorage storage, + final Consumer onConnectionEvent) + throws NoSuchFieldException, IllegalAccessException { - return resolver; - } + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); + return injectFlagStore(resolver, storage); + } + + // helper to inject flagStore override + private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { + + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, storage); + + return resolver; + } }