Skip to content

feat(flagd): Improve e2e coverage #1092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion providers/flagd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<dependencies>
<!-- we inherent dev.openfeature.javasdk and the test dependencies from the parent pom -->

<!-- pin protobuf-java to version used by io.grpc deps; update to 4.0 when io.grpc requires it -->
<dependency>
<groupId>com.google.protobuf</groupId>
Expand Down Expand Up @@ -335,6 +335,38 @@
</arguments>
</configuration>
</execution>
<execution>
<id>copy-gherkin-flagd-rpc-caching.feature</id>
<phase>validate</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<!-- copy the feature spec we want to test into resources so them can be easily loaded -->
<!-- run: cp test-harness/features/flagd.feature src/test/resources/features/ -->
<executable>cp</executable>
<arguments>
<argument>test-harness/gherkin/flagd-rpc-caching.feature</argument>
<argument>src/test/resources/features/</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>copy-gherkin-config.feature</id>
<phase>validate</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<!-- copy the feature spec we want to test into resources so them can be easily loaded -->
<!-- run: cp test-harness/features/flagd.feature src/test/resources/features/ -->
<executable>cp</executable>
<arguments>
<argument>test-harness/gherkin/config.feature</argument>
<argument>src/test/resources/features/</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>copy-gherkin-flagd-reconnect.feature</id>
<phase>validate</phase>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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 {


}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
@Order(value = Integer.MAX_VALUE)
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features/evaluation.feature")
@SelectClasspathResource("features/flagd-json-evaluator.feature")
@SelectClasspathResource("features/flagd.feature")
//@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ public static void setup() {
FlagdRpcSetup.provider = new FlagdProvider(FlagdOptions.builder()
.resolverType(Config.Resolver.RPC)
.port(flagdContainer.getFirstMappedPort())
.deadline(1000)
.cacheType(CacheType.DISABLED.getValue())
.deadline(500)
.build());
StepDefinitions.setProvider(provider);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public class StepDefinitions {
* 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) {
Expand Down Expand Up @@ -208,6 +208,12 @@ public void a_string_flag_with_key_is_evaluated_with_details_and_default_value(S
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) {
Expand Down Expand Up @@ -347,6 +353,7 @@ public void a_string_flag_with_key_is_evaluated_as_an_integer_with_details_and_a
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());
Expand Down Expand Up @@ -525,7 +532,7 @@ 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);
Expand All @@ -537,4 +544,4 @@ public void the_returned_reason_should_be(String reason) {
this.context);
assertEquals(reason, details.getReason());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dev.openfeature.contrib.providers.flagd.e2e.steps.config;

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.Then;
import io.cucumber.java.en.When;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static org.assertj.core.api.Assertions.assertThat;

public class ConfigSteps {

FlagdOptions.FlagdOptionsBuilder builder = FlagdOptions.builder();
FlagdOptions options;

@When("we initialize a config")
public void we_initialize_a_config() {
options = builder.build();
}

@When("we initialize a config 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();
break;
case "rpc":
options = builder.resolverType(Config.Resolver.RPC).build();
break;
default:
throw new RuntimeException("Unknown resolver type: " + string);
}
}

@When("we have 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 {
Object converted = convert(value, type);
Method method = Arrays.stream(builder.getClass().getMethods())
.filter(method1 -> method1.getName().equals(option))
.findFirst()
.orElseThrow(RuntimeException::new);
method.invoke(builder, converted);
}


Map<String, String> envVarsSet = new HashMap<>();

@When("we have an environment variable {string} with value {string}")
public void we_have_an_environment_variable_with_value(String string, String string2) throws IllegalAccessException, NoSuchFieldException {
String getenv = System.getenv(string);
envVarsSet.put(string, getenv);
EnvironmentVariableUtils.set(string, string2);
}

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);

assertThat(options).hasFieldOrPropertyWithValue(option, convert);

// Resetting env vars
for (Map.Entry<String, String> envVar : envVarsSet.entrySet()) {
if (envVar.getValue() == null) {
EnvironmentVariableUtils.clear(envVar.getKey());
} else {
EnvironmentVariableUtils.set(envVar.getKey(), envVar.getValue());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package dev.openfeature.contrib.providers.flagd.e2e.steps.config;

/*
* Copy of JUnit Pioneer's EnvironmentVariable Utils
* https://github.com/junit-pioneer/junit-pioneer/blob/main/src/main/java/org/junitpioneer/jupiter/EnvironmentVariableUtils.java
*/

import java.lang.reflect.Field;
import java.util.Map;
import java.util.function.Consumer;

import org.junit.platform.commons.PreconditionViolationException;

/**
* This class modifies the internals of the environment variables map with reflection.
* Warning: If your {@link SecurityManager} does not allow modifications, it fails.
*/
class EnvironmentVariableUtils {

private EnvironmentVariableUtils() {
// private constructor to prevent instantiation of utility class
}

/**
* Set a value of an environment variable.
*
* @param name of the environment variable
* @param value of the environment variable
*/
public static void set(String name, String value) {
modifyEnvironmentVariables(map -> map.put(name, value));
}

/**
* Clear an environment variable.
*
* @param name of the environment variable
*/
public static void clear(String name) {
modifyEnvironmentVariables(map -> map.remove(name));
}

private static void modifyEnvironmentVariables(Consumer<Map<String, String>> consumer) {
try {
setInProcessEnvironmentClass(consumer);
}
catch (ReflectiveOperationException ex) {
trySystemEnvClass(consumer, ex);
}
}

private static void trySystemEnvClass(Consumer<Map<String, String>> consumer,
ReflectiveOperationException processEnvironmentClassEx) {
try {
setInSystemEnvClass(consumer);
}
catch (ReflectiveOperationException ex) {
ex.addSuppressed(processEnvironmentClassEx);
throw new PreconditionViolationException("Could not modify environment variables", ex);
}
}

/*
* Works on Windows
*/
private static void setInProcessEnvironmentClass(Consumer<Map<String, String>> consumer)
throws ReflectiveOperationException {
Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
// The order of operations is critical here: On some operating systems, theEnvironment is present but
// theCaseInsensitiveEnvironment is not present. In such cases, this method must throw a
// ReflectiveOperationException without modifying theEnvironment. Otherwise, the contents of theEnvironment will
// be corrupted. For this reason, both fields are fetched by reflection before either field is modified.
Map<String, String> theEnvironment = getFieldValue(processEnvironmentClass, null, "theEnvironment");
Map<String, String> theCaseInsensitiveEnvironment = getFieldValue(processEnvironmentClass, null,
"theCaseInsensitiveEnvironment");
consumer.accept(theEnvironment);
consumer.accept(theCaseInsensitiveEnvironment);
}

/*
* Works on Linux and OSX
*/
private static void setInSystemEnvClass(Consumer<Map<String, String>> consumer)
throws ReflectiveOperationException {
Map<String, String> env = System.getenv(); //NOSONAR access required to implement the extension
consumer.accept(getFieldValue(env.getClass(), env, "m"));
}

@SuppressWarnings("unchecked")
private static Map<String, String> getFieldValue(Class<?> clazz, Object object, String name)
throws ReflectiveOperationException {
Field field = clazz.getDeclaredField(name);
try {
field.setAccessible(true); //NOSONAR illegal access required to implement the extension
}
catch (Exception ex) {
throw new PreconditionViolationException(
"Cannot access and modify JDK internals to modify environment variables. "
+ "Have a look at the documentation for possible solutions: "
+ "https://junit-pioneer.org/docs/environment-variables/#warnings-for-reflective-access",
ex);
}
return (Map<String, String>) field.get(object);
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# todo: properly configure renovate with a regex matcher to update this version
# renovate: datasource=docker packageName=docker versioning=docker
version=v0.5.13
version=v0.5.15
2 changes: 1 addition & 1 deletion providers/flagd/test-harness
Loading