From d2b3cd42f51b1ea4a3067d94a73d5ef1fe53713e Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Sat, 28 Dec 2024 17:27:19 +0100 Subject: [PATCH] test(flagd): rework e2e tests to new format Signed-off-by: Simon Schrottner --- pom.xml | 7 + providers/flagd/pom.xml | 94 --- .../providers/flagd/e2e/ContainerConfig.java | 97 ---- .../providers/flagd/e2e/FlagdContainer.java | 76 +++ .../flagd/e2e/RunConfigCucumberTest.java | 24 - .../e2e/RunFlagdInProcessCucumberTest.java | 27 - .../RunFlagdInProcessEnvoyCucumberTest.java | 27 - ...unFlagdInProcessReconnectCucumberTest.java | 25 - .../e2e/RunFlagdInProcessSSLCucumberTest.java | 27 - .../flagd/e2e/RunFlagdRpcCucumberTest.java | 29 - .../flagd/e2e/RunFlagdRpcSSLCucumberTest.java | 26 - .../providers/flagd/e2e/RunInProcessTest.java | 37 ++ ...nnectCucumberTest.java => RunRpcTest.java} | 21 +- .../contrib/providers/flagd/e2e/State.java | 29 + .../e2e/process/core/FlagdInProcessSetup.java | 45 -- .../envoy/FlagdInProcessEnvoySetup.java | 45 -- .../process/FlagdInProcessSetup.java | 48 -- .../e2e/reconnect/rpc/FlagdRpcSetup.java | 52 -- .../e2e/reconnect/steps/StepDefinitions.java | 128 ---- .../flagd/e2e/rpc/FlagdRpcSetup.java | 43 -- .../e2e/ssl/process/FlagdInProcessSetup.java | 57 -- .../flagd/e2e/ssl/rpc/FlagdRpcSetup.java | 57 -- .../flagd/e2e/steps/AbstractSteps.java | 10 + .../e2e/steps/{config => }/ConfigSteps.java | 55 +- .../flagd/e2e/steps/ContextSteps.java | 43 ++ .../EnvironmentVariableUtils.java | 2 +- .../providers/flagd/e2e/steps/Event.java | 13 + .../providers/flagd/e2e/steps/EventSteps.java | 63 ++ .../providers/flagd/e2e/steps/FlagSteps.java | 86 +++ .../flagd/e2e/steps/ProviderSteps.java | 114 ++++ .../flagd/e2e/steps/ProviderType.java | 7 + .../flagd/e2e/steps/StepDefinitions.java | 547 ------------------ .../providers/flagd/e2e/steps/Utils.java | 39 ++ .../src/test/resources/features/.gitignore | 1 - .../src/test/resources/features/.gitkeep | 0 providers/flagd/test-harness | 2 +- 36 files changed, 557 insertions(+), 1446 deletions(-) delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/FlagdContainer.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessReconnectCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcCucumberTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/{RunFlagdRpcReconnectCucumberTest.java => RunRpcTest.java} (51%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/envoy/FlagdInProcessEnvoySetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/rpc/FlagdRpcSetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/AbstractSteps.java rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/{config => }/ConfigSteps.java (66%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/{config => }/EnvironmentVariableUtils.java (98%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Event.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderType.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/StepDefinitions.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java delete mode 100644 providers/flagd/src/test/resources/features/.gitignore delete mode 100644 providers/flagd/src/test/resources/features/.gitkeep diff --git a/pom.xml b/pom.xml index 1129f01b3..b54f22fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -213,6 +213,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 e1daa6d4b..bb5e49ff7 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -287,100 +287,6 @@ - - copy-gherkin-evaluation.feature - validate - - exec - - - - - cp - - spec/specification/assets/gherkin/evaluation.feature - src/test/resources/features/ - - - - - copy-gherkin-flagd-json-evaluator.feature - validate - - exec - - - - - cp - - test-harness/gherkin/flagd-json-evaluator.feature - src/test/resources/features/ - - - - - copy-gherkin-flagd.feature - validate - - exec - - - - - cp - - test-harness/gherkin/flagd.feature - src/test/resources/features/ - - - - - copy-gherkin-flagd-rpc-caching.feature - validate - - exec - - - cp - - test-harness/gherkin/flagd-rpc-caching.feature - src/test/resources/features/ - - - - - copy-gherkin-config.feature - validate - - exec - - - - - cp - - test-harness/gherkin/config.feature - src/test/resources/features/ - - - - - copy-gherkin-flagd-reconnect.feature - validate - - exec - - - - - cp - - test-harness/gherkin/flagd-reconnect.feature - src/test/resources/features/ - - - 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 705802ebf..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ContainerConfig.java +++ /dev/null @@ -1,97 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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; - -import java.io.File; -import java.nio.file.Files; -import java.util.List; - -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..8cfe2efce --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/FlagdContainer.java @@ -0,0 +1,76 @@ +package dev.openfeature.contrib.providers.flagd.e2e; + +import dev.openfeature.contrib.providers.flagd.Config; +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.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +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); + } + } + + public FlagdContainer() { + this(""); + } + + public FlagdContainer(String feature) { + super(generateContainerName(feature)); + this.addExposedPorts(8013, 8014, 8015, 8016); + } + + public int getPort(Config.Resolver resolver) { + waitUntilContainerStarted(); + switch (resolver) { + case RPC: + return this.getMappedPort(8013); + case IN_PROCESS: + return this.getMappedPort(8015); + default: + throw new IllegalArgumentException("Unsupported resolver: " + resolver); + } + } + + + /** + * @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 deleted file mode 100644 index a09c17e54..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunConfigCucumberTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; - -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; - -/** - * 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") -@SelectClasspathResource("features/config.feature") -@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") -@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps.config") -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 0930a3710..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessCucumberTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; - -/** - * 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") -@SelectClasspathResource("features/evaluation.feature") -@SelectClasspathResource("features/flagd-json-evaluator.feature") -@SelectClasspathResource("features/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 2771f3546..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessEnvoyCucumberTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; - -/** - * 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") -@SelectClasspathResource("features/evaluation.feature") -@SelectClasspathResource("features/flagd-json-evaluator.feature") -@SelectClasspathResource("features/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 97fe2d355..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessReconnectCucumberTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; - -/** - * Class for running the reconnection tests for the in-process provider - */ -@Order(value = Integer.MAX_VALUE) -@Suite -@IncludeEngines("cucumber") -@SelectClasspathResource("features/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 212eeb598..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -import org.apache.logging.log4j.core.config.Order; -import org.junit.jupiter.api.Disabled; -import org.junit.platform.suite.api.ConfigurationParameter; -import org.junit.platform.suite.api.IncludeEngines; -import org.junit.platform.suite.api.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; - -/** - * Class for running the reconnection tests for the RPC provider - */ -@Order(value = Integer.MAX_VALUE) -@Suite(failIfNoTests = false) -@IncludeEngines("cucumber") -//@SelectClasspathResource("features/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 8f8bad579..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 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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; - -/** - * 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") -@SelectClasspathResource("features/evaluation.feature") -@SelectClasspathResource("features/flagd-json-evaluator.feature") -@SelectClasspathResource("features/flagd.feature") -@SelectClasspathResource("features/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/RunFlagdRpcSSLCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java deleted file mode 100644 index a971f3e05..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e; - -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.SelectClasspathResource; -import org.junit.platform.suite.api.Suite; -import org.testcontainers.junit.jupiter.Testcontainers; - -import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; - -/** - * Class for running the reconnection tests for the RPC provider - */ -@Order(value = Integer.MAX_VALUE) -@Suite -@IncludeEngines("cucumber") -@SelectClasspathResource("features/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..9f93c6878 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -0,0 +1,37 @@ +package dev.openfeature.contrib.providers.flagd.e2e; + +import dev.openfeature.contrib.providers.flagd.Config; +import io.cucumber.junit.platform.engine.Constants; +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.IncludeEngines; +import org.junit.platform.suite.api.SelectDirectories; +import org.junit.platform.suite.api.Suite; +import org.testcontainers.junit.jupiter.Testcontainers; + +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; + +/** + * Class for running the reconnection tests for the RPC provider + */ +@Order(value = Integer.MAX_VALUE) +@Suite +@IncludeEngines("cucumber") +@SelectDirectories("test-harness/gherkin") +@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") +@ConfigurationParameter(key = Constants.FILTER_TAGS_PROPERTY_NAME, value = "@in-process and not @customCert and not @unixsocket") + +@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/RunFlagdRpcReconnectCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java similarity index 51% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcReconnectCucumberTest.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java index fa226c1a6..325989323 100644 --- 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/RunRpcTest.java @@ -1,14 +1,18 @@ package dev.openfeature.contrib.providers.flagd.e2e; +import dev.openfeature.contrib.providers.flagd.Config; +import io.cucumber.junit.platform.engine.Constants; 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.IncludeEngines; -import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.SelectDirectories; import org.junit.platform.suite.api.Suite; import org.testcontainers.junit.jupiter.Testcontainers; -import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; 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; /** * Class for running the reconnection tests for the RPC provider @@ -16,11 +20,18 @@ @Order(value = Integer.MAX_VALUE) @Suite @IncludeEngines("cucumber") -@SelectClasspathResource("features/flagd-reconnect.feature") +@SelectDirectories("test-harness/gherkin") @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") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") +@ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") +@ConfigurationParameter(key = Constants.FILTER_TAGS_PROPERTY_NAME, value = "@rpc and not @unixsocket") @Testcontainers -public class RunFlagdRpcReconnectCucumberTest { +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..99126a583 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/State.java @@ -0,0 +1,29 @@ +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.ArrayList; +import java.util.Optional; + +public class State { + public ProviderType providerType; + public Client client; + public ArrayList events = new ArrayList<>(); + 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 fa9bac43d..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/process/core/FlagdInProcessSetup.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e.process.core; - -import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig; -import io.cucumber.java.AfterAll; -import io.cucumber.java.Before; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.parallel.Isolated; - -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.steps.StepDefinitions; -import dev.openfeature.sdk.FeatureProvider; -import io.cucumber.java.BeforeAll; -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 347bff725..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(); - } -} \ No newline at end of file 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 2ff58eb34..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/process/FlagdInProcessSetup.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e.reconnect.process; - -import dev.openfeature.contrib.providers.flagd.e2e.ContainerConfig; -import io.cucumber.java.AfterAll; -import io.cucumber.java.Before; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.parallel.Isolated; - -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.reconnect.steps.StepDefinitions; -import dev.openfeature.sdk.FeatureProvider; -import io.cucumber.java.BeforeAll; -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 6601a5dd3..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/rpc/FlagdRpcSetup.java +++ /dev/null @@ -1,52 +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) - .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 1ba647e71..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/reconnect/steps/StepDefinitions.java +++ /dev/null @@ -1,128 +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 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; - -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; - -/** - * 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 bf9f4fc80..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/rpc/FlagdRpcSetup.java +++ /dev/null @@ -1,43 +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 ecd4e4b72..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java +++ /dev/null @@ -1,57 +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.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; -import org.testcontainers.utility.DockerImageName; - -import java.io.File; - -@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 1cdc518ef..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java +++ /dev/null @@ -1,57 +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.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; -import org.testcontainers.utility.DockerImageName; - -import java.io.File; - -@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..c712bf242 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/AbstractSteps.java @@ -0,0 +1,10 @@ +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 66% 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 659641ed8..b18e81758 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,8 +1,7 @@ -package dev.openfeature.contrib.providers.flagd.e2e.steps.config; +package dev.openfeature.contrib.providers.flagd.e2e.steps; 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; @@ -16,11 +15,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; -public class ConfigSteps { +public class ConfigSteps extends AbstractSteps { /** * Not all properties are correctly implemented, hence that we need to ignore them till this is fixed */ @@ -33,22 +31,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); @@ -62,12 +62,12 @@ public void we_have_an_option_of_type_with_value(String option, String type, Str 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); } @@ -80,35 +80,10 @@ 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); @@ -118,7 +93,7 @@ public void the_option_of_type_should_have_the_value(String option, String type, option = mapOptionNames(option); - assertThat(options).hasFieldOrPropertyWithValue(option, convert); + assertThat(state.options).hasFieldOrPropertyWithValue(option, convert); // Resetting env vars for ( 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..c8309a3b4 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java @@ -0,0 +1,43 @@ +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 org.junit.jupiter.api.parallel.Isolated; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@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 802c8781e..e0fa7aa9f 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..63df55ff6 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/EventSteps.java @@ -0,0 +1,63 @@ +package dev.openfeature.contrib.providers.flagd.e2e.steps; + +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 org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.parallel.Isolated; + +import java.util.ArrayList; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.awaitility.Awaitility.await; + +@Isolated() +public class EventSteps extends AbstractSteps { + + + public EventSteps(State state) { + super(state); + state.events = new ArrayList<>(); + } + + @Given("a {} event handler") + public void a_stale_event_handler(String eventType) { + state.client.on(mapEventType(eventType), eventDetails -> 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) { + eventHandlerShouldBeExecutedWithin(eventType, 10000); + } + + @Then("the {} event handler should have been executed") + public void eventHandlerShouldBeExecuted(String eventType) { + eventHandlerShouldBeExecutedWithin(eventType, 10000); + } + + @Then("the {} event handler should have been executed within {int}ms") + public void eventHandlerShouldBeExecutedWithin(String eventType, int ms) { + 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..4b193af3e --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/FlagSteps.java @@ -0,0 +1,86 @@ +package dev.openfeature.contrib.providers.flagd.e2e.steps; + +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; + +import static org.assertj.core.api.Assertions.assertThat; + + +@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 ClassNotFoundException { + 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() { + 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 {string}") + public void the_resolved_details_value_should_be(String value) throws ClassNotFoundException { + 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 was modified") + public void the_flag_was_modified() { + assertThat(state.lastEvent).isPresent().hasValueSatisfying((event) -> { + assertThat(event.type).isEqualTo("change"); + assertThat(event.details.getFlagsChanged()).contains(state.flag.name); + }); + } + + + 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..72378fb31 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ProviderSteps.java @@ -0,0 +1,114 @@ +package dev.openfeature.contrib.providers.flagd.e2e.steps; + +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.Client; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.OpenFeatureAPI; +import io.cucumber.java.AfterAll; +import io.cucumber.java.BeforeAll; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.When; +import org.junit.jupiter.api.parallel.Isolated; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +@Isolated() +public class ProviderSteps extends AbstractSteps { + + static Map containers = new HashMap<>(); + static String tmpdir; + + public ProviderSteps(State state) { + super(state); + } + + @BeforeAll + public static void beforeAll() throws IOException { + containers.put(ProviderType.DEFAULT, new FlagdContainer()); + containers.put(ProviderType.SSL, new FlagdContainer("ssl")); + tmpdir = Files.createTempDirectory("flagd").toFile().getAbsolutePath(); + //containers.put(ProviderType.SOCKET, new FlagdContainer("socket").withFileSystemBind(tmpdir, "/tmp", BindMode.READ_WRITE)); + + containers.forEach((name, container) -> container.start()); + } + + @AfterAll + public static void afterAll() { + containers.forEach((name, container) -> container.stop()); + } + + + @Given("a {} flagd provider") + public void setupProvider(String providerType) { + state.builder + .deadline(500) + .keepAlive(0) + .maxEventStreamRetries(2); + boolean wait = true; + switch (providerType) { + case "unavailable": + this.state.providerType = ProviderType.SOCKET; + state.builder.port(9999); + wait = false; + break; + case "socket": + this.state.providerType = ProviderType.SOCKET; + state.builder.socketPath(tmpdir); + 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(getContainer().getPort(State.resolverType)) + .tls(true) + .certPath(absolutePath); + break; + + default: + this.state.providerType = ProviderType.DEFAULT; + state.builder.port(getContainer().getPort(State.resolverType)); + 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) { + FlagdContainer container = getContainer(); + + TimerTask task = new TimerTask() { + public void run() { + container.start(); + } + }; + Timer timer = new Timer("Timer"); + + timer.schedule(task, seconds * 1000L); + } + + private FlagdContainer getContainer() { + return containers.getOrDefault(state.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 73d4de87d..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/StepDefinitions.java +++ /dev/null @@ -1,547 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.e2e.steps; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -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; - -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; - -/** - * 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..ea511158e --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java @@ -0,0 +1,39 @@ +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 java.util.Objects; + +public final class Utils { + + private Utils() {} + + public static 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 "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(); + } + throw new RuntimeException("Unknown config type: " + type); + } +} diff --git a/providers/flagd/src/test/resources/features/.gitignore b/providers/flagd/src/test/resources/features/.gitignore deleted file mode 100644 index d2092accf..000000000 --- a/providers/flagd/src/test/resources/features/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.feature \ No newline at end of file diff --git a/providers/flagd/src/test/resources/features/.gitkeep b/providers/flagd/src/test/resources/features/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness index fd66a39e1..ed9b0a908 160000 --- a/providers/flagd/test-harness +++ b/providers/flagd/test-harness @@ -1 +1 @@ -Subproject commit fd66a39e1192409f60cb388d443f821364ee9af4 +Subproject commit ed9b0a908faa538b64519aaf78399721e78230fa