Skip to content

Commit

Permalink
feat(flagd): ssl e2e tests
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
  • Loading branch information
aepfli committed Dec 18, 2024
1 parent d5110e3 commit f970220
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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<String> 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);

Expand All @@ -51,20 +53,18 @@ 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() {
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);
String container = generateContainerName("flagd", unstable ? "unstable" : "");
return new GenericContainer(DockerImageName.parse(container))
.withExposedPorts(8013);
}
Expand All @@ -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";
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 {

}

Original file line number Diff line number Diff line change
@@ -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 {

}

Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,42 @@
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 {
public static final List<String> IGNORED_FOR_NOW = new ArrayList<String>() {
{
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":
Expand All @@ -39,11 +52,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);
Expand All @@ -52,7 +70,7 @@ public void we_have_an_option_of_type_with_value(String option, String type, Str

Map<String, String> 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);
Expand Down Expand Up @@ -89,15 +107,38 @@ 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<String, String> envVar : envVarsSet.entrySet()) {
for (
Map.Entry<String, String> envVar : envVarsSet.entrySet()) {
if (envVar.getValue() == null) {
EnvironmentVariableUtils.clear(envVar.getKey());
} else {
EnvironmentVariableUtils.set(envVar.getKey(), envVar.getValue());
}
}
}

private static String mapOptionNames(String option) {
Map<String, String> 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;
}
}
Loading

0 comments on commit f970220

Please sign in to comment.