diff --git a/.gitmodules b/.gitmodules
index e439dea5d..619d4fff1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,7 +4,7 @@
[submodule "providers/flagd/test-harness"]
path = providers/flagd/test-harness
url = https://github.com/open-feature/test-harness.git
- branch = v0.5.21
+ branch = v1.1.1
[submodule "providers/flagd/spec"]
path = providers/flagd/spec
url = https://github.com/open-feature/spec.git
diff --git a/pom.xml b/pom.xml
index 7372429cc..6efd7c428 100644
--- a/pom.xml
+++ b/pom.xml
@@ -200,6 +200,13 @@
test
+
+ io.cucumber
+ cucumber-picocontainer
+ 7.20.1
+ test
+
+
org.awaitility
awaitility
diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml
index 90f50d2ce..15a9d779f 100644
--- a/providers/flagd/pom.xml
+++ b/providers/flagd/pom.xml
@@ -149,7 +149,20 @@
1.20.4
test
+
+ org.testcontainers
+ toxiproxy
+ 1.20.4
+ test
+
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.16
+ test
+
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 1e9c30882..bbf7674f1 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
@@ -1,7 +1,8 @@
package dev.openfeature.contrib.providers.flagd;
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.FlagdProviderEvent;
+import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
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;
@@ -12,12 +13,17 @@
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.ProviderEventDetails;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
@@ -30,11 +36,31 @@ public class FlagdProvider extends EventProvider {
private Function contextEnricher;
private static final String FLAGD_PROVIDER = "flagd";
private final Resolver flagResolver;
- private volatile boolean initialized = false;
- private volatile boolean connected = false;
- private volatile Structure syncMetadata = new ImmutableStructure();
- private volatile EvaluationContext enrichedContext = new ImmutableContext();
private final List hooks = new ArrayList<>();
+ private final EventsLock eventsLock = new EventsLock();
+
+ /**
+ * An executor service responsible for emitting
+ * {@link ProviderEvent#PROVIDER_ERROR} after the provider went
+ * {@link ProviderEvent#PROVIDER_STALE} for {@link #gracePeriod} seconds.
+ */
+ private final ScheduledExecutorService errorExecutor;
+
+ /**
+ * A scheduled task for emitting {@link ProviderEvent#PROVIDER_ERROR}.
+ */
+ private ScheduledFuture> errorTask;
+
+ /**
+ * The grace period in milliseconds to wait after
+ * {@link ProviderEvent#PROVIDER_STALE} before emitting a
+ * {@link ProviderEvent#PROVIDER_ERROR}.
+ */
+ private final long gracePeriod;
+ /**
+ * The deadline in milliseconds for GRPC operations.
+ */
+ private final long deadline;
protected final void finalize() {
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
@@ -55,11 +81,11 @@ public FlagdProvider() {
public FlagdProvider(final FlagdOptions options) {
switch (options.getResolverType().asString()) {
case Config.RESOLVER_IN_PROCESS:
- this.flagResolver = new InProcessResolver(options, this::isConnected, this::onConnectionEvent);
+ this.flagResolver = new InProcessResolver(options, this::onProviderEvent);
break;
case Config.RESOLVER_RPC:
this.flagResolver = new GrpcResolver(
- options, new Cache(options.getCacheType(), options.getMaxCacheSize()), this::onConnectionEvent);
+ options, new Cache(options.getCacheType(), options.getMaxCacheSize()), this::onProviderEvent);
break;
default:
throw new IllegalStateException(
@@ -67,6 +93,22 @@ public FlagdProvider(final FlagdOptions options) {
}
hooks.add(new SyncMetadataHook(this::getEnrichedContext));
contextEnricher = options.getContextEnricher();
+ errorExecutor = Executors.newSingleThreadScheduledExecutor();
+ gracePeriod = options.getRetryGracePeriod();
+ deadline = options.getDeadline();
+ }
+
+ /**
+ * Internal constructor for test cases.
+ * DO NOT MAKE PUBLIC
+ */
+ FlagdProvider(Resolver resolver, boolean initialized) {
+ this.flagResolver = resolver;
+ deadline = Config.DEFAULT_DEADLINE;
+ gracePeriod = Config.DEFAULT_STREAM_RETRY_GRACE_PERIOD;
+ hooks.add(new SyncMetadataHook(this::getEnrichedContext));
+ errorExecutor = Executors.newSingleThreadScheduledExecutor();
+ this.eventsLock.initialized = initialized;
}
@Override
@@ -75,27 +117,39 @@ public List getProviderHooks() {
}
@Override
- public synchronized void initialize(EvaluationContext evaluationContext) throws Exception {
- if (this.initialized) {
- return;
- }
+ public void initialize(EvaluationContext evaluationContext) throws Exception {
+ synchronized (eventsLock) {
+ if (eventsLock.initialized) {
+ return;
+ }
- this.flagResolver.init();
- this.initialized = this.connected = true;
+ flagResolver.init();
+ }
+ // block till ready - this works with deadline fine for rpc, but with in_process
+ // we also need to take parsing into the equation
+ // TODO: evaluate where we are losing time, so we can remove this magic number -
+ // follow up
+ // wait outside of the synchonrization or we'll deadlock
+ Util.busyWaitAndCheck(this.deadline * 2, () -> eventsLock.initialized);
}
@Override
- public synchronized void shutdown() {
- if (!this.initialized) {
- return;
- }
-
- try {
- this.flagResolver.shutdown();
- } catch (Exception e) {
- log.error("Error during shutdown {}", FLAGD_PROVIDER, e);
- } finally {
- this.initialized = false;
+ public void shutdown() {
+ synchronized (eventsLock) {
+ if (!eventsLock.initialized) {
+ return;
+ }
+ try {
+ this.flagResolver.shutdown();
+ if (errorExecutor != null) {
+ errorExecutor.shutdownNow();
+ errorExecutor.awaitTermination(deadline, TimeUnit.MILLISECONDS);
+ }
+ } catch (Exception e) {
+ log.error("Error during shutdown {}", FLAGD_PROVIDER, e);
+ } finally {
+ eventsLock.initialized = false;
+ }
}
}
@@ -106,27 +160,27 @@ public Metadata getMetadata() {
@Override
public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
- return this.flagResolver.booleanEvaluation(key, defaultValue, ctx);
+ return flagResolver.booleanEvaluation(key, defaultValue, ctx);
}
@Override
public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- return this.flagResolver.stringEvaluation(key, defaultValue, ctx);
+ return flagResolver.stringEvaluation(key, defaultValue, ctx);
}
@Override
public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- return this.flagResolver.doubleEvaluation(key, defaultValue, ctx);
+ return flagResolver.doubleEvaluation(key, defaultValue, ctx);
}
@Override
public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- return this.flagResolver.integerEvaluation(key, defaultValue, ctx);
+ return flagResolver.integerEvaluation(key, defaultValue, ctx);
}
@Override
public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
- return this.flagResolver.objectEvaluation(key, defaultValue, ctx);
+ return flagResolver.objectEvaluation(key, defaultValue, ctx);
}
/**
@@ -139,7 +193,7 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa
* @return Object map representing sync metadata
*/
protected Structure getSyncMetadata() {
- return new ImmutableStructure(syncMetadata.asMap());
+ return new ImmutableStructure(eventsLock.syncMetadata.asMap());
}
/**
@@ -148,50 +202,109 @@ protected Structure getSyncMetadata() {
* @return context
*/
EvaluationContext getEnrichedContext() {
- return enrichedContext;
+ return eventsLock.enrichedContext;
}
- private boolean isConnected() {
- return this.connected;
- }
+ @SuppressWarnings("checkstyle:fallthrough")
+ private void onProviderEvent(FlagdProviderEvent flagdProviderEvent) {
- private void onConnectionEvent(ConnectionEvent connectionEvent) {
- final boolean wasConnected = connected;
- final boolean isConnected = connected = connectionEvent.isConnected();
+ synchronized (eventsLock) {
+ log.info("FlagdProviderEvent: {}", flagdProviderEvent);
+ eventsLock.syncMetadata = flagdProviderEvent.getSyncMetadata();
+ if (flagdProviderEvent.getSyncMetadata() != null) {
+ eventsLock.enrichedContext = contextEnricher.apply(flagdProviderEvent.getSyncMetadata());
+ }
- syncMetadata = connectionEvent.getSyncMetadata();
- enrichedContext = contextEnricher.apply(connectionEvent.getSyncMetadata());
+ /*
+ * We only use Error and Ready as previous states.
+ * As error will first be emitted as Stale, and only turns after a while into an
+ * emitted Error.
+ * Ready is needed, as the InProcessResolver does not have a dedicated ready
+ * event, hence we need to
+ * forward a configuration changed to the ready, if we are not in the ready
+ * state.
+ */
+ switch (flagdProviderEvent.getEvent()) {
+ case PROVIDER_CONFIGURATION_CHANGED:
+ if (eventsLock.previousEvent == ProviderEvent.PROVIDER_READY) {
+ onConfigurationChanged(flagdProviderEvent);
+ break;
+ }
+ // intentional fall through, a not-ready change will trigger a ready.
+ case PROVIDER_READY:
+ onReady();
+ eventsLock.previousEvent = ProviderEvent.PROVIDER_READY;
+ break;
- if (!initialized) {
- return;
+ case PROVIDER_ERROR:
+ if (eventsLock.previousEvent != ProviderEvent.PROVIDER_ERROR) {
+ onError();
+ }
+ eventsLock.previousEvent = ProviderEvent.PROVIDER_ERROR;
+ break;
+ default:
+ log.info("Unknown event {}", flagdProviderEvent.getEvent());
+ }
}
+ }
+
+ private void onConfigurationChanged(FlagdProviderEvent flagdProviderEvent) {
+ this.emitProviderConfigurationChanged(ProviderEventDetails.builder()
+ .flagsChanged(flagdProviderEvent.getFlagsChanged())
+ .message("configuration changed")
+ .build());
+ }
- if (!wasConnected && isConnected) {
- ProviderEventDetails details = ProviderEventDetails.builder()
- .flagsChanged(connectionEvent.getFlagsChanged())
- .message("connected to flagd")
- .build();
- this.emitProviderReady(details);
- return;
+ private void onReady() {
+ if (!eventsLock.initialized) {
+ eventsLock.initialized = true;
+ log.info("initialized FlagdProvider");
}
+ if (errorTask != null && !errorTask.isCancelled()) {
+ errorTask.cancel(false);
+ log.debug("Reconnection task cancelled as connection became READY.");
+ }
+ this.emitProviderReady(
+ ProviderEventDetails.builder().message("connected to flagd").build());
+ }
- if (wasConnected && isConnected) {
- ProviderEventDetails details = ProviderEventDetails.builder()
- .flagsChanged(connectionEvent.getFlagsChanged())
- .message("configuration changed")
- .build();
- this.emitProviderConfigurationChanged(details);
- return;
+ private void onError() {
+ log.info("Connection lost. Emit STALE event...");
+ log.debug("Waiting {}s for connection to become available...", gracePeriod);
+ this.emitProviderStale(ProviderEventDetails.builder()
+ .message("there has been an error")
+ .build());
+
+ if (errorTask != null && !errorTask.isCancelled()) {
+ errorTask.cancel(false);
}
- if (connectionEvent.isStale()) {
- this.emitProviderStale(ProviderEventDetails.builder()
- .message("there has been an error")
- .build());
- } else {
- this.emitProviderError(ProviderEventDetails.builder()
- .message("there has been an error")
- .build());
+ if (!errorExecutor.isShutdown()) {
+ errorTask = errorExecutor.schedule(
+ () -> {
+ if (eventsLock.previousEvent == ProviderEvent.PROVIDER_ERROR) {
+ log.debug(
+ "Provider did not reconnect successfully within {}s. Emit ERROR event...",
+ gracePeriod);
+ flagResolver.onError();
+ this.emitProviderError(ProviderEventDetails.builder()
+ .message("there has been an error")
+ .build());
+ }
+ },
+ gracePeriod,
+ TimeUnit.SECONDS);
}
}
+
+ /**
+ * Contains all fields we need to worry about locking, used as intrinsic lock
+ * for sync blocks.
+ */
+ static class EventsLock {
+ volatile ProviderEvent previousEvent = null;
+ volatile Structure syncMetadata = new ImmutableStructure();
+ volatile boolean initialized = false;
+ volatile EvaluationContext enrichedContext = new ImmutableContext();
+ }
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
index 1f9106501..c86a175fe 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/Resolver.java
@@ -10,6 +10,8 @@ public interface Resolver {
void shutdown() throws Exception;
+ default void onError() {}
+
ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx);
ProviderEvaluation stringEvaluation(String key, String defaultValue, EvaluationContext ctx);
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java
index 8ccb73c15..1b201d640 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ChannelMonitor.java
@@ -32,7 +32,7 @@ public static void monitorChannelState(
Runnable onConnectionLost) {
channel.notifyWhenStateChanged(expectedState, () -> {
ConnectivityState currentState = channel.getState(true);
- log.info("Channel state changed to: {}", currentState);
+ log.debug("Channel state changed to: {}", currentState);
if (currentState == ConnectivityState.READY) {
if (onConnectionReady != null) {
onConnectionReady.run();
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java
deleted file mode 100644
index 6dbd388a0..000000000
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionState.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.resolver.common;
-
-/**
- * Represents the possible states of a connection.
- */
-public enum ConnectionState {
-
- /**
- * The connection is active and functioning as expected.
- */
- CONNECTED,
-
- /**
- * The connection is not active and has been fully disconnected.
- */
- DISCONNECTED,
-
- /**
- * The connection is inactive or degraded but may still recover.
- */
- STALE,
-
- /**
- * The connection has encountered an error and cannot function correctly.
- */
- ERROR,
-}
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/FlagdProviderEvent.java
similarity index 56%
rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/FlagdProviderEvent.java
index 0e8ff4c6b..517a23218 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/ConnectionEvent.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/FlagdProviderEvent.java
@@ -1,9 +1,11 @@
package dev.openfeature.contrib.providers.flagd.resolver.common;
import dev.openfeature.sdk.ImmutableStructure;
+import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.Structure;
import java.util.Collections;
import java.util.List;
+import lombok.Getter;
/**
* Represents an event payload for a connection state change in a
@@ -11,12 +13,13 @@
* The event includes information about the connection status, any flags that have changed,
* and metadata associated with the synchronization process.
*/
-public class ConnectionEvent {
+public class FlagdProviderEvent {
/**
* The current state of the connection.
*/
- private final ConnectionState connected;
+ @Getter
+ private final ProviderEvent event;
/**
* A list of flags that have changed due to this connection event.
@@ -28,57 +31,45 @@ public class ConnectionEvent {
*/
private final Structure syncMetadata;
- /**
- * Constructs a new {@code ConnectionEvent} with the connection status only.
- *
- * @param connected {@code true} if the connection is established, otherwise {@code false}.
- */
- public ConnectionEvent(boolean connected) {
- this(
- connected ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED,
- Collections.emptyList(),
- new ImmutableStructure());
- }
-
/**
* Constructs a new {@code ConnectionEvent} with the specified connection state.
*
- * @param connected the connection state indicating if the connection is established or not.
+ * @param event the event indicating the provider state.
*/
- public ConnectionEvent(ConnectionState connected) {
- this(connected, Collections.emptyList(), new ImmutableStructure());
+ public FlagdProviderEvent(ProviderEvent event) {
+ this(event, Collections.emptyList(), new ImmutableStructure());
}
/**
* Constructs a new {@code ConnectionEvent} with the specified connection state and changed flags.
*
- * @param connected the connection state indicating if the connection is established or not.
+ * @param event the event indicating the provider state.
* @param flagsChanged a list of flags that have changed due to this connection event.
*/
- public ConnectionEvent(ConnectionState connected, List flagsChanged) {
- this(connected, flagsChanged, new ImmutableStructure());
+ public FlagdProviderEvent(ProviderEvent event, List flagsChanged) {
+ this(event, flagsChanged, new ImmutableStructure());
}
/**
* Constructs a new {@code ConnectionEvent} with the specified connection state and synchronization metadata.
*
- * @param connected the connection state indicating if the connection is established or not.
+ * @param event the event indicating the provider state.
* @param syncMetadata metadata related to the synchronization process of this event.
*/
- public ConnectionEvent(ConnectionState connected, Structure syncMetadata) {
- this(connected, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap()));
+ public FlagdProviderEvent(ProviderEvent event, Structure syncMetadata) {
+ this(event, Collections.emptyList(), new ImmutableStructure(syncMetadata.asMap()));
}
/**
* Constructs a new {@code ConnectionEvent} with the specified connection state, changed flags, and
* synchronization metadata.
*
- * @param connectionState the state of the connection.
+ * @param event the event.
* @param flagsChanged a list of flags that have changed due to this connection event.
* @param syncMetadata metadata related to the synchronization process of this event.
*/
- public ConnectionEvent(ConnectionState connectionState, List flagsChanged, Structure syncMetadata) {
- this.connected = connectionState;
+ public FlagdProviderEvent(ProviderEvent event, List flagsChanged, Structure syncMetadata) {
+ this.event = event;
this.flagsChanged = flagsChanged != null ? flagsChanged : Collections.emptyList(); // Ensure non-null list
this.syncMetadata = syncMetadata != null
? new ImmutableStructure(syncMetadata.asMap())
@@ -103,22 +94,7 @@ public Structure getSyncMetadata() {
return new ImmutableStructure(syncMetadata.asMap());
}
- /**
- * Indicates whether the current connection state is connected.
- *
- * @return {@code true} if connected, otherwise {@code false}.
- */
- public boolean isConnected() {
- return this.connected == ConnectionState.CONNECTED;
- }
-
- /**
- * Indicates
- * whether the current connection state is stale.
- *
- * @return {@code true} if stale, otherwise {@code false}.
- */
- public boolean isStale() {
- return this.connected == ConnectionState.STALE;
+ public boolean isDisconnected() {
+ return event == ProviderEvent.PROVIDER_ERROR || event == ProviderEvent.PROVIDER_STALE;
}
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnector.java
index d5ca69aff..ae83227f2 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnector.java
@@ -2,14 +2,12 @@
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.sdk.ImmutableStructure;
+import dev.openfeature.sdk.ProviderEvent;
import io.grpc.ConnectivityState;
import io.grpc.ManagedChannel;
import io.grpc.stub.AbstractBlockingStub;
import io.grpc.stub.AbstractStub;
import java.util.Collections;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -54,34 +52,19 @@ public class GrpcConnector, K extends AbstractBlocking
/**
* A consumer that handles connection events such as connection loss or reconnection.
*/
- private final Consumer onConnectionEvent;
+ private final Consumer onConnectionEvent;
/**
* A consumer that handles GRPC service stubs for event stream handling.
*/
private final Consumer streamObserver;
- /**
- * An executor service responsible for scheduling reconnection attempts.
- */
- private final ScheduledExecutorService reconnectExecutor;
-
- /**
- * The grace period in milliseconds to wait for reconnection before emitting an error event.
- */
- private final long gracePeriod;
-
/**
* Indicates whether the connector is currently connected to the GRPC service.
*/
@Getter
private boolean connected = false;
- /**
- * A scheduled task for managing reconnection attempts.
- */
- private ScheduledFuture> reconnectTask;
-
/**
* Constructs a new {@code GrpcConnector} instance with the specified options and parameters.
*
@@ -96,19 +79,17 @@ public GrpcConnector(
final FlagdOptions options,
final Function stub,
final Function blockingStub,
- final Consumer onConnectionEvent,
+ final Consumer onConnectionEvent,
final Consumer eventStreamObserver,
ManagedChannel channel) {
this.channel = channel;
- this.serviceStub = stub.apply(channel);
- this.blockingStub = blockingStub.apply(channel);
+ this.serviceStub = stub.apply(channel).withWaitForReady();
+ this.blockingStub = blockingStub.apply(channel).withWaitForReady();
this.deadline = options.getDeadline();
this.streamDeadlineMs = options.getStreamDeadlineMs();
this.onConnectionEvent = onConnectionEvent;
this.streamObserver = eventStreamObserver;
- this.gracePeriod = options.getRetryGracePeriod();
- this.reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
}
/**
@@ -124,7 +105,7 @@ public GrpcConnector(
final FlagdOptions options,
final Function stub,
final Function blockingStub,
- final Consumer onConnectionEvent,
+ final Consumer onConnectionEvent,
final Consumer eventStreamObserver) {
this(options, stub, blockingStub, onConnectionEvent, eventStreamObserver, ChannelBuilder.nettyChannel(options));
}
@@ -136,8 +117,6 @@ public GrpcConnector(
*/
public void initialize() throws Exception {
log.info("Initializing GRPC connection...");
- ChannelMonitor.waitForDesiredState(
- ConnectivityState.READY, channel, this::onInitialConnect, deadline, TimeUnit.MILLISECONDS);
ChannelMonitor.monitorChannelState(ConnectivityState.READY, channel, this::onReady, this::onConnectionLost);
}
@@ -157,25 +136,11 @@ public K getResolver() {
*/
public void shutdown() throws InterruptedException {
log.info("Shutting down GRPC connection...");
- if (reconnectExecutor != null) {
- reconnectExecutor.shutdownNow();
- reconnectExecutor.awaitTermination(deadline, TimeUnit.MILLISECONDS);
- }
if (!channel.isShutdown()) {
channel.shutdownNow();
channel.awaitTermination(deadline, TimeUnit.MILLISECONDS);
}
-
- if (connected) {
- this.onConnectionEvent.accept(new ConnectionEvent(false));
- connected = false;
- }
- }
-
- private synchronized void onInitialConnect() {
- connected = true;
- restartStream();
}
/**
@@ -184,13 +149,7 @@ private synchronized void onInitialConnect() {
*/
private synchronized void onReady() {
connected = true;
-
- if (reconnectTask != null && !reconnectTask.isCancelled()) {
- reconnectTask.cancel(false);
- log.debug("Reconnection task cancelled as connection became READY.");
- }
restartStream();
- this.onConnectionEvent.accept(new ConnectionEvent(true));
}
/**
@@ -198,27 +157,10 @@ private synchronized void onReady() {
* Schedules a reconnection task after a grace period and emits a stale connection event.
*/
private synchronized void onConnectionLost() {
- log.debug("Connection lost. Emit STALE event...");
- log.debug("Waiting {}s for connection to become available...", gracePeriod);
connected = false;
- this.onConnectionEvent.accept(
- new ConnectionEvent(ConnectionState.STALE, Collections.emptyList(), new ImmutableStructure()));
-
- if (reconnectTask != null && !reconnectTask.isCancelled()) {
- reconnectTask.cancel(false);
- }
-
- if (!reconnectExecutor.isShutdown()) {
- reconnectTask = reconnectExecutor.schedule(
- () -> {
- log.debug(
- "Provider did not reconnect successfully within {}s. Emit ERROR event...", gracePeriod);
- this.onConnectionEvent.accept(new ConnectionEvent(false));
- },
- gracePeriod,
- TimeUnit.SECONDS);
- }
+ this.onConnectionEvent.accept(new FlagdProviderEvent(
+ ProviderEvent.PROVIDER_ERROR, Collections.emptyList(), new ImmutableStructure()));
}
/**
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
index db89931e5..8b8886bf8 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserver.java
@@ -1,15 +1,15 @@
package dev.openfeature.contrib.providers.flagd.resolver.grpc;
import com.google.protobuf.Value;
-import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
+import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse;
+import dev.openfeature.sdk.ProviderEvent;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.grpc.stub.StreamObserver;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
@@ -20,25 +20,17 @@
@SuppressFBWarnings(justification = "cache needs to be read and write by multiple objects")
class EventStreamObserver implements StreamObserver {
- /**
- * A consumer to handle connection events with a flag indicating success and a list of changed flags.
- */
- private final BiConsumer> onConnectionEvent;
-
- /**
- * The cache to update based on received events.
- */
- private final Cache cache;
+ private final Consumer> onConfigurationChange;
+ private final Consumer onReady;
/**
* Constructs a new {@code EventStreamObserver} instance.
*
- * @param cache the cache to update based on received events
* @param onConnectionEvent a consumer to handle connection events with a boolean and a list of changed flags
*/
- EventStreamObserver(Cache cache, BiConsumer> onConnectionEvent) {
- this.cache = cache;
- this.onConnectionEvent = onConnectionEvent;
+ EventStreamObserver(Consumer> onConfigurationChange, Consumer onReady) {
+ this.onConfigurationChange = onConfigurationChange;
+ this.onReady = onReady;
}
/**
@@ -60,28 +52,11 @@ public void onNext(EventStreamResponse value) {
}
}
- /**
- * Called when an error occurs in the stream.
- *
- * @param throwable the error that occurred
- */
@Override
- public void onError(Throwable throwable) {
- if (this.cache.getEnabled().equals(Boolean.TRUE)) {
- this.cache.clear();
- }
- }
+ public void onError(Throwable throwable) {}
- /**
- * Called when the stream is completed.
- */
@Override
- public void onCompleted() {
- if (this.cache.getEnabled().equals(Boolean.TRUE)) {
- this.cache.clear();
- }
- this.onConnectionEvent.accept(false, Collections.emptyList());
- }
+ public void onCompleted() {}
/**
* Handles configuration change events by updating the cache and notifying listeners about changed flags.
@@ -90,33 +65,22 @@ public void onCompleted() {
*/
private void handleConfigurationChangeEvent(EventStreamResponse value) {
List changedFlags = new ArrayList<>();
- boolean cachingEnabled = this.cache.getEnabled();
Map data = value.getData().getFieldsMap();
Value flagsValue = data.get(Constants.FLAGS_KEY);
- if (flagsValue == null) {
- if (cachingEnabled) {
- this.cache.clear();
- }
- } else {
+ if (flagsValue != null) {
Map flags = flagsValue.getStructValue().getFieldsMap();
- for (String flagKey : flags.keySet()) {
- changedFlags.add(flagKey);
- if (cachingEnabled) {
- this.cache.remove(flagKey);
- }
- }
+ changedFlags.addAll(flags.keySet());
}
- this.onConnectionEvent.accept(true, changedFlags);
+ onConfigurationChange.accept(changedFlags);
}
/**
* Handles provider readiness events by clearing the cache (if enabled) and notifying listeners of readiness.
*/
private void handleProviderReadyEvent() {
- if (this.cache.getEnabled().equals(Boolean.TRUE)) {
- this.cache.clear();
- }
+ log.info("Received provider ready event");
+ onReady.accept(new FlagdProviderEvent(ProviderEvent.PROVIDER_READY));
}
}
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 5c8ad3ea1..2a9669ec3 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
@@ -10,8 +10,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.common.ConnectionState;
+import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.contrib.providers.flagd.resolver.common.GrpcConnector;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.strategy.ResolveFactory;
@@ -26,6 +25,7 @@
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
@@ -57,23 +57,28 @@ public final class GrpcResolver implements Resolver {
*
* @param options flagd options
* @param cache cache to use
- * @param onConnectionEvent lambda which handles changes in the connection/stream
+ * @param onProviderEvent lambda which handles changes in the connection/stream
*/
public GrpcResolver(
- final FlagdOptions options, final Cache cache, final Consumer onConnectionEvent) {
+ final FlagdOptions options, final Cache cache, final Consumer onProviderEvent) {
this.cache = cache;
this.strategy = ResolveFactory.getStrategy(options);
this.connector = new GrpcConnector<>(
options,
ServiceGrpc::newStub,
ServiceGrpc::newBlockingStub,
- onConnectionEvent,
+ onProviderEvent,
stub -> stub.eventStream(
Evaluation.EventStreamRequest.getDefaultInstance(),
new EventStreamObserver(
- cache,
- (k, e) ->
- onConnectionEvent.accept(new ConnectionEvent(ConnectionState.CONNECTED, e)))));
+ (flags) -> {
+ if (cache != null) {
+ flags.forEach(cache::remove);
+ }
+ onProviderEvent.accept(new FlagdProviderEvent(
+ ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, flags));
+ },
+ onProviderEvent)));
}
/**
@@ -90,6 +95,13 @@ public void shutdown() throws Exception {
this.connector.shutdown();
}
+ @Override
+ public void onError() {
+ if (cache != null) {
+ cache.clear();
+ }
+ }
+
/**
* Boolean evaluation from grpc resolver.
*/
@@ -207,7 +219,7 @@ private Boolean isEvaluationCacheable(ProviderEvaluation evaluation) {
}
private Boolean cacheAvailable() {
- return this.cache.getEnabled() && this.connector.isConnected();
+ return this.cache.getEnabled();
}
private static ImmutableMetadata metadataFromResponse(Message response) {
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 95f2eb6d6..b3385781e 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
@@ -4,14 +4,11 @@
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.ConnectionState;
-import dev.openfeature.contrib.providers.flagd.resolver.common.Util;
+import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult;
-import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector;
@@ -22,13 +19,13 @@
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import java.util.Map;
import java.util.function.Consumer;
-import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
/**
@@ -39,10 +36,9 @@
@Slf4j
public class InProcessResolver implements Resolver {
private final Storage flagStore;
- private final Consumer onConnectionEvent;
+ private final Consumer onConnectionEvent;
private final Operator operator;
private final long deadline;
- private final Supplier connectedSupplier;
private final String scope;
/**
@@ -51,20 +47,14 @@ public class InProcessResolver implements Resolver {
* Flags are evaluated locally.
*
* @param options flagd options
- * @param connectedSupplier lambda providing current connection status from
- * caller
* @param onConnectionEvent lambda which handles changes in the
* connection/stream
*/
- public InProcessResolver(
- FlagdOptions options,
- final Supplier connectedSupplier,
- Consumer onConnectionEvent) {
+ public InProcessResolver(FlagdOptions options, Consumer onConnectionEvent) {
this.flagStore = new FlagStore(getConnector(options, onConnectionEvent));
this.deadline = options.getDeadline();
this.onConnectionEvent = onConnectionEvent;
this.operator = new Operator();
- this.connectedSupplier = connectedSupplier;
this.scope = options.getSelector();
}
@@ -78,15 +68,20 @@ public void init() throws Exception {
while (true) {
final StorageStateChange storageStateChange =
flagStore.getStateQueue().take();
- if (storageStateChange.getStorageState() != StorageState.OK) {
- log.info(
- String.format("Storage returned NOK status: %s", storageStateChange.getStorageState()));
- continue;
+ switch (storageStateChange.getStorageState()) {
+ case OK:
+ onConnectionEvent.accept(new FlagdProviderEvent(
+ ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
+ storageStateChange.getChangedFlagsKeys(),
+ storageStateChange.getSyncMetadata()));
+ break;
+ case ERROR:
+ onConnectionEvent.accept(new FlagdProviderEvent(ProviderEvent.PROVIDER_ERROR));
+ break;
+ default:
+ log.info(String.format(
+ "Storage emitted unhandled status: %s", storageStateChange.getStorageState()));
}
- onConnectionEvent.accept(new ConnectionEvent(
- ConnectionState.CONNECTED,
- storageStateChange.getChangedFlagsKeys(),
- storageStateChange.getSyncMetadata()));
}
} catch (InterruptedException e) {
log.warn("Storage state watcher interrupted", e);
@@ -95,9 +90,6 @@ public void init() throws Exception {
});
stateWatcher.setDaemon(true);
stateWatcher.start();
-
- // block till ready
- Util.busyWaitAndCheck(this.deadline, this.connectedSupplier);
}
/**
@@ -107,7 +99,6 @@ public void init() throws Exception {
*/
public void shutdown() throws InterruptedException {
flagStore.shutdown();
- onConnectionEvent.accept(new ConnectionEvent(false));
}
/**
@@ -154,7 +145,7 @@ public ProviderEvaluation objectEvaluation(String key, Value defaultValue
.build();
}
- static Connector getConnector(final FlagdOptions options, Consumer onConnectionEvent) {
+ static Connector getConnector(final FlagdOptions options, Consumer onConnectionEvent) {
if (options.getCustomConnector() != null) {
return options.getCustomConnector();
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
index e48a65211..832c8a487 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/grpc/GrpcStreamConnector.java
@@ -1,7 +1,7 @@
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent;
+import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.contrib.providers.flagd.resolver.common.GrpcConnector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector;
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
@@ -16,7 +16,6 @@
import io.grpc.Context.CancellableContext;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
@@ -43,7 +42,7 @@ public class GrpcStreamConnector implements Connector {
/**
* Creates a new GrpcStreamConnector responsible for observing the event stream.
*/
- public GrpcStreamConnector(final FlagdOptions options, Consumer onConnectionEvent) {
+ public GrpcStreamConnector(final FlagdOptions options, Consumer onConnectionEvent) {
deadline = options.getDeadline();
selector = options.getSelector();
streamReceiver = new LinkedBlockingQueue<>(QUEUE_SIZE);
@@ -60,7 +59,7 @@ public void init() throws Exception {
grpcConnector.initialize();
Thread listener = new Thread(() -> {
try {
- observeEventStream(blockingQueue, shutdown, selector, deadline);
+ observeEventStream(blockingQueue, shutdown, deadline);
} catch (InterruptedException e) {
log.warn("gRPC event stream interrupted, flag configurations are stale", e);
Thread.currentThread().interrupt();
@@ -89,11 +88,7 @@ public void shutdown() throws InterruptedException {
}
/** Contains blocking calls, to be used concurrently. */
- void observeEventStream(
- final BlockingQueue writeTo,
- final AtomicBoolean shutdown,
- final String selector,
- final int deadline)
+ void observeEventStream(final BlockingQueue writeTo, final AtomicBoolean shutdown, final int deadline)
throws InterruptedException {
log.info("Initializing sync stream observer");
@@ -114,10 +109,7 @@ void observeEventStream(
try (CancellableContext context = Context.current().withCancellation()) {
try {
- metadataResponse = grpcConnector
- .getResolver()
- .withDeadlineAfter(deadline, TimeUnit.MILLISECONDS)
- .getMetadata(metadataRequest.build());
+ metadataResponse = grpcConnector.getResolver().getMetadata(metadataRequest.build());
} catch (Exception e) {
// the chances this call fails but the syncRequest does not are slim
// it could be that the server doesn't implement this RPC
@@ -126,6 +118,7 @@ void observeEventStream(
metadataException = e;
}
+ log.info("stream");
while (!shutdown.get()) {
final GrpcResponseModel response = streamReceiver.take();
if (response.isComplete()) {
@@ -137,6 +130,7 @@ void observeEventStream(
Throwable streamException = response.getError();
if (streamException != null || metadataException != null) {
+ log.debug("Exception in GRPC connection");
if (!writeTo.offer(new QueuePayload(
QueuePayloadType.ERROR, "Error from stream or metadata", metadataResponse))) {
log.error("Failed to convey ERROR status, queue is full");
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 c223cbc31..0aa226fcf 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
@@ -17,8 +17,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.common.ConnectionState;
+import dev.openfeature.contrib.providers.flagd.resolver.common.FlagdProviderEvent;
import dev.openfeature.contrib.providers.flagd.resolver.common.GrpcConnector;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.GrpcResolver;
import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
@@ -43,11 +42,13 @@
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.ProviderEvent;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import io.cucumber.java.AfterAll;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -318,11 +319,6 @@ 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";
@@ -570,6 +566,16 @@ void initializationAndShutdown() throws Exception {
flagResolver.setAccessible(true);
flagResolver.set(provider, resolverMock);
+ Method onProviderEvent = FlagdProvider.class.getDeclaredMethod("onProviderEvent", FlagdProviderEvent.class);
+ onProviderEvent.setAccessible(true);
+
+ doAnswer((i) -> {
+ onProviderEvent.invoke(provider, new FlagdProviderEvent(ProviderEvent.PROVIDER_READY));
+ return null;
+ })
+ .when(resolverMock)
+ .init();
+
// when
// validate multiple initialization
@@ -599,16 +605,17 @@ void contextEnrichment() throws Exception {
// mock a resolver
try (MockedConstruction mockResolver =
mockConstruction(InProcessResolver.class, (mock, context) -> {
- Consumer onConnectionEvent;
+ Consumer onConnectionEvent;
// get a reference to the onConnectionEvent callback
onConnectionEvent =
- (Consumer) context.arguments().get(2);
+ (Consumer) context.arguments().get(1);
// when our mock resolver initializes, it runs the passed onConnectionEvent
// callback
doAnswer(invocation -> {
- onConnectionEvent.accept(new ConnectionEvent(ConnectionState.CONNECTED, metadata));
+ onConnectionEvent.accept(
+ new FlagdProviderEvent(ProviderEvent.PROVIDER_READY, metadata));
return null;
})
.when(mock)
@@ -639,16 +646,17 @@ void updatesSyncMetadataWithCallback() throws Exception {
// mock a resolver
try (MockedConstruction mockResolver =
mockConstruction(InProcessResolver.class, (mock, context) -> {
- Consumer onConnectionEvent;
+ Consumer onConnectionEvent;
// get a reference to the onConnectionEvent callback
onConnectionEvent =
- (Consumer) context.arguments().get(2);
+ (Consumer) context.arguments().get(1);
// when our mock resolver initializes, it runs the passed onConnectionEvent
// callback
doAnswer(invocation -> {
- onConnectionEvent.accept(new ConnectionEvent(ConnectionState.CONNECTED, metadata));
+ onConnectionEvent.accept(
+ new FlagdProviderEvent(ProviderEvent.PROVIDER_READY, metadata));
return null;
})
.when(mock)
@@ -692,20 +700,14 @@ private FlagdProvider createProvider(GrpcConnector grpc, Cache cache) {
final FlagdOptions flagdOptions = FlagdOptions.builder().build();
final GrpcResolver grpcResolver = new GrpcResolver(flagdOptions, cache, (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);
}
-
+ final FlagdProvider provider = new FlagdProvider(grpcResolver, true);
return provider;
}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java
deleted file mode 100644
index e5231b1bc..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import java.io.File;
-import java.nio.file.Files;
-import java.util.List;
-import org.apache.logging.log4j.util.Strings;
-import org.jetbrains.annotations.NotNull;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.Network;
-import org.testcontainers.utility.DockerImageName;
-import org.testcontainers.utility.MountableFile;
-
-public class ContainerConfig {
- private static final String version;
- private static final Network network = Network.newNetwork();
-
- static {
- String path = "test-harness/version.txt";
- File file = new File(path);
- try {
- List lines = Files.readAllLines(file.toPath());
- version = lines.get(0);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * @return a {@link org.testcontainers.containers.GenericContainer} instance of a stable sync
- * flagd server with the port 9090 exposed
- */
- public static GenericContainer sync() {
- return sync(false, false);
- }
-
- /**
- * @param unstable if an unstable version of the container, which terminates the connection
- * regularly should be used.
- * @param addNetwork if set to true a custom network is attached for cross container access e.g.
- * envoy --> sync:8015
- * @return a {@link org.testcontainers.containers.GenericContainer} instance of a sync flagd
- * server with the port 8015 exposed
- */
- public static GenericContainer sync(boolean unstable, boolean addNetwork) {
- String container = generateContainerName("flagd", unstable ? "unstable" : "");
- GenericContainer genericContainer =
- new GenericContainer(DockerImageName.parse(container)).withExposedPorts(8015);
-
- if (addNetwork) {
- genericContainer.withNetwork(network);
- genericContainer.withNetworkAliases("sync-service");
- }
-
- return genericContainer;
- }
-
- /**
- * @return a {@link org.testcontainers.containers.GenericContainer} instance of a stable flagd
- * server with the port 8013 exposed
- */
- public static GenericContainer flagd() {
- return flagd(false);
- }
-
- /**
- * @param unstable if an unstable version of the container, which terminates the connection
- * regularly should be used.
- * @return a {@link org.testcontainers.containers.GenericContainer} instance of a flagd server
- * with the port 8013 exposed
- */
- public static GenericContainer flagd(boolean unstable) {
- String container = generateContainerName("flagd", unstable ? "unstable" : "");
- return new GenericContainer(DockerImageName.parse(container)).withExposedPorts(8013);
- }
-
- /**
- * @return a {@link org.testcontainers.containers.GenericContainer} instance of envoy container
- * using flagd sync service as backend expose on port 9211
- */
- public static GenericContainer envoy() {
- final String container = "envoyproxy/envoy:v1.31.0";
- return new GenericContainer(DockerImageName.parse(container))
- .withCopyFileToContainer(
- MountableFile.forClasspathResource("/envoy-config/envoy-custom.yaml"), "/etc/envoy/envoy.yaml")
- .withExposedPorts(9211)
- .withNetwork(network)
- .withNetworkAliases("envoy");
- }
-
- public static @NotNull String generateContainerName(String type, String addition) {
- String container = "ghcr.io/open-feature/";
- container += type;
- container += "-testbed";
- if (!Strings.isBlank(addition)) {
- container += "-" + addition;
- }
- container += ":v" + version;
- return container;
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/FlagdContainer.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/FlagdContainer.java
new file mode 100644
index 000000000..0aa8f50d0
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/FlagdContainer.java
@@ -0,0 +1,63 @@
+package dev.openfeature.contrib.providers.flagd.e2e;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+import org.apache.logging.log4j.util.Strings;
+import org.jetbrains.annotations.NotNull;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+public class FlagdContainer extends GenericContainer {
+ private static final String version;
+ private static final Network network = Network.newNetwork();
+
+ static {
+ String path = "test-harness/version.txt";
+ File file = new File(path);
+ try {
+ List lines = Files.readAllLines(file.toPath());
+ version = lines.get(0);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String feature;
+
+ public FlagdContainer() {
+ this("");
+ }
+
+ public FlagdContainer(String feature) {
+ super(generateContainerName(feature));
+ this.withReuse(true);
+ this.feature = feature;
+ if (!"socket".equals(this.feature)) this.addExposedPorts(8013, 8014, 8015, 8016);
+ }
+
+ /**
+ * @return a {@link org.testcontainers.containers.GenericContainer} instance of envoy container using
+ * flagd sync service as backend expose on port 9211
+ */
+ public static GenericContainer envoy() {
+ final String container = "envoyproxy/envoy:v1.31.0";
+ return new GenericContainer(DockerImageName.parse(container))
+ .withCopyFileToContainer(
+ MountableFile.forClasspathResource("/envoy-config/envoy-custom.yaml"), "/etc/envoy/envoy.yaml")
+ .withExposedPorts(9211)
+ .withNetwork(network)
+ .withNetworkAliases("envoy");
+ }
+
+ public static @NotNull String generateContainerName(String feature) {
+ String container = "ghcr.io/open-feature/flagd-testbed";
+ if (!Strings.isBlank(feature)) {
+ container += "-" + feature;
+ }
+ container += ":v" + version;
+ return container;
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java
index 0ef8b36be..d5dce18fe 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java
@@ -18,5 +18,5 @@
@IncludeEngines("cucumber")
@SelectFile("test-harness/gherkin/config.feature")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps.config")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps")
public class RunConfigCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java
deleted file mode 100644
index 39ff92993..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.junit.jupiter.api.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/**
- * Class for running the tests associated with "stable" e2e tests (no fake disconnection) for the
- * in-process provider
- */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("spec/specification/assets/gherkin/evaluation.feature")
-@SelectFile("test-harness/gherkin/flagd-json-evaluator.feature")
-@SelectFile("test-harness/gherkin/flagd.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value =
- "dev.openfeature.contrib.providers.flagd.e2e.process.core,dev.openfeature.contrib.providers.flagd.e2e.steps")
-@Testcontainers
-public class RunFlagdInProcessCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java
deleted file mode 100644
index a2bb60793..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.junit.jupiter.api.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/**
- * Class for running the tests associated with "stable" e2e tests (no fake disconnection) for the
- * in-process provider
- */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("spec/specification/assets/gherkin/evaluation.feature")
-@SelectFile("test-harness/gherkin/flagd-json-evaluator.feature")
-@SelectFile("test-harness/gherkin/flagd.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value =
- "dev.openfeature.contrib.providers.flagd.e2e.process.envoy,dev.openfeature.contrib.providers.flagd.e2e.steps")
-@Testcontainers
-public class RunFlagdInProcessEnvoyCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessReconnectCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessReconnectCucumberTest.java
deleted file mode 100644
index 346d9b5a4..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessReconnectCucumberTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.junit.jupiter.api.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/** Class for running the reconnection tests for the in-process provider */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("test-harness/gherkin/flagd-reconnect.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value =
- "dev.openfeature.contrib.providers.flagd.e2e.reconnect.process,dev.openfeature.contrib.providers.flagd.e2e.reconnect.steps")
-@Testcontainers
-public class RunFlagdInProcessReconnectCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java
deleted file mode 100644
index ae7ca415b..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.apache.logging.log4j.core.config.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/** Class for running the reconnection tests for the RPC provider */
-@Order(value = Integer.MAX_VALUE)
-@Suite(failIfNoTests = false)
-@IncludeEngines("cucumber")
-// @SelectFile("spec/specification/assets/gherkin/evaluation.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value =
- "dev.openfeature.contrib.providers.flagd.e2e.ssl.process,dev.openfeature.contrib.providers.flagd.e2e.steps")
-@Testcontainers
-public class RunFlagdInProcessSSLCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcCucumberTest.java
deleted file mode 100644
index e0d872b9f..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcCucumberTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.apache.logging.log4j.core.config.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/**
- * Class for running the tests associated with "stable" e2e tests (no fake disconnection) for the
- * RPC provider
- */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("spec/specification/assets/gherkin/evaluation.feature")
-@SelectFile("test-harness/gherkin/flagd-json-evaluator.feature")
-@SelectFile("test-harness/gherkin/flagd.feature")
-@SelectFile("test-harness/gherkin/flagd-rpc-caching.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value = "dev.openfeature.contrib.providers.flagd.e2e.rpc,dev.openfeature.contrib.providers.flagd.e2e.steps")
-@Testcontainers
-public class RunFlagdRpcCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcReconnectCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcReconnectCucumberTest.java
deleted file mode 100644
index 436ebf00f..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcReconnectCucumberTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.apache.logging.log4j.core.config.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/** Class for running the reconnection tests for the RPC provider */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("test-harness/gherkin/flagd-reconnect.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value =
- "dev.openfeature.contrib.providers.flagd.e2e.reconnect.rpc,dev.openfeature.contrib.providers.flagd.e2e.reconnect.steps")
-@Testcontainers
-public class RunFlagdRpcReconnectCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java
deleted file mode 100644
index f09846d41..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e;
-
-import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
-import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
-
-import org.apache.logging.log4j.core.config.Order;
-import org.junit.platform.suite.api.ConfigurationParameter;
-import org.junit.platform.suite.api.IncludeEngines;
-import org.junit.platform.suite.api.SelectFile;
-import org.junit.platform.suite.api.Suite;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-/** Class for running the reconnection tests for the RPC provider */
-@Order(value = Integer.MAX_VALUE)
-@Suite
-@IncludeEngines("cucumber")
-@SelectFile("spec/specification/assets/gherkin/evaluation.feature")
-@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
-@ConfigurationParameter(
- key = GLUE_PROPERTY_NAME,
- value = "dev.openfeature.contrib.providers.flagd.e2e.ssl.rpc,dev.openfeature.contrib.providers.flagd.e2e.steps")
-@Testcontainers
-public class RunFlagdRpcSSLCucumberTest {}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java
new file mode 100644
index 000000000..e0edef240
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java
@@ -0,0 +1,39 @@
+package dev.openfeature.contrib.providers.flagd.e2e;
+
+import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.OBJECT_FACTORY_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import org.apache.logging.log4j.core.config.Order;
+import org.junit.platform.suite.api.BeforeSuite;
+import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.ExcludeTags;
+import org.junit.platform.suite.api.IncludeEngines;
+import org.junit.platform.suite.api.IncludeTags;
+import org.junit.platform.suite.api.SelectDirectories;
+import org.junit.platform.suite.api.Suite;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+/**
+ * Class for running the reconnection tests for the RPC provider
+ */
+@Order(value = Integer.MAX_VALUE)
+@Suite
+@IncludeEngines("cucumber")
+@SelectDirectories("test-harness/gherkin")
+// if you want to run just one feature file, use the following line instead of @SelectDirectories
+// @SelectFile("test-harness/gherkin/connection.feature")
+@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps")
+@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory")
+@IncludeTags("in-process")
+@ExcludeTags({"unixsocket", "targetURI"})
+@Testcontainers
+public class RunInProcessTest {
+
+ @BeforeSuite
+ public static void before() {
+ State.resolverType = Config.Resolver.IN_PROCESS;
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java
new file mode 100644
index 000000000..bc649ddeb
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java
@@ -0,0 +1,39 @@
+package dev.openfeature.contrib.providers.flagd.e2e;
+
+import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.OBJECT_FACTORY_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import org.apache.logging.log4j.core.config.Order;
+import org.junit.platform.suite.api.BeforeSuite;
+import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.ExcludeTags;
+import org.junit.platform.suite.api.IncludeEngines;
+import org.junit.platform.suite.api.IncludeTags;
+import org.junit.platform.suite.api.SelectDirectories;
+import org.junit.platform.suite.api.Suite;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+/**
+ * Class for running the reconnection tests for the RPC provider
+ */
+@Order(value = Integer.MAX_VALUE)
+@Suite
+@IncludeEngines("cucumber")
+@SelectDirectories("test-harness/gherkin")
+// if you want to run just one feature file, use the following line instead of @SelectDirectories
+// @SelectFile("test-harness/gherkin/rpc-caching.feature")
+@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps")
+@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory")
+@IncludeTags({"rpc"})
+@ExcludeTags({"targetURI", "unixsocket"})
+@Testcontainers
+public class RunRpcTest {
+
+ @BeforeSuite
+ public static void before() {
+ State.resolverType = Config.Resolver.RPC;
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java
new file mode 100644
index 000000000..4ecab84e5
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java
@@ -0,0 +1,26 @@
+package dev.openfeature.contrib.providers.flagd.e2e;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdOptions;
+import dev.openfeature.contrib.providers.flagd.e2e.steps.Event;
+import dev.openfeature.contrib.providers.flagd.e2e.steps.FlagSteps;
+import dev.openfeature.contrib.providers.flagd.e2e.steps.ProviderType;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FlagEvaluationDetails;
+import dev.openfeature.sdk.MutableContext;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+
+public class State {
+ public ProviderType providerType;
+ public Client client;
+ public List events = new LinkedList<>();
+ public Optional lastEvent;
+ public FlagSteps.Flag flag;
+ public MutableContext context = new MutableContext();
+ public FlagEvaluationDetails evaluation;
+ public FlagdOptions options;
+ public FlagdOptions.FlagdOptionsBuilder builder = FlagdOptions.builder();
+ public static Config.Resolver resolverType;
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java
deleted file mode 100644
index e23a95965..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.process.core;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdInProcessSetup {
-
- private static FeatureProvider provider;
-
- private static final GenericContainer flagdContainer = ContainerConfig.sync();
-
- @BeforeAll()
- public static void setup() throws InterruptedException {
- flagdContainer.start();
- }
-
- @Before()
- public static void setupTest() throws InterruptedException {
- FlagdInProcessSetup.provider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.IN_PROCESS)
- .deadline(1000)
- .streamDeadlineMs(0) // this makes reconnect tests more predictable
- .port(flagdContainer.getFirstMappedPort())
- .build());
- StepDefinitions.setProvider(provider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java
deleted file mode 100644
index 37381a682..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.process.envoy;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.BeforeAll;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdInProcessEnvoySetup {
-
- private static FeatureProvider provider;
-
- private static final GenericContainer flagdContainer = ContainerConfig.sync(false, true);
- private static final GenericContainer envoyContainer = ContainerConfig.envoy();
-
- @BeforeAll()
- public static void setup() throws InterruptedException {
- flagdContainer.start();
- envoyContainer.start();
- final String targetUri =
- String.format("envoy://localhost:%s/flagd-sync.service", envoyContainer.getFirstMappedPort());
-
- FlagdInProcessEnvoySetup.provider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.IN_PROCESS)
- .deadline(1000)
- .streamDeadlineMs(0) // this makes reconnect tests more predictabl
- .targetUri(targetUri)
- .build());
- StepDefinitions.setProvider(provider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- envoyContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
deleted file mode 100644
index 4ab76dd40..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.reconnect.process;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.reconnect.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdInProcessSetup {
-
- private static final GenericContainer flagdContainer = ContainerConfig.sync(true, false);
-
- @BeforeAll()
- public static void setup() throws InterruptedException {
- flagdContainer.start();
- }
-
- @Before()
- public static void setupTest() throws InterruptedException {
- FeatureProvider workingProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.IN_PROCESS)
- .port(flagdContainer.getFirstMappedPort())
- // set a generous deadline, to prevent timeouts in actions
- .deadline(3000)
- .build());
- StepDefinitions.setUnstableProvider(workingProvider);
-
- FeatureProvider unavailableProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.IN_PROCESS)
- .deadline(100)
- .port(9092) // this port isn't serving anything, error expected
- .build());
- StepDefinitions.setUnavailableProvider(unavailableProvider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java
deleted file mode 100644
index e913265d5..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.reconnect.rpc;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.reconnect.steps.StepDefinitions;
-import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.CacheType;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdRpcSetup {
- private static final GenericContainer flagdContainer = ContainerConfig.flagd(true);
-
- @BeforeAll()
- public static void setups() throws InterruptedException {
- flagdContainer.start();
- }
-
- @Before()
- public static void setupTest() throws InterruptedException {
-
- FeatureProvider workingProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.RPC)
- .port(flagdContainer.getFirstMappedPort())
- .deadline(1000)
- .retryGracePeriod(1)
- .streamDeadlineMs(0) // this makes reconnect tests more predictable
- .cacheType(CacheType.DISABLED.getValue())
- .build());
- StepDefinitions.setUnstableProvider(workingProvider);
-
- FeatureProvider unavailableProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.RPC)
- .port(8015) // this port isn't serving anything, error expected
- .deadline(100)
- .cacheType(CacheType.DISABLED.getValue())
- .build());
- StepDefinitions.setUnavailableProvider(unavailableProvider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java
deleted file mode 100644
index d438e7d0f..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.reconnect.steps;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import dev.openfeature.sdk.Client;
-import dev.openfeature.sdk.EventDetails;
-import dev.openfeature.sdk.FeatureProvider;
-import dev.openfeature.sdk.OpenFeatureAPI;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.en.Given;
-import io.cucumber.java.en.Then;
-import io.cucumber.java.en.When;
-import java.time.Duration;
-import java.util.function.Consumer;
-import org.awaitility.Awaitility;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-
-/**
- * Test suite for testing flagd provider reconnect functionality. The associated container run a
- * flagd instance which restarts every 5s.
- */
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class StepDefinitions {
-
- private static Client client;
- private static FeatureProvider unstableProvider;
- private static FeatureProvider unavailableProvider;
-
- private int readyHandlerRunCount = 0;
- private int errorHandlerRunCount = 0;
-
- private Consumer readyHandler = (EventDetails) -> {
- readyHandlerRunCount++;
- };
- private Consumer errorHandler = (EventDetails) -> {
- errorHandlerRunCount++;
- };
-
- /**
- * Injects the client to use for this test. Tests run one at a time, but just in case, a lock is
- * used to make sure the client is not updated mid-test.
- *
- * @param provider client to inject into test.
- */
- public static void setUnstableProvider(FeatureProvider provider) {
- StepDefinitions.unstableProvider = provider;
- }
-
- public static void setUnavailableProvider(FeatureProvider provider) {
- StepDefinitions.unavailableProvider = provider;
- }
-
- public StepDefinitions() {
- StepDefinitions.client = OpenFeatureAPI.getInstance().getClient("unstable");
- OpenFeatureAPI.getInstance().setProviderAndWait("unstable", unstableProvider);
- }
-
- @Given("a flagd provider is set")
- public static void setup() {
- // done in constructor
- }
-
- @AfterAll()
- public static void cleanUp() throws InterruptedException {
- StepDefinitions.unstableProvider.shutdown();
- StepDefinitions.unstableProvider = null;
- StepDefinitions.client = null;
- }
-
- @When("a PROVIDER_READY handler and a PROVIDER_ERROR handler are added")
- public void a_provider_ready_handler_and_a_provider_error_handler_are_added() {
- client.onProviderReady(this.readyHandler);
- client.onProviderError(this.errorHandler);
- }
-
- @Then("the PROVIDER_READY handler must run when the provider connects")
- public void the_provider_ready_handler_must_run_when_the_provider_connects() {
- // no errors expected yet
- assertEquals(0, errorHandlerRunCount);
- // wait up to 240 seconds for a connect (PROVIDER_READY event)
- Awaitility.await().atMost(Duration.ofSeconds(240)).until(() -> {
- return this.readyHandlerRunCount == 1;
- });
- }
-
- @Then("the PROVIDER_ERROR handler must run when the provider's connection is lost")
- public void the_provider_error_handler_must_run_when_the_provider_s_connection_is_lost() {
- // wait up to 240 seconds for a disconnect (PROVIDER_ERROR event)
- Awaitility.await().atMost(Duration.ofSeconds(240)).until(() -> {
- return this.errorHandlerRunCount > 0;
- });
- }
-
- @Then("when the connection is reestablished the PROVIDER_READY handler must run again")
- public void when_the_connection_is_reestablished_the_provider_ready_handler_must_run_again() {
- // wait up to 240 seconds for a reconnect (PROVIDER_READY event)
- Awaitility.await().atMost(Duration.ofSeconds(240)).until(() -> {
- return this.readyHandlerRunCount > 1;
- });
- }
-
- @Given("flagd is unavailable")
- public void flagd_is_unavailable() {
- // there is no flag available on the port used by StepDefinitions.unavailableProvider
- }
-
- @When("a flagd provider is set and initialization is awaited")
- public void a_flagd_provider_is_set_and_initialization_is_awaited() {
- // handled below
- }
-
- @Then("an error should be indicated within the configured deadline")
- public void an_error_should_be_indicated_within_the_configured_deadline() {
- assertThrows(Exception.class, () -> {
- OpenFeatureAPI.getInstance().setProviderAndWait("unavailable", StepDefinitions.unavailableProvider);
- });
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/rpc/FlagdRpcSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/rpc/FlagdRpcSetup.java
deleted file mode 100644
index 3d61dc034..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/rpc/FlagdRpcSetup.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.rpc;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdRpcSetup {
-
- private static FeatureProvider provider;
- private static final GenericContainer flagdContainer = ContainerConfig.flagd();
-
- @BeforeAll()
- public static void setup() {
- flagdContainer.start();
- }
-
- @Before()
- public static void test_setup() {
- FlagdRpcSetup.provider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.RPC)
- .port(flagdContainer.getFirstMappedPort())
- .deadline(500)
- .build());
- StepDefinitions.setProvider(provider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java
deleted file mode 100644
index e75c92394..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.ssl.process;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import java.io.File;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.utility.DockerImageName;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdInProcessSetup {
- private static final GenericContainer flagdContainer = new GenericContainer(
- DockerImageName.parse(ContainerConfig.generateContainerName("flagd", "ssl")))
- .withExposedPorts(8015);
-
- @BeforeAll()
- public static void setups() throws InterruptedException {
- flagdContainer.start();
- }
-
- @Before()
- public static void setupTest() throws InterruptedException {
- String path = "test-harness/ssl/custom-root-cert.crt";
-
- File file = new File(path);
- String absolutePath = file.getAbsolutePath();
- FeatureProvider workingProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.IN_PROCESS)
- .port(flagdContainer.getFirstMappedPort())
- .deadline(10000)
- .streamDeadlineMs(0) // this makes reconnect tests more predictable
- .tls(true)
- .certPath(absolutePath)
- .build());
- StepDefinitions.setProvider(workingProvider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java
deleted file mode 100644
index 6b318ae9e..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.ssl.rpc;
-
-import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.FlagdProvider;
-import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig;
-import dev.openfeature.contrib.providers.flagd.e2e.steps.StepDefinitions;
-import dev.openfeature.sdk.FeatureProvider;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.Before;
-import io.cucumber.java.BeforeAll;
-import java.io.File;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.utility.DockerImageName;
-
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class FlagdRpcSetup {
- private static final GenericContainer flagdContainer = new GenericContainer(
- DockerImageName.parse(ContainerConfig.generateContainerName("flagd", "ssl")))
- .withExposedPorts(8013);
-
- @BeforeAll()
- public static void setups() throws InterruptedException {
- flagdContainer.start();
- }
-
- @Before()
- public static void setupTest() throws InterruptedException {
- String path = "test-harness/ssl/custom-root-cert.crt";
-
- File file = new File(path);
- String absolutePath = file.getAbsolutePath();
- FeatureProvider workingProvider = new FlagdProvider(FlagdOptions.builder()
- .resolverType(Config.Resolver.RPC)
- .port(flagdContainer.getFirstMappedPort())
- .deadline(10000)
- .streamDeadlineMs(0) // this makes reconnect tests more predictable
- .tls(true)
- .certPath(absolutePath)
- .build());
- StepDefinitions.setProvider(workingProvider);
- }
-
- @AfterAll
- public static void tearDown() {
- flagdContainer.stop();
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/AbstractSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/AbstractSteps.java
new file mode 100644
index 000000000..133c1fb49
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/AbstractSteps.java
@@ -0,0 +1,11 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import dev.openfeature.contrib.providers.flagd.e2e.State;
+
+abstract class AbstractSteps {
+ State state;
+
+ public AbstractSteps(State state) {
+ this.state = state;
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ConfigSteps.java
similarity index 57%
rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java
rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ConfigSteps.java
index a680a9947..8e8ee44d6 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ConfigSteps.java
@@ -1,25 +1,22 @@
-package dev.openfeature.contrib.providers.flagd.e2e.steps.config;
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
import static org.assertj.core.api.Assertions.assertThat;
import dev.openfeature.contrib.providers.flagd.Config;
-import dev.openfeature.contrib.providers.flagd.FlagdOptions;
-import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.CacheType;
+import dev.openfeature.contrib.providers.flagd.e2e.State;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
-import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import lombok.extern.slf4j.Slf4j;
-public class ConfigSteps {
+@Slf4j
+public class ConfigSteps extends AbstractSteps {
/**
* Not all properties are correctly implemented, hence that we need to ignore them till this is
* fixed
@@ -31,24 +28,24 @@ public class ConfigSteps {
}
};
- private static final Logger LOG = LoggerFactory.getLogger(ConfigSteps.class);
-
- FlagdOptions.FlagdOptionsBuilder builder = FlagdOptions.builder();
- FlagdOptions options;
+ public ConfigSteps(State state) {
+ super(state);
+ }
@When("a config was initialized")
public void we_initialize_a_config() {
- options = builder.build();
+ state.options = state.builder.build();
}
@When("a config was initialized for {string}")
public void we_initialize_a_config_for(String string) {
switch (string.toLowerCase()) {
case "in-process":
- options = builder.resolverType(Config.Resolver.IN_PROCESS).build();
+ state.options =
+ state.builder.resolverType(Config.Resolver.IN_PROCESS).build();
break;
case "rpc":
- options = builder.resolverType(Config.Resolver.RPC).build();
+ state.options = state.builder.resolverType(Config.Resolver.RPC).build();
break;
default:
throw new RuntimeException("Unknown resolver type: " + string);
@@ -56,19 +53,18 @@ public void we_initialize_a_config_for(String string) {
}
@Given("an option {string} of type {string} with value {string}")
- public void we_have_an_option_of_type_with_value(String option, String type, String value)
- throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ public void we_have_an_option_of_type_with_value(String option, String type, String value) throws Throwable {
if (IGNORED_FOR_NOW.contains(option)) {
- LOG.error("option '{}' is not supported", option);
+ log.error("option '{}' is not supported", option);
return;
}
- Object converted = convert(value, type);
- Method method = Arrays.stream(builder.getClass().getMethods())
+ Object converted = Utils.convert(value, type);
+ Method method = Arrays.stream(state.builder.getClass().getMethods())
.filter(method1 -> method1.getName().equals(mapOptionNames(option)))
.findFirst()
.orElseThrow(RuntimeException::new);
- method.invoke(builder, converted);
+ method.invoke(state.builder, converted);
}
Map envVarsSet = new HashMap<>();
@@ -81,44 +77,18 @@ public void we_have_an_environment_variable_with_value(String varName, String va
EnvironmentVariableUtils.set(varName, value);
}
- private Object convert(String value, String type) throws ClassNotFoundException {
- if (Objects.equals(value, "null")) return null;
- switch (type) {
- case "Boolean":
- return Boolean.parseBoolean(value);
- case "String":
- return value;
- case "Integer":
- return Integer.parseInt(value);
- case "Long":
- return Long.parseLong(value);
- case "ResolverType":
- switch (value.toLowerCase()) {
- case "in-process":
- return Config.Resolver.IN_PROCESS;
- case "rpc":
- return Config.Resolver.RPC;
- default:
- throw new RuntimeException("Unknown resolver type: " + value);
- }
- case "CacheType":
- return CacheType.valueOf(value.toUpperCase()).getValue();
- }
- throw new RuntimeException("Unknown config type: " + type);
- }
-
@Then("the option {string} of type {string} should have the value {string}")
public void the_option_of_type_should_have_the_value(String option, String type, String value) throws Throwable {
- Object convert = convert(value, type);
+ Object convert = Utils.convert(value, type);
if (IGNORED_FOR_NOW.contains(option)) {
- LOG.error("option '{}' is not supported", option);
+ log.error("option '{}' is not supported", option);
return;
}
option = mapOptionNames(option);
- assertThat(options).hasFieldOrPropertyWithValue(option, convert);
+ assertThat(state.options).hasFieldOrPropertyWithValue(option, convert);
// Resetting env vars
for (Map.Entry envVar : envVarsSet.entrySet()) {
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java
new file mode 100644
index 000000000..9de541e23
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java
@@ -0,0 +1,39 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import dev.openfeature.contrib.providers.flagd.e2e.State;
+import dev.openfeature.sdk.ImmutableStructure;
+import dev.openfeature.sdk.MutableContext;
+import dev.openfeature.sdk.Value;
+import io.cucumber.java.en.Given;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.parallel.Isolated;
+
+@Isolated()
+public class ContextSteps extends AbstractSteps {
+
+ public ContextSteps(State state) {
+ super(state);
+ }
+
+ @Given("a context containing a key {string}, with type {string} and with value {string}")
+ public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value)
+ throws ClassNotFoundException, InstantiationException {
+ Map map = state.context.asMap();
+ map.put(key, new Value(value));
+ state.context = new MutableContext(state.context.getTargetingKey(), map);
+ }
+
+ @Given("a context containing a targeting key with value {string}")
+ public void a_context_containing_a_targeting_key_with_value(String string) {
+ state.context.setTargetingKey(string);
+ }
+
+ @Given("a context containing a nested property with outer key {string} and inner key {string}, with value {string}")
+ public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value(
+ String outer, String inner, String value) {
+ Map innerMap = new HashMap<>();
+ innerMap.put(inner, new Value(value));
+ state.context.add(outer, new ImmutableStructure(innerMap));
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/EnvironmentVariableUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EnvironmentVariableUtils.java
similarity index 98%
rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/EnvironmentVariableUtils.java
rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EnvironmentVariableUtils.java
index 6d1946134..b3ef4346e 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/EnvironmentVariableUtils.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EnvironmentVariableUtils.java
@@ -1,4 +1,4 @@
-package dev.openfeature.contrib.providers.flagd.e2e.steps.config;
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
/*
* Copy of JUnit Pioneer's EnvironmentVariable Utils
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Event.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Event.java
new file mode 100644
index 000000000..f93c327e6
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Event.java
@@ -0,0 +1,13 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import dev.openfeature.sdk.EventDetails;
+
+public class Event {
+ public String type;
+ public EventDetails details;
+
+ public Event(String eventType, EventDetails eventDetails) {
+ this.type = eventType;
+ this.details = eventDetails;
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java
new file mode 100644
index 000000000..6dbd0c9ca
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java
@@ -0,0 +1,70 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.awaitility.Awaitility.await;
+
+import dev.openfeature.contrib.providers.flagd.e2e.State;
+import dev.openfeature.sdk.ProviderEvent;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.util.LinkedList;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.parallel.Isolated;
+
+@Isolated()
+@Slf4j
+public class EventSteps extends AbstractSteps {
+
+ public EventSteps(State state) {
+ super(state);
+ state.events = new LinkedList<>();
+ }
+
+ @Given("a {} event handler")
+ public void a_stale_event_handler(String eventType) {
+ state.client.on(mapEventType(eventType), eventDetails -> {
+ log.info("event tracked for {} ", eventType);
+ state.events.add(new Event(eventType, eventDetails));
+ });
+ }
+
+ private static @NotNull ProviderEvent mapEventType(String eventType) {
+ switch (eventType) {
+ case "stale":
+ return ProviderEvent.PROVIDER_STALE;
+ case "ready":
+ return ProviderEvent.PROVIDER_READY;
+ case "error":
+ return ProviderEvent.PROVIDER_ERROR;
+ case "change":
+ return ProviderEvent.PROVIDER_CONFIGURATION_CHANGED;
+ default:
+ throw new IllegalArgumentException("Unknown event type: " + eventType);
+ }
+ }
+
+ @When("a {} event was fired")
+ public void eventWasFired(String eventType) throws InterruptedException {
+ eventHandlerShouldBeExecutedWithin(eventType, 10000);
+ // we might be too fast in the execution
+ Thread.sleep(500);
+ }
+
+ @Then("the {} event handler should have been executed")
+ public void eventHandlerShouldBeExecuted(String eventType) {
+ eventHandlerShouldBeExecutedWithin(eventType, 30000);
+ }
+
+ @Then("the {} event handler should have been executed within {int}ms")
+ public void eventHandlerShouldBeExecutedWithin(String eventType, int ms) {
+ log.info("waiting for eventtype: {}", eventType);
+ await().atMost(ms, MILLISECONDS)
+ .until(() -> state.events.stream().anyMatch(event -> event.type.equals(eventType)));
+ state.lastEvent = state.events.stream()
+ .filter(event -> event.type.equals(eventType))
+ .findFirst();
+ state.events.removeIf(event -> event.type.equals(eventType));
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java
new file mode 100644
index 000000000..aab14a857
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java
@@ -0,0 +1,95 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import dev.openfeature.contrib.providers.flagd.e2e.State;
+import dev.openfeature.sdk.FlagEvaluationDetails;
+import dev.openfeature.sdk.Value;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import org.junit.jupiter.api.parallel.Isolated;
+
+@Isolated()
+public class FlagSteps extends AbstractSteps {
+
+ public FlagSteps(State state) {
+ super(state);
+ }
+
+ @Given("a {}-flag with key {string} and a default value {string}")
+ public void givenAFlag(String type, String name, String defaultValue) throws Throwable {
+ state.flag = new Flag(type, name, Utils.convert(defaultValue, type));
+ }
+
+ @When("the flag was evaluated with details")
+ public void the_flag_was_evaluated_with_details() throws InterruptedException {
+ FlagEvaluationDetails details;
+ switch (state.flag.type) {
+ case "String":
+ details =
+ state.client.getStringDetails(state.flag.name, (String) state.flag.defaultValue, state.context);
+ break;
+ case "Boolean":
+ details = state.client.getBooleanDetails(
+ state.flag.name, (Boolean) state.flag.defaultValue, state.context);
+ break;
+ case "Float":
+ details =
+ state.client.getDoubleDetails(state.flag.name, (Double) state.flag.defaultValue, state.context);
+ break;
+ case "Integer":
+ details = state.client.getIntegerDetails(
+ state.flag.name, (Integer) state.flag.defaultValue, state.context);
+ break;
+ case "Object":
+ details =
+ state.client.getObjectDetails(state.flag.name, (Value) state.flag.defaultValue, state.context);
+ break;
+ default:
+ throw new AssertionError();
+ }
+ state.evaluation = details;
+ }
+
+ @Then("the resolved details value should be \"{}\"")
+ public void the_resolved_details_value_should_be(String value) throws Throwable {
+ assertThat(state.evaluation.getValue()).isEqualTo(Utils.convert(value, state.flag.type));
+ }
+
+ @Then("the reason should be {string}")
+ public void the_reason_should_be(String reason) {
+ assertThat(state.evaluation.getReason()).isEqualTo(reason);
+ }
+
+ @Then("the variant should be {string}")
+ public void the_variant_should_be(String variant) {
+ assertThat(state.evaluation.getVariant()).isEqualTo(variant);
+ }
+
+ @Then("the flag should be part of the event payload")
+ @Then("the flag was modified")
+ public void the_flag_was_modified() {
+ await().atMost(5000, MILLISECONDS).until(() -> state.events.stream()
+ .anyMatch(event -> event.type.equals("change")
+ && event.details.getFlagsChanged().contains(state.flag.name)));
+ state.lastEvent = state.events.stream()
+ .filter(event -> event.type.equals("change")
+ && event.details.getFlagsChanged().contains(state.flag.name))
+ .findFirst();
+ }
+
+ public class Flag {
+ String name;
+ Object defaultValue;
+ String type;
+
+ public Flag(String type, String name, Object defaultValue) {
+ this.name = name;
+ this.defaultValue = defaultValue;
+ this.type = type;
+ }
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java
new file mode 100644
index 000000000..cf0e5ed0c
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java
@@ -0,0 +1,232 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.FlagdProvider;
+import dev.openfeature.contrib.providers.flagd.e2e.FlagdContainer;
+import dev.openfeature.contrib.providers.flagd.e2e.State;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import eu.rekawek.toxiproxy.Proxy;
+import eu.rekawek.toxiproxy.ToxiproxyClient;
+import eu.rekawek.toxiproxy.model.ToxicDirection;
+import io.cucumber.java.After;
+import io.cucumber.java.AfterAll;
+import io.cucumber.java.Before;
+import io.cucumber.java.BeforeAll;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.When;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Timer;
+import java.util.TimerTask;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.ToxiproxyContainer;
+import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
+
+@Isolated()
+@Slf4j
+public class ProviderSteps extends AbstractSteps {
+
+ public static final int UNAVAILABLE_PORT = 9999;
+ static Map containers = new HashMap<>();
+ public static Network network = Network.newNetwork();
+ public static ToxiproxyContainer toxiproxy =
+ new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0").withNetwork(network);
+ public static ToxiproxyClient toxiproxyClient;
+
+ static Path sharedTempDir;
+
+ public ProviderSteps(State state) {
+ super(state);
+ }
+
+ static String generateProxyName(Config.Resolver resolver, ProviderType providerType) {
+ return providerType + "-" + resolver;
+ }
+
+ @BeforeAll
+ public static void beforeAll() throws IOException {
+ toxiproxy.start();
+ toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort());
+ toxiproxyClient.createProxy(
+ generateProxyName(Config.Resolver.RPC, ProviderType.DEFAULT), "0.0.0.0:8666", "default:8013");
+
+ toxiproxyClient.createProxy(
+ generateProxyName(Config.Resolver.IN_PROCESS, ProviderType.DEFAULT), "0.0.0.0:8667", "default:8015");
+ toxiproxyClient.createProxy(
+ generateProxyName(Config.Resolver.RPC, ProviderType.SSL), "0.0.0.0:8668", "ssl:8013");
+ toxiproxyClient.createProxy(
+ generateProxyName(Config.Resolver.IN_PROCESS, ProviderType.SSL), "0.0.0.0:8669", "ssl:8015");
+
+ containers.put(
+ ProviderType.DEFAULT, new FlagdContainer().withNetwork(network).withNetworkAliases("default"));
+ containers.put(
+ ProviderType.SSL, new FlagdContainer("ssl").withNetwork(network).withNetworkAliases("ssl"));
+ sharedTempDir = Files.createDirectories(
+ Paths.get("tmp/" + RandomStringUtils.randomAlphanumeric(8).toLowerCase() + "/"));
+ containers.put(
+ ProviderType.SOCKET,
+ new FlagdContainer("socket")
+ .withFileSystemBind(sharedTempDir.toAbsolutePath().toString(), "/tmp", BindMode.READ_WRITE));
+ }
+
+ @AfterAll
+ public static void afterAll() throws IOException {
+
+ containers.forEach((name, container) -> container.stop());
+ FileUtils.deleteDirectory(sharedTempDir.toFile());
+ toxiproxyClient.reset();
+ toxiproxy.stop();
+ }
+
+ @Before
+ public void before() throws IOException {
+
+ toxiproxyClient.getProxies().forEach(proxy -> {
+ try {
+ proxy.toxics().getAll().forEach(toxic -> {
+ try {
+ toxic.remove();
+ } catch (IOException e) {
+ log.debug("Failed to remove timout", e);
+ }
+ });
+ } catch (IOException e) {
+ log.debug("Failed to remove timout", e);
+ }
+ });
+
+ containers.values().stream()
+ .filter(containers -> !containers.isRunning())
+ .forEach(FlagdContainer::start);
+ }
+
+ @After
+ public void tearDown() {
+ OpenFeatureAPI.getInstance().shutdown();
+ }
+
+ public int getPort(Config.Resolver resolver, ProviderType providerType) {
+ switch (resolver) {
+ case RPC:
+ switch (providerType) {
+ case DEFAULT:
+ return toxiproxy.getMappedPort(8666);
+ case SSL:
+ return toxiproxy.getMappedPort(8668);
+ }
+ case IN_PROCESS:
+ switch (providerType) {
+ case DEFAULT:
+ return toxiproxy.getMappedPort(8667);
+ case SSL:
+ return toxiproxy.getMappedPort(8669);
+ }
+ default:
+ throw new IllegalArgumentException("Unsupported resolver: " + resolver);
+ }
+ }
+
+ @Given("a {} flagd provider")
+ public void setupProvider(String providerType) throws IOException {
+ state.builder.deadline(500).keepAlive(0).retryGracePeriod(3);
+ boolean wait = true;
+ switch (providerType) {
+ case "unavailable":
+ this.state.providerType = ProviderType.SOCKET;
+ state.builder.port(UNAVAILABLE_PORT);
+ wait = false;
+ break;
+ case "socket":
+ this.state.providerType = ProviderType.SOCKET;
+ String socketPath =
+ sharedTempDir.resolve("socket.sock").toAbsolutePath().toString();
+ state.builder.socketPath(socketPath);
+ state.builder.port(UNAVAILABLE_PORT);
+ break;
+ case "ssl":
+ String path = "test-harness/ssl/custom-root-cert.crt";
+
+ File file = new File(path);
+ String absolutePath = file.getAbsolutePath();
+ this.state.providerType = ProviderType.SSL;
+ state.builder
+ .port(getPort(State.resolverType, state.providerType))
+ .tls(true)
+ .certPath(absolutePath);
+ break;
+ case "offline":
+ File flags = new File("test-harness/flags");
+ ObjectMapper objectMapper = new ObjectMapper();
+ Object merged = new Object();
+ for (File listFile : Objects.requireNonNull(flags.listFiles())) {
+ ObjectReader updater = objectMapper.readerForUpdating(merged);
+ merged = updater.readValue(listFile, Object.class);
+ }
+ Path offlinePath = Files.createTempFile("flags", ".json");
+ objectMapper.writeValue(offlinePath.toFile(), merged);
+
+ state.builder
+ .port(UNAVAILABLE_PORT)
+ .offlineFlagSourcePath(offlinePath.toAbsolutePath().toString());
+ break;
+
+ default:
+ this.state.providerType = ProviderType.DEFAULT;
+ state.builder.port(getPort(State.resolverType, state.providerType));
+ break;
+ }
+ FeatureProvider provider =
+ new FlagdProvider(state.builder.resolverType(State.resolverType).build());
+
+ OpenFeatureAPI api = OpenFeatureAPI.getInstance();
+ if (wait) {
+ api.setProviderAndWait(providerType, provider);
+ } else {
+ api.setProvider(providerType, provider);
+ }
+ this.state.client = api.getClient(providerType);
+ }
+
+ @When("the connection is lost for {int}s")
+ public void the_connection_is_lost_for(int seconds) throws InterruptedException, IOException {
+ log.info("Timeout and wait for {} seconds", seconds);
+ String randomizer = RandomStringUtils.randomAlphanumeric(5);
+ String timeoutUpName = "restart-up-" + randomizer;
+ String timeoutDownName = "restart-down-" + randomizer;
+ Proxy proxy = toxiproxyClient.getProxy(generateProxyName(State.resolverType, state.providerType));
+ proxy.toxics().timeout(timeoutDownName, ToxicDirection.DOWNSTREAM, seconds);
+ proxy.toxics().timeout(timeoutUpName, ToxicDirection.UPSTREAM, seconds);
+
+ TimerTask task = new TimerTask() {
+ public void run() {
+ try {
+ proxy.toxics().get(timeoutUpName).remove();
+ proxy.toxics().get(timeoutDownName).remove();
+ } catch (IOException e) {
+ log.debug("Failed to remove timeout", e);
+ }
+ }
+ };
+ Timer restartTimer = new Timer("Timer" + randomizer);
+
+ restartTimer.schedule(task, seconds * 1000L);
+ }
+
+ static FlagdContainer getContainer(ProviderType providerType) {
+ log.info("getting container for {}", providerType);
+ return containers.getOrDefault(providerType, containers.get(ProviderType.DEFAULT));
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderType.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderType.java
new file mode 100644
index 000000000..eac0ba967
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderType.java
@@ -0,0 +1,7 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+public enum ProviderType {
+ DEFAULT,
+ SSL,
+ SOCKET
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/StepDefinitions.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/StepDefinitions.java
deleted file mode 100644
index c3f894a08..000000000
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/StepDefinitions.java
+++ /dev/null
@@ -1,555 +0,0 @@
-package dev.openfeature.contrib.providers.flagd.e2e.steps;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import dev.openfeature.sdk.Client;
-import dev.openfeature.sdk.EvaluationContext;
-import dev.openfeature.sdk.EventDetails;
-import dev.openfeature.sdk.FeatureProvider;
-import dev.openfeature.sdk.FlagEvaluationDetails;
-import dev.openfeature.sdk.ImmutableContext;
-import dev.openfeature.sdk.ImmutableStructure;
-import dev.openfeature.sdk.OpenFeatureAPI;
-import dev.openfeature.sdk.Reason;
-import dev.openfeature.sdk.Structure;
-import dev.openfeature.sdk.Value;
-import io.cucumber.java.AfterAll;
-import io.cucumber.java.en.And;
-import io.cucumber.java.en.Given;
-import io.cucumber.java.en.Then;
-import io.cucumber.java.en.When;
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Consumer;
-import org.awaitility.Awaitility;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.parallel.Isolated;
-
-/** Common test suite used by both RPC and in-process flagd providers. */
-@Isolated()
-@Order(value = Integer.MAX_VALUE)
-public class StepDefinitions {
-
- private static Client client;
- private static FeatureProvider provider;
-
- private String booleanFlagKey;
- private String stringFlagKey;
- private String intFlagKey;
- private String doubleFlagKey;
- private String objectFlagKey;
-
- private boolean booleanFlagDefaultValue;
- private String stringFlagDefaultValue;
- private int intFlagDefaultValue;
- private double doubleFlagDefaultValue;
- private Value objectFlagDefaultValue;
-
- private FlagEvaluationDetails objectFlagDetails;
-
- private String contextAwareFlagKey;
- private String contextAwareDefaultValue;
- private EvaluationContext context;
- private String contextAwareValue;
-
- private String notFoundFlagKey;
- private String notFoundDefaultValue;
- private FlagEvaluationDetails notFoundDetails;
- private String typeErrorFlagKey;
- private int typeErrorDefaultValue;
- private FlagEvaluationDetails typeErrorDetails;
-
- private boolean isChangeHandlerRun = false;
- private String changedFlag;
- private boolean isReadyHandlerRun = false;
-
- private Consumer changeHandler;
- private Consumer readyHandler;
-
- /**
- * Injects the client to use for this test. Tests run one at a time, but just in case, a lock is
- * used to make sure the client is not updated mid-test.
- *
- * @param client client to inject into test.
- */
- public static void setProvider(FeatureProvider provider) {
- StepDefinitions.provider = provider;
- }
-
- public StepDefinitions() {
- OpenFeatureAPI.getInstance().setProviderAndWait("e2e", provider);
- StepDefinitions.client = OpenFeatureAPI.getInstance().getClient("e2e");
- }
-
- @Given("a provider is registered")
- @Given("a flagd provider is set")
- public static void setup() {
- // done in constructor
- }
-
- @AfterAll()
- public static void cleanUp() throws InterruptedException {
- StepDefinitions.provider.shutdown();
- StepDefinitions.provider = null;
- StepDefinitions.client = null;
- }
-
- /*
- * Basic evaluation
- */
-
- // boolean value
- @When("a boolean flag with key {string} is evaluated with default value {string}")
- public void a_boolean_flag_with_key_boolean_flag_is_evaluated_with_default_value_false(
- String flagKey, String defaultValue) {
- this.booleanFlagKey = flagKey;
- this.booleanFlagDefaultValue = Boolean.valueOf(defaultValue);
- }
-
- @Then("the resolved boolean value should be {string}")
- public void the_resolved_boolean_value_should_be_true(String expected) {
- boolean value = client.getBooleanValue(this.booleanFlagKey, Boolean.valueOf(this.booleanFlagDefaultValue));
- assertEquals(Boolean.valueOf(expected), value);
- }
-
- // string value
-
- @When("a string flag with key {string} is evaluated with default value {string}")
- public void a_string_flag_with_key_is_evaluated_with_default_value(String flagKey, String defaultValue) {
- this.stringFlagKey = flagKey;
- this.stringFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved string value should be {string}")
- public void the_resolved_string_value_should_be(String expected) {
- String value = client.getStringValue(this.stringFlagKey, this.stringFlagDefaultValue);
- assertEquals(expected, value);
- }
-
- // integer value
- @When("an integer flag with key {string} is evaluated with default value {int}")
- public void an_integer_flag_with_key_is_evaluated_with_default_value(String flagKey, Integer defaultValue) {
- this.intFlagKey = flagKey;
- this.intFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved integer value should be {int}")
- public void the_resolved_integer_value_should_be(int expected) {
- int value = client.getIntegerValue(this.intFlagKey, this.intFlagDefaultValue);
- assertEquals(expected, value);
- }
-
- // float/double value
- @When("a float flag with key {string} is evaluated with default value {double}")
- public void a_float_flag_with_key_is_evaluated_with_default_value(String flagKey, double defaultValue) {
- this.doubleFlagKey = flagKey;
- this.doubleFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved float value should be {double}")
- public void the_resolved_float_value_should_be(double expected) {
- double value = client.getDoubleValue(this.doubleFlagKey, this.doubleFlagDefaultValue);
- assertEquals(expected, value);
- }
-
- // object value
- @When("an object flag with key {string} is evaluated with a null default value")
- public void an_object_flag_with_key_is_evaluated_with_a_null_default_value(String flagKey) {
- this.objectFlagKey = flagKey;
- this.objectFlagDefaultValue = new Value(); // empty value is equivalent to null
- }
-
- @Then(
- "the resolved object value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively")
- public void the_resolved_object_value_should_be_contain_fields_and_with_values_and_respectively(
- String boolField,
- String stringField,
- String numberField,
- String boolValue,
- String stringValue,
- int numberValue) {
- Value value = client.getObjectValue(this.objectFlagKey, this.objectFlagDefaultValue);
- Structure structure = value.asStructure();
-
- assertEquals(
- Boolean.valueOf(boolValue), structure.asMap().get(boolField).asBoolean());
- assertEquals(stringValue, structure.asMap().get(stringField).asString());
- assertEquals(numberValue, structure.asMap().get(numberField).asInteger());
- }
-
- /*
- * Detailed evaluation
- */
-
- // boolean details
- @When("a boolean flag with key {string} is evaluated with details and default value {string}")
- public void a_boolean_flag_with_key_is_evaluated_with_details_and_default_value(
- String flagKey, String defaultValue) {
- this.booleanFlagKey = flagKey;
- this.booleanFlagDefaultValue = Boolean.valueOf(defaultValue);
- }
-
- @Then(
- "the resolved boolean details value should be {string}, the variant should be {string}, and the reason should be {string}")
- public void the_resolved_boolean_value_should_be_the_variant_should_be_and_the_reason_should_be(
- String expectedValue, String expectedVariant, String expectedReason) {
- FlagEvaluationDetails details =
- client.getBooleanDetails(this.booleanFlagKey, Boolean.valueOf(this.booleanFlagDefaultValue));
-
- assertEquals(Boolean.valueOf(expectedValue), details.getValue());
- assertEquals(expectedVariant, details.getVariant());
- assertEquals(expectedReason, details.getReason());
- }
-
- // string details
- @When("a string flag with key {string} is evaluated with details and default value {string}")
- public void a_string_flag_with_key_is_evaluated_with_details_and_default_value(
- String flagKey, String defaultValue) {
- this.stringFlagKey = flagKey;
- this.stringFlagDefaultValue = defaultValue;
- }
-
- @When("a string flag with key {string} is evaluated with details")
- public void a_string_flag_with_key_is_evaluated_with_details(String flagKey) {
- this.stringFlagKey = flagKey;
- this.stringFlagDefaultValue = "";
- }
-
- @Then(
- "the resolved string details value should be {string}, the variant should be {string}, and the reason should be {string}")
- public void the_resolved_string_value_should_be_the_variant_should_be_and_the_reason_should_be(
- String expectedValue, String expectedVariant, String expectedReason) {
- FlagEvaluationDetails details =
- client.getStringDetails(this.stringFlagKey, this.stringFlagDefaultValue);
-
- assertEquals(expectedValue, details.getValue());
- assertEquals(expectedVariant, details.getVariant());
- assertEquals(expectedReason, details.getReason());
- }
-
- // integer details
- @When("an integer flag with key {string} is evaluated with details and default value {int}")
- public void an_integer_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, int defaultValue) {
- this.intFlagKey = flagKey;
- this.intFlagDefaultValue = defaultValue;
- }
-
- @Then(
- "the resolved integer details value should be {int}, the variant should be {string}, and the reason should be {string}")
- public void the_resolved_integer_value_should_be_the_variant_should_be_and_the_reason_should_be(
- int expectedValue, String expectedVariant, String expectedReason) {
- FlagEvaluationDetails details = client.getIntegerDetails(this.intFlagKey, this.intFlagDefaultValue);
-
- assertEquals(expectedValue, details.getValue());
- assertEquals(expectedVariant, details.getVariant());
- assertEquals(expectedReason, details.getReason());
- }
-
- // float/double details
- @When("a float flag with key {string} is evaluated with details and default value {double}")
- public void a_float_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, double defaultValue) {
- this.doubleFlagKey = flagKey;
- this.doubleFlagDefaultValue = defaultValue;
- }
-
- @Then(
- "the resolved float details value should be {double}, the variant should be {string}, and the reason should be {string}")
- public void the_resolved_float_value_should_be_the_variant_should_be_and_the_reason_should_be(
- double expectedValue, String expectedVariant, String expectedReason) {
- FlagEvaluationDetails details =
- client.getDoubleDetails(this.doubleFlagKey, this.doubleFlagDefaultValue);
-
- assertEquals(expectedValue, details.getValue());
- assertEquals(expectedVariant, details.getVariant());
- assertEquals(expectedReason, details.getReason());
- }
-
- // object details
- @When("an object flag with key {string} is evaluated with details and a null default value")
- public void an_object_flag_with_key_is_evaluated_with_details_and_a_null_default_value(String flagKey) {
- this.objectFlagKey = flagKey;
- this.objectFlagDefaultValue = new Value();
- }
-
- @Then(
- "the resolved object details value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively")
- public void the_resolved_object_value_should_be_contain_fields_and_with_values_and_respectively_again(
- String boolField,
- String stringField,
- String numberField,
- String boolValue,
- String stringValue,
- int numberValue) {
- this.objectFlagDetails = client.getObjectDetails(this.objectFlagKey, this.objectFlagDefaultValue);
- Structure structure = this.objectFlagDetails.getValue().asStructure();
-
- assertEquals(
- Boolean.valueOf(boolValue), structure.asMap().get(boolField).asBoolean());
- assertEquals(stringValue, structure.asMap().get(stringField).asString());
- assertEquals(numberValue, structure.asMap().get(numberField).asInteger());
- }
-
- @Then("the variant should be {string}, and the reason should be {string}")
- public void the_variant_should_be_and_the_reason_should_be(String expectedVariant, String expectedReason) {
- assertEquals(expectedVariant, this.objectFlagDetails.getVariant());
- assertEquals(expectedReason, this.objectFlagDetails.getReason());
- }
-
- /*
- * Context-aware evaluation
- */
-
- @When(
- "context contains keys {string}, {string}, {string}, {string} with values {string}, {string}, {int}, {string}")
- public void context_contains_keys_with_values(
- String field1,
- String field2,
- String field3,
- String field4,
- String value1,
- String value2,
- Integer value3,
- String value4) {
- Map attributes = new HashMap<>();
- attributes.put(field1, new Value(value1));
- attributes.put(field2, new Value(value2));
- attributes.put(field3, new Value(value3));
- attributes.put(field4, new Value(Boolean.valueOf(value4)));
- this.context = new ImmutableContext(attributes);
- }
-
- @When("a flag with key {string} is evaluated with default value {string}")
- public void an_a_flag_with_key_is_evaluated(String flagKey, String defaultValue) {
- contextAwareFlagKey = flagKey;
- contextAwareDefaultValue = defaultValue;
- contextAwareValue = client.getStringValue(flagKey, contextAwareDefaultValue, context);
- }
-
- @Then("the resolved string response should be {string}")
- public void the_resolved_string_response_should_be(String expected) {
- assertEquals(expected, this.contextAwareValue);
- }
-
- @Then("the resolved flag value is {string} when the context is empty")
- public void the_resolved_flag_value_is_when_the_context_is_empty(String expected) {
- String emptyContextValue =
- client.getStringValue(contextAwareFlagKey, contextAwareDefaultValue, new ImmutableContext());
- assertEquals(expected, emptyContextValue);
- }
-
- /*
- * Errors
- */
-
- // not found
- @When("a non-existent string flag with key {string} is evaluated with details and a default value {string}")
- public void a_non_existent_string_flag_with_key_is_evaluated_with_details_and_a_default_value(
- String flagKey, String defaultValue) {
- notFoundFlagKey = flagKey;
- notFoundDefaultValue = defaultValue;
- notFoundDetails = client.getStringDetails(notFoundFlagKey, notFoundDefaultValue);
- }
-
- @Then("the default string value should be returned")
- public void then_the_default_string_value_should_be_returned() {
- assertEquals(notFoundDefaultValue, notFoundDetails.getValue());
- }
-
- @Then("the reason should indicate an error and the error code should indicate a missing flag with {string}")
- public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) {
- assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason());
- assertEquals(errorCode, notFoundDetails.getErrorCode().toString());
- }
-
- // type mismatch
- @When("a string flag with key {string} is evaluated as an integer, with details and a default value {int}")
- public void a_string_flag_with_key_is_evaluated_as_an_integer_with_details_and_a_default_value(
- String flagKey, int defaultValue) {
- typeErrorFlagKey = flagKey;
- typeErrorDefaultValue = defaultValue;
- typeErrorDetails = client.getIntegerDetails(typeErrorFlagKey, typeErrorDefaultValue);
- }
-
- @Then("the default integer value should be returned")
- public void then_the_default_integer_value_should_be_returned() {
- assertEquals(typeErrorDefaultValue, typeErrorDetails.getValue());
- }
-
- @Then("the reason should indicate an error and the error code should indicate a type mismatch with {string}")
- public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) {
- assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason());
- assertEquals(errorCode, typeErrorDetails.getErrorCode().toString());
- }
-
- /*
- * Custom JSON evaluators (only run for flagd-in-process)
- */
-
- @And("a context containing a nested property with outer key {string} and inner key {string}, with value {string}")
- public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value(
- String outerKey, String innerKey, String value) throws InstantiationException {
- Map innerMap = new HashMap();
- innerMap.put(innerKey, new Value(value));
- Map outerMap = new HashMap();
- outerMap.put(outerKey, new Value(new ImmutableStructure(innerMap)));
- this.context = new ImmutableContext(outerMap);
- }
-
- @And("a context containing a nested property with outer key {string} and inner key {string}, with value {int}")
- public void a_context_containing_a_nested_property_with_outer_key_and_inner_key_with_value_int(
- String outerKey, String innerKey, Integer value) throws InstantiationException {
- Map innerMap = new HashMap();
- innerMap.put(innerKey, new Value(value));
- Map outerMap = new HashMap();
- outerMap.put(outerKey, new Value(new ImmutableStructure(innerMap)));
- this.context = new ImmutableContext(outerMap);
- }
-
- @And("a context containing a key {string}, with value {string}")
- public void a_context_containing_a_key_with_value(String key, String value) {
- Map attrs = new HashMap();
- attrs.put(key, new Value(value));
- this.context = new ImmutableContext(attrs);
- }
-
- @And("a context containing a key {string}, with value {double}")
- public void a_context_containing_a_key_with_value_double(String key, Double value) {
- Map attrs = new HashMap();
- attrs.put(key, new Value(value));
- this.context = new ImmutableContext(attrs);
- }
-
- @Then("the returned value should be {string}")
- public void the_returned_value_should_be(String expected) {
- String value = client.getStringValue(this.stringFlagKey, this.stringFlagDefaultValue, this.context);
- assertEquals(expected, value);
- }
-
- @Then("the returned value should be {int}")
- public void the_returned_value_should_be(Integer expectedValue) {
- Integer value = client.getIntegerValue(this.intFlagKey, this.intFlagDefaultValue, this.context);
- assertEquals(expectedValue, value);
- }
-
- /*
- * Events
- */
-
- // Flag change event
- @When("a PROVIDER_CONFIGURATION_CHANGED handler is added")
- public void a_provider_configuration_changed_handler_is_added() {
- this.changeHandler = (EventDetails details) -> {
- if (details.getFlagsChanged().size() > 0) {
- // we get multiple change events from the test container...
- // we're only interested in the ones with the changed flag in question
- this.changedFlag = details.getFlagsChanged().get(0);
- this.isChangeHandlerRun = true;
- }
- };
- client.onProviderConfigurationChanged(this.changeHandler);
- }
-
- @When("a flag with key {string} is modified")
- public void a_flag_with_key_is_modified(String flagKey) {
- // This happens automatically
- }
-
- @Then("the PROVIDER_CONFIGURATION_CHANGED handler must run")
- public void the_provider_configuration_changed_handler_must_run() {
- Awaitility.await().atMost(Duration.ofSeconds(2)).until(() -> {
- return this.isChangeHandlerRun;
- });
- }
-
- @Then("the event details must indicate {string} was altered")
- public void the_event_details_must_indicate_was_altered(String flagKey) {
- assertEquals(flagKey, this.changedFlag);
- }
-
- // Provider ready event
- @When("a PROVIDER_READY handler is added")
- public void a_provider_ready_handler_is_added() {
- this.readyHandler = (EventDetails details) -> {
- this.isReadyHandlerRun = true;
- };
- client.onProviderReady(this.readyHandler);
- }
-
- @Then("the PROVIDER_READY handler must run")
- public void the_provider_ready_handler_must_run() {
- Awaitility.await().atMost(Duration.ofSeconds(2)).until(() -> {
- return this.isReadyHandlerRun;
- });
- }
-
- /*
- * Zero Value
- */
-
- // boolean value
- @When("a zero-value boolean flag with key {string} is evaluated with default value {string}")
- public void a_zero_value_boolean_flag_with_key_is_evaluated_with_default_value(
- String flagKey, String defaultValue) {
- this.booleanFlagKey = flagKey;
- this.booleanFlagDefaultValue = Boolean.valueOf(defaultValue);
- }
-
- @Then("the resolved boolean zero-value should be {string}")
- public void the_resolved_boolean_zero_value_should_be(String expected) {
- boolean value = client.getBooleanValue(this.booleanFlagKey, this.booleanFlagDefaultValue);
- assertEquals(Boolean.valueOf(expected), value);
- }
-
- // float/double value
- @When("a zero-value float flag with key {string} is evaluated with default value {double}")
- public void a_zero_value_float_flag_with_key_is_evaluated_with_default_value(String flagKey, Double defaultValue) {
- this.doubleFlagKey = flagKey;
- this.doubleFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved float zero-value should be {double}")
- public void the_resolved_float_zero_value_should_be(Double expected) {
- FlagEvaluationDetails details = client.getDoubleDetails("float-zero-flag", this.doubleFlagDefaultValue);
- assertEquals(expected, details.getValue());
- }
-
- // integer value
- @When("a zero-value integer flag with key {string} is evaluated with default value {int}")
- public void a_zero_value_integer_flag_with_key_is_evaluated_with_default_value(
- String flagKey, Integer defaultValue) {
- this.intFlagKey = flagKey;
- this.intFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved integer zero-value should be {int}")
- public void the_resolved_integer_zero_value_should_be(Integer expected) {
- int value = client.getIntegerValue(this.intFlagKey, this.intFlagDefaultValue);
- assertEquals(expected, value);
- }
-
- // string value
- @When("a zero-value string flag with key {string} is evaluated with default value {string}")
- public void a_zero_value_string_flag_with_key_is_evaluated_with_default_value(String flagKey, String defaultValue) {
- this.stringFlagKey = flagKey;
- this.stringFlagDefaultValue = defaultValue;
- }
-
- @Then("the resolved string zero-value should be {string}")
- public void the_resolved_string_zero_value_should_be(String expected) {
- String value = client.getStringValue(this.stringFlagKey, this.stringFlagDefaultValue);
- assertEquals(expected, value);
- }
-
- @When("a context containing a targeting key with value {string}")
- public void a_context_containing_a_targeting_key_with_value(String targetingKey) {
- this.context = new ImmutableContext(targetingKey);
- }
-
- @Then("the returned reason should be {string}")
- public void the_returned_reason_should_be(String reason) {
- FlagEvaluationDetails details =
- client.getStringDetails(this.stringFlagKey, this.stringFlagDefaultValue, this.context);
- assertEquals(reason, details.getReason());
- }
-}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java
new file mode 100644
index 000000000..909d4800a
--- /dev/null
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java
@@ -0,0 +1,43 @@
+package dev.openfeature.contrib.providers.flagd.e2e.steps;
+
+import dev.openfeature.contrib.providers.flagd.Config;
+import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.CacheType;
+import dev.openfeature.sdk.Value;
+import java.io.IOException;
+import java.util.Objects;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+public final class Utils {
+
+ private Utils() {}
+
+ public static Object convert(String value, String type) throws ClassNotFoundException, IOException {
+ if (Objects.equals(value, "null")) return null;
+ switch (type) {
+ case "Boolean":
+ return Boolean.parseBoolean(value);
+ case "String":
+ return value;
+ case "Integer":
+ return Integer.parseInt(value);
+ case "Float":
+ return Double.parseDouble(value);
+ case "Long":
+ return Long.parseLong(value);
+ case "ResolverType":
+ switch (value.toLowerCase()) {
+ case "in-process":
+ return Config.Resolver.IN_PROCESS;
+ case "rpc":
+ return Config.Resolver.RPC;
+ default:
+ throw new RuntimeException("Unknown resolver type: " + value);
+ }
+ case "CacheType":
+ return CacheType.valueOf(value.toUpperCase()).getValue();
+ case "Object":
+ return Value.objectToValue(new ObjectMapper().readValue(value, Object.class));
+ }
+ throw new RuntimeException("Unknown config type: " + type);
+ }
+}
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnectorTest.java
index 4c417f957..596042bb5 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnectorTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/common/GrpcConnectorTest.java
@@ -70,44 +70,12 @@ private void tearDownGrpcServer() throws InterruptedException {
}
}
- @Test
- void whenShuttingDownAndRestartingGrpcServer_ConsumerReceivesDisconnectedAndConnectedEvent() throws Exception {
- CountDownLatch sync = new CountDownLatch(2);
- ArrayList connectionStateChanges = Lists.newArrayList();
- Consumer testConsumer = event -> {
- connectionStateChanges.add(event.isConnected());
- sync.countDown();
- };
-
- GrpcConnector instance = new GrpcConnector<>(
- FlagdOptions.builder().build(),
- ServiceGrpc::newStub,
- ServiceGrpc::newBlockingStub,
- testConsumer,
- stub -> stub.eventStream(Evaluation.EventStreamRequest.getDefaultInstance(), mockEventStreamObserver),
- testChannel);
-
- instance.initialize();
-
- // when shutting down server
- testServer.shutdown();
- testServer.awaitTermination(1, TimeUnit.SECONDS);
-
- // when restarting server
- setupTestGrpcServer();
-
- // then consumer received DISCONNECTED and CONNECTED event
- boolean finished = sync.await(10, TimeUnit.SECONDS);
- Assertions.assertTrue(finished);
- Assertions.assertEquals(Lists.newArrayList(DISCONNECTED, CONNECTED), connectionStateChanges);
- }
-
@Test
void whenShuttingDownGrpcConnector_ConsumerReceivesDisconnectedEvent() throws Exception {
CountDownLatch sync = new CountDownLatch(1);
ArrayList connectionStateChanges = Lists.newArrayList();
- Consumer testConsumer = event -> {
- connectionStateChanges.add(event.isConnected());
+ Consumer testConsumer = event -> {
+ connectionStateChanges.add(!event.isDisconnected());
sync.countDown();
};
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
index 9370f821a..a4183deb7 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/grpc/EventStreamObserverTest.java
@@ -1,19 +1,12 @@
package dev.openfeature.contrib.providers.flagd.resolver.grpc;
-import static org.junit.Assert.assertTrue;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.atMost;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
-import dev.openfeature.contrib.providers.flagd.resolver.grpc.cache.Cache;
import dev.openfeature.flagd.grpc.evaluation.Evaluation.EventStreamResponse;
+import dev.openfeature.sdk.ProviderEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -26,8 +19,7 @@ class EventStreamObserverTest {
@Nested
class StateChange {
- Cache cache;
- List states;
+ List states;
EventStreamObserver stream;
Runnable reconnect;
Object sync;
@@ -35,10 +27,10 @@ class StateChange {
@BeforeEach
void setUp() {
states = new ArrayList<>();
- cache = mock(Cache.class);
reconnect = mock(Runnable.class);
- when(cache.getEnabled()).thenReturn(true);
- stream = new EventStreamObserver(cache, (state, changed) -> states.add(state));
+ stream = new EventStreamObserver(
+ (state) -> states.add(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED),
+ (state) -> states.add(state.getEvent()));
}
@Test
@@ -50,42 +42,7 @@ public void change() {
when(flagData.getFieldsMap()).thenReturn(new HashMap<>());
stream.onNext(resp);
// we notify that we are ready
- assertEquals(1, states.size());
- assertTrue(states.get(0));
- // we flush the cache
- verify(cache, atLeast(1)).clear();
- }
-
- @Test
- public void cacheBustingForKnownKeys() {
- final String key1 = "myKey1";
- final String key2 = "myKey2";
-
- EventStreamResponse resp = mock(EventStreamResponse.class);
- Struct flagData = mock(Struct.class);
- Value flagsValue = mock(Value.class);
- Struct flagsStruct = mock(Struct.class);
- HashMap fields = new HashMap<>();
- fields.put(Constants.FLAGS_KEY, flagsValue);
- HashMap flags = new HashMap<>();
- flags.put(key1, null);
- flags.put(key2, null);
-
- when(resp.getType()).thenReturn("configuration_change");
- when(resp.getData()).thenReturn(flagData);
- when(flagData.getFieldsMap()).thenReturn(fields);
- when(flagsValue.getStructValue()).thenReturn(flagsStruct);
- when(flagsStruct.getFieldsMap()).thenReturn(flags);
-
- stream.onNext(resp);
- // we notify that the configuration changed
- assertEquals(1, states.size());
- assertTrue(states.get(0));
- // we did NOT flush the whole cache
- verify(cache, atMost(0)).clear();
- // we only clean the two keys
- verify(cache, times(1)).remove(eq(key1));
- verify(cache, times(1)).remove(eq(key2));
+ assertThat(states).hasSize(1).contains(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED);
}
}
}
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 90295ef10..141f4a305 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
@@ -20,7 +20,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.common.FlagdProviderEvent;
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;
@@ -88,7 +88,7 @@ void eventHandling() throws Throwable {
InProcessResolver inProcessResolver = getInProcessResolverWith(
new MockStorage(new HashMap<>(), sender),
connectionEvent -> receiver.offer(new StorageStateChange(
- connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR,
+ connectionEvent.isDisconnected() ? StorageState.ERROR : StorageState.OK,
connectionEvent.getFlagsChanged(),
connectionEvent.getSyncMetadata())));
@@ -517,25 +517,25 @@ void flagSetMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception {
private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage)
throws NoSuchFieldException, IllegalAccessException {
- final InProcessResolver resolver = new InProcessResolver(options, () -> true, connectionEvent -> {});
+ final InProcessResolver resolver = new InProcessResolver(options, connectionEvent -> {});
return injectFlagStore(resolver, storage);
}
private InProcessResolver getInProcessResolverWith(
- final MockStorage storage, final Consumer onConnectionEvent)
+ final MockStorage storage, final Consumer onConnectionEvent)
throws NoSuchFieldException, IllegalAccessException {
final InProcessResolver resolver =
- new InProcessResolver(FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent);
+ new InProcessResolver(FlagdOptions.builder().deadline(1000).build(), onConnectionEvent);
return injectFlagStore(resolver, storage);
}
private InProcessResolver getInProcessResolverWith(
- final MockStorage storage, final Consumer onConnectionEvent, String selector)
+ final MockStorage storage, final Consumer onConnectionEvent, String selector)
throws NoSuchFieldException, IllegalAccessException {
final InProcessResolver resolver = new InProcessResolver(
- FlagdOptions.builder().selector(selector).deadline(1000).build(), () -> true, onConnectionEvent);
+ FlagdOptions.builder().selector(selector).deadline(1000).build(), onConnectionEvent);
return injectFlagStore(resolver, storage);
}
diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness
index 8931c8645..fc7867922 160000
--- a/providers/flagd/test-harness
+++ b/providers/flagd/test-harness
@@ -1 +1 @@
-Subproject commit 8931c8645b8600e251d5e3ebbad42dff8ce4c78e
+Subproject commit fc786792273b7984911dc3bcb7b47489f261ba57