diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index c37478aae..8230ce59c 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -9,6 +9,12 @@ permissions: jobs: build: runs-on: ubuntu-latest + services: + flagd: + image: ghcr.io/open-feature/flagd-testbed:latest + ports: + - 8013:8013 + steps: - name: Check out the code uses: actions/checkout@v3 @@ -28,7 +34,7 @@ jobs: ${{ runner.os }}-maven- - name: Build with Maven - run: mvn --batch-mode --update-snapshots verify + run: mvn --batch-mode --update-snapshots verify -P integration-test - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5893173a6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-harness"] + path = test-harness + url = https://github.com/open-feature/test-harness diff --git a/pom.xml b/pom.xml index 8b4e4e408..0f6784462 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,8 @@ 1.8 ${maven.compiler.source} 5.8.1 + + **/integration/*.java OpenFeature Java SDK @@ -46,7 +48,6 @@ provided - com.github.spotbugs @@ -54,7 +55,6 @@ 4.7.1 compile - org.slf4j slf4j-api @@ -68,28 +68,24 @@ 4.6.1 test - uk.org.lidalia slf4j-test 1.2.0 test - org.assertj assertj-core 3.23.1 test - org.junit.jupiter junit-jupiter ${junit.jupiter.version} test - org.junit.jupiter junit-jupiter-engine @@ -111,11 +107,51 @@ org.junit.platform junit-platform-suite - 1.8.1 + 1.9.0 + test + + + io.cucumber + cucumber-java + test + + + io.cucumber + cucumber-junit-platform-engine + test + + + com.google.guava + guava + 31.1-jre + test + + + dev.openfeature.contrib.providers + flagd + 0.3.2 test + + + + io.cucumber + cucumber-bom + 7.5.0 + pom + import + + + org.junit + junit-bom + 5.9.0 + pom + import + + + @@ -137,6 +173,9 @@ org.junit* + com.google.guava* + io.cucumber* + org.junit* com.google.code.findbugs* com.github.spotbugs* uk.org.lidalia:lidalia-slf4j-ext:* @@ -155,6 +194,10 @@ ${surefireArgLine} + + + ${testExclusions} + @@ -372,6 +415,61 @@ + + + + integration-test + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + update-test-harness-submodule + validate + + exec + + + + git + + submodule + update + --init + --recursive + + + + + copy-gherkin-tests + validate + + exec + + + + cp + + test-harness/features/evaluation.feature + src/test/resources/features/ + + + + + + + + + + ossrh diff --git a/src/test/java/dev/openfeature/javasdk/integration/RunCucumberTest.java b/src/test/java/dev/openfeature/javasdk/integration/RunCucumberTest.java new file mode 100644 index 000000000..65c86c292 --- /dev/null +++ b/src/test/java/dev/openfeature/javasdk/integration/RunCucumberTest.java @@ -0,0 +1,16 @@ +package dev.openfeature.javasdk.integration; + +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.PLUGIN_PROPERTY_NAME; + +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("features") +@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") +public class RunCucumberTest { + +} diff --git a/src/test/java/dev/openfeature/javasdk/integration/StepDefinitions.java b/src/test/java/dev/openfeature/javasdk/integration/StepDefinitions.java new file mode 100644 index 000000000..d4048b877 --- /dev/null +++ b/src/test/java/dev/openfeature/javasdk/integration/StepDefinitions.java @@ -0,0 +1,282 @@ +package dev.openfeature.javasdk.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import dev.openfeature.contrib.providers.flagd.FlagdProvider; +import dev.openfeature.javasdk.Client; +import dev.openfeature.javasdk.ErrorCode; +import dev.openfeature.javasdk.EvaluationContext; +import dev.openfeature.javasdk.FlagEvaluationDetails; +import dev.openfeature.javasdk.OpenFeatureAPI; +import dev.openfeature.javasdk.Reason; +import dev.openfeature.javasdk.Structure; +import dev.openfeature.javasdk.Value; +import io.cucumber.java.BeforeAll; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; + +public class StepDefinitions { + + private static Client client; + private boolean booleanFlagValue; + private String stringFlagValue; + private int intFlagValue; + private double doubleFlagValue; + private Value objectFlagValue; + + private FlagEvaluationDetails booleanFlagDetails; + private FlagEvaluationDetails stringFlagDetails; + private FlagEvaluationDetails intFlagDetails; + private FlagEvaluationDetails doubleFlagDetails; + 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; + + @BeforeAll() + public static void setup() { + OpenFeatureAPI.getInstance().setProvider(new FlagdProvider()); + client = OpenFeatureAPI.getInstance().getClient(); + } + + /* + * 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.booleanFlagValue = client.getBooleanValue(flagKey, Boolean.valueOf(defaultValue)); + } + + @Then("the resolved boolean value should be {string}") + public void the_resolved_boolean_value_should_be_true(String expected) { + assertEquals(Boolean.valueOf(expected), this.booleanFlagValue); + } + + // 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.stringFlagValue = client.getStringValue(flagKey, defaultValue); + } + + @Then("the resolved string value should be {string}") + public void the_resolved_string_value_should_be(String expected) { + assertEquals(expected, this.stringFlagValue); + } + + // 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.intFlagValue = client.getIntegerValue(flagKey, defaultValue); + } + + @Then("the resolved integer value should be {int}") + public void the_resolved_integer_value_should_be(int expected) { + assertEquals(expected, this.intFlagValue); + } + + // 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.doubleFlagValue = client.getDoubleValue(flagKey, defaultValue); + } + + @Then("the resolved float value should be {double}") + public void the_resolved_float_value_should_be(double expected) { + assertEquals(expected, this.doubleFlagValue); + } + + // 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.objectFlagValue = client.getObjectValue(flagKey, new Value()); + } + + @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) { + Structure structure = this.objectFlagValue.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.booleanFlagDetails = client.getBooleanDetails(flagKey, 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) { + assertEquals(Boolean.valueOf(expectedValue), booleanFlagDetails.getValue()); + assertEquals(expectedVariant, booleanFlagDetails.getVariant()); + assertEquals(Reason.valueOf(expectedReason), booleanFlagDetails.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.stringFlagDetails = client.getStringDetails(flagKey, defaultValue); + } + + @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) { + assertEquals(expectedValue, this.stringFlagDetails.getValue()); + assertEquals(expectedVariant, this.stringFlagDetails.getVariant()); + assertEquals(Reason.valueOf(expectedReason), this.stringFlagDetails.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.intFlagDetails = client.getIntegerDetails(flagKey, 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) { + assertEquals(expectedValue, this.intFlagDetails.getValue()); + assertEquals(expectedVariant, this.intFlagDetails.getVariant()); + assertEquals(Reason.valueOf(expectedReason), this.intFlagDetails.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.doubleFlagDetails = client.getDoubleDetails(flagKey, 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) { + assertEquals(expectedValue, this.doubleFlagDetails.getValue()); + assertEquals(expectedVariant, this.doubleFlagDetails.getVariant()); + assertEquals(Reason.valueOf(expectedReason), this.doubleFlagDetails.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.objectFlagDetails = client.getObjectDetails(flagKey, 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) { + 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(Reason.valueOf(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) { + this.context = new EvaluationContext() + .add(field1, value1) + .add(field2, value2) + .add(field3, value3) + .add(field4, Boolean.valueOf(value4)); + } + + @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 EvaluationContext()); + 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("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 be FLAG_NOT_FOUND") + public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found() { + assertEquals(Reason.ERROR, notFoundDetails.getReason()); + assertTrue(notFoundDetails.getErrorCode().contains(ErrorCode.FLAG_NOT_FOUND.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("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 be TYPE_MISMATCH") + public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch() { + assertEquals(Reason.ERROR, typeErrorDetails.getReason()); + assertTrue(typeErrorDetails.getErrorCode().contains(ErrorCode.TYPE_MISMATCH.toString())); + } + +} diff --git a/src/test/resources/features/.gitignore b/src/test/resources/features/.gitignore new file mode 100644 index 000000000..ce4de1a72 --- /dev/null +++ b/src/test/resources/features/.gitignore @@ -0,0 +1 @@ +evaluation.feature \ No newline at end of file diff --git a/src/test/resources/features/.gitkeep b/src/test/resources/features/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test-harness b/test-harness new file mode 160000 index 000000000..cf1e121bd --- /dev/null +++ b/test-harness @@ -0,0 +1 @@ +Subproject commit cf1e121bdab52f8d9bc3af880646e5d822eff6d7