diff --git a/.gitmodules b/.gitmodules index 922e776fd..86ed9ea04 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,7 @@ [submodule "providers/flagd/test-harness"] path = providers/flagd/test-harness url = https://github.com/open-feature/test-harness.git + branch = v0.5.19 [submodule "providers/flagd/spec"] path = providers/flagd/spec url = https://github.com/open-feature/spec.git 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 index 0d51ef9e5..705802ebf 100644 --- 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 @@ -1,44 +1,46 @@ 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.IOException; -import java.util.Properties; +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 { - Properties properties = new Properties(); + String path = "test-harness/version.txt"; + File file = new File(path); try { - properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("flagdTestbed.properties")); - version = properties.getProperty("version"); - } catch (IOException e) { + 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() { + 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 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); + String container = generateContainerName("flagd", unstable ? "unstable" : ""); GenericContainer genericContainer = new GenericContainer(DockerImageName.parse(container)) .withExposedPorts(8015); @@ -51,7 +53,6 @@ public static GenericContainer sync(boolean unstable, boolean addNetwork) { } /** - * * @return a {@link org.testcontainers.containers.GenericContainer} instance of a stable flagd server with the port 8013 exposed */ public static GenericContainer flagd() { @@ -59,12 +60,11 @@ public static GenericContainer flagd() { } /** - * * @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); + String container = generateContainerName("flagd", unstable ? "unstable" : ""); return new GenericContainer(DockerImageName.parse(container)) .withExposedPorts(8013); } @@ -73,7 +73,6 @@ public static GenericContainer flagd(boolean unstable) { /** * @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"; @@ -85,14 +84,14 @@ public static GenericContainer envoy() { .withNetworkAliases("envoy"); } - private static @NotNull String generateContainerName(String type, boolean unstable) { + public static @NotNull String generateContainerName(String type, String addition) { String container = "ghcr.io/open-feature/"; container += type; container += "-testbed"; - if (unstable) { - container += "-unstable"; + if (!Strings.isBlank(addition)) { + container += "-" + addition; } - container += ":" + version; + container += ":v" + version; return container; } } 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 new file mode 100644 index 000000000..212eeb598 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdInProcessSSLCucumberTest.java @@ -0,0 +1,27 @@ +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/RunFlagdRpcSSLCucumberTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java new file mode 100644 index 000000000..a971f3e05 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFlagdRpcSSLCucumberTest.java @@ -0,0 +1,26 @@ +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/ssl/process/FlagdInProcessSetup.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java new file mode 100644 index 000000000..ecd4e4b72 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/process/FlagdInProcessSetup.java @@ -0,0 +1,57 @@ +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 new file mode 100644 index 000000000..1cdc518ef --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/ssl/rpc/FlagdRpcSetup.java @@ -0,0 +1,57 @@ +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/config/ConfigSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java index 0e2cb1414..659641ed8 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/config/ConfigSteps.java @@ -3,29 +3,45 @@ 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 io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; public class ConfigSteps { + /** + * Not all properties are correctly implemented, hence that we need to ignore them till this is fixed + */ + public static final List IGNORED_FOR_NOW = new ArrayList() { + { + add("offlinePollIntervalMs"); + add("retryGraceAttempts"); + add("retryBackoffMaxMs"); + } + }; + private static final Logger LOG = LoggerFactory.getLogger(ConfigSteps.class); FlagdOptions.FlagdOptionsBuilder builder = FlagdOptions.builder(); FlagdOptions options; - @When("we initialize a config") + @When("a config was initialized") public void we_initialize_a_config() { options = builder.build(); } - @When("we initialize a config for {string}") + @When("a config was initialized for {string}") public void we_initialize_a_config_for(String string) { switch (string.toLowerCase()) { case "in-process": @@ -39,11 +55,16 @@ public void we_initialize_a_config_for(String string) { } } - @When("we have an option {string} of type {string} with value {string}") + @Given("an option {string} of type {string} with value {string}") public void we_have_an_option_of_type_with_value(String option, String type, String value) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + if(IGNORED_FOR_NOW.contains(option)) { + LOG.error("option '{}' is not supported", option); + return; + } + Object converted = convert(value, type); Method method = Arrays.stream(builder.getClass().getMethods()) - .filter(method1 -> method1.getName().equals(option)) + .filter(method1 -> method1.getName().equals(mapOptionNames(option))) .findFirst() .orElseThrow(RuntimeException::new); method.invoke(builder, converted); @@ -52,7 +73,7 @@ public void we_have_an_option_of_type_with_value(String option, String type, Str Map envVarsSet = new HashMap<>(); - @When("we have an environment variable {string} with value {string}") + @Given("an environment variable {string} with value {string}") public void we_have_an_environment_variable_with_value(String varName, String value) throws IllegalAccessException, NoSuchFieldException { String getenv = System.getenv(varName); envVarsSet.put(varName, getenv); @@ -89,10 +110,19 @@ private Object convert(String value, String type) throws ClassNotFoundException public void the_option_of_type_should_have_the_value(String option, String type, String value) throws Throwable { Object convert = convert(value, type); + if(IGNORED_FOR_NOW.contains(option)) { + LOG.error("option '{}' is not supported", option); + return; + } + + + option = mapOptionNames(option); + assertThat(options).hasFieldOrPropertyWithValue(option, convert); // Resetting env vars - for (Map.Entry envVar : envVarsSet.entrySet()) { + for ( + Map.Entry envVar : envVarsSet.entrySet()) { if (envVar.getValue() == null) { EnvironmentVariableUtils.clear(envVar.getKey()); } else { @@ -100,4 +130,18 @@ public void the_option_of_type_should_have_the_value(String option, String type, } } } + + private static String mapOptionNames(String option) { + Map propertyMapper = new HashMap<>(); + propertyMapper.put("resolver", "resolverType"); + propertyMapper.put("deadlineMs", "deadline"); + propertyMapper.put("keepAliveTime", "keepAlive"); + propertyMapper.put("retryBackoffMaxMs", "keepAlive"); + propertyMapper.put("cache", "cacheType"); + + if (propertyMapper.get(option) != null) { + option = propertyMapper.get(option); + } + return option; + } } diff --git a/providers/flagd/src/test/resources/flagdTestbed.properties b/providers/flagd/src/test/resources/flagdTestbed.properties deleted file mode 100644 index 2fe8b3c99..000000000 --- a/providers/flagd/src/test/resources/flagdTestbed.properties +++ /dev/null @@ -1,3 +0,0 @@ -# todo: properly configure renovate with a regex matcher to update this version -# renovate: datasource=docker packageName=docker versioning=docker -version=v0.5.15 diff --git a/providers/flagd/test-harness b/providers/flagd/test-harness index 536d4845c..fd66a39e1 160000 --- a/providers/flagd/test-harness +++ b/providers/flagd/test-harness @@ -1 +1 @@ -Subproject commit 536d4845c0fa4255e3e98b7ee382d0eb73f7b4c0 +Subproject commit fd66a39e1192409f60cb388d443f821364ee9af4