diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 41469bce8ea..b6bb58ce621 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,21 @@ version: 2 updates: -- package-ecosystem: gomod - directory: "/" - schedule: - interval: weekly - time: "04:00" - open-pull-requests-limit: 30 - labels: - - "dependencies" - - "automerge" + - package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + time: "04:00" + open-pull-requests-limit: 30 + labels: + - "dependencies" + - "automerge" + + - package-ecosystem: maven + directory: "/openfeature/provider_tests/java-integration-tests" + schedule: + interval: daily + time: "04:00" + open-pull-requests-limit: 30 + labels: + - "dependencies" + - "automerge" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de05deaf0f3..ca283d847bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,24 @@ on: types: [ published ] jobs: + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: '^1.18.0' + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.8.2 + - run: make vendor + - run: make provider-tests goreleaser: + needs: integration-tests runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 0b14f5afc3d..f774bf80f44 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ tmp/ # Local dev files goff-proxy.yaml flags.yaml + diff --git a/Makefile b/Makefile index aa1e4c85585..3a0b3a98b37 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,9 @@ swagger: ## Build swagger documentation test: ## Run the tests of the project $(GOTEST) -v -race ./... +provider-tests: + ./openfeature/provider_tests/integration_tests.sh + coverage: ## Run the tests of the project and export the coverage $(GOTEST) -cover -covermode=count -coverprofile=coverage.cov.tmp ./... \ && cat coverage.cov.tmp | grep -v "/examples/" > coverage.cov diff --git a/openfeature/provider_tests/.gitignore b/openfeature/provider_tests/.gitignore new file mode 100644 index 00000000000..866192b5b7b --- /dev/null +++ b/openfeature/provider_tests/.gitignore @@ -0,0 +1,6 @@ +!flags.yaml +!goff-proxy.yaml + +# Java +*.class +target/ \ No newline at end of file diff --git a/openfeature/provider_tests/flags.yaml b/openfeature/provider_tests/flags.yaml new file mode 100644 index 00000000000..1e9601d2a24 --- /dev/null +++ b/openfeature/provider_tests/flags.yaml @@ -0,0 +1,134 @@ +bool_targeting_match: + variations: + Default: false + "False": false + "True": true + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 +disabled_bool: + variations: + Default: false + "False": false + "True": true + defaultRule: + percentage: + "False": 0 + "True": 100 + disable: true +disabled_float: + variations: + Default: 103.25 + "False": 101.25 + "True": 100.25 + defaultRule: + percentage: + "False": 0 + "True": 100 + disable: true +disabled_int: + variations: + Default: 103 + "False": 101 + "True": 100 + defaultRule: + percentage: + "False": 0 + "True": 100 + disable: true +disabled_interface: + variations: + Default: + test: default + "False": + test: "false" + "True": + test: test1 + test2: false + test3: 123.3 + test4: 1 + defaultRule: + percentage: + "False": 0 + "True": 100 + disable: true +disabled_string: + variations: + Default: CC0002 + "False": CC0001 + "True": CC0000 + defaultRule: + percentage: + "False": 0 + "True": 100 + disable: true +double_key: + variations: + Default: 103.25 + "False": 101.25 + "True": 100.25 + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 +integer_key: + variations: + Default: 103 + "False": 101 + "True": 100 + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 +object_key: + variations: + Default: + test: default + "False": + test: "false" + "True": + test: test1 + test2: false + test3: 123.3 + test4: 1 + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 +string_key: + variations: + Default: CC0002 + "False": CC0001 + "True": CC0000 + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 +string_key_with_version: + variations: + Default: CC0002 + "False": CC0001 + "True": CC0000 + targeting: + - query: email eq "john.doe@gofeatureflag.org" + variation: "True" + defaultRule: + percentage: + "False": 0 + "True": 100 diff --git a/openfeature/provider_tests/goff-proxy.yaml b/openfeature/provider_tests/goff-proxy.yaml new file mode 100644 index 00000000000..e3acd7fa614 --- /dev/null +++ b/openfeature/provider_tests/goff-proxy.yaml @@ -0,0 +1,9 @@ +listen: 1031 +pollingInterval: 1000 +startWithRetrieverError: false +retriever: + kind: file + path: ./openfeature/provider_tests/flags.yaml +exporter: + kind: log +enableSwagger: true diff --git a/openfeature/provider_tests/integration_tests.sh b/openfeature/provider_tests/integration_tests.sh new file mode 100755 index 00000000000..1700f910aed --- /dev/null +++ b/openfeature/provider_tests/integration_tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +make build-relayproxy +./out/bin/relayproxy --config $(pwd)/openfeature/provider_tests/goff-proxy.yaml & + +# Waiting for the relay proxy to be ready +NB_ITERATION=10 +while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:1031/health)" != "200" ]]; do + sleep 1 + NB_ITERATION=$((NB_ITERATION - 1)) + if [ ${NB_ITERATION} == "0" ]; then echo "ERROR: relay-proxy is not ready" && exit 123; fi +done + +# Launch java integration tests +mvn -f $(pwd)/openfeature/provider_tests/java-integration-tests/pom.xml test + +# Kill all process launched by the script (here the relay-proxy) +kill -KILL %1 \ No newline at end of file diff --git a/openfeature/provider_tests/java-integration-tests/pom.xml b/openfeature/provider_tests/java-integration-tests/pom.xml new file mode 100644 index 00000000000..21c0ebf335f --- /dev/null +++ b/openfeature/provider_tests/java-integration-tests/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + org.example + java-integration-tests + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + dev.openfeature + sdk + 1.3.0 + + + dev.openfeature.contrib.providers + go-feature-flag + 0.2.3 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + \ No newline at end of file diff --git a/openfeature/provider_tests/java-integration-tests/src/test/java/org/gofeatureflag/integrationtests/ProviderTests.java b/openfeature/provider_tests/java-integration-tests/src/test/java/org/gofeatureflag/integrationtests/ProviderTests.java new file mode 100644 index 00000000000..77e0455c852 --- /dev/null +++ b/openfeature/provider_tests/java-integration-tests/src/test/java/org/gofeatureflag/integrationtests/ProviderTests.java @@ -0,0 +1,305 @@ +package org.gofeatureflag.integrationtests; + + +import dev.openfeature.contrib.providers.gofeatureflag.GoFeatureFlagProvider; +import dev.openfeature.contrib.providers.gofeatureflag.GoFeatureFlagProviderOptions; +import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions; +import dev.openfeature.sdk.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ProviderTests { + private static final String relayProxyEndpoint = "http://localhost:1031"; + private EvaluationContext defaultEvaluationContext; + private Client goffClient; + + @BeforeEach + void init() throws InvalidOptions { + MutableContext userContext = new MutableContext() + .add("email", "john.doe@gofeatureflag.org") + .add("firstname", "john") + .add("lastname", "doe") + .add("anonymous", false) + .add("professional", true) + .add("rate", 3.14) + .add("age", 30) + .add("admin", true) + .add("labels", Arrays.asList(new Value("pro"), new Value("beta"))) + .add("company_info", new MutableStructure().add("name", "my_company").add("size", 120)); + userContext.setTargetingKey("d45e303a-38c2-11ed-a261-0242ac120002"); + defaultEvaluationContext = userContext; + + GoFeatureFlagProviderOptions options = GoFeatureFlagProviderOptions.builder().endpoint(relayProxyEndpoint).build(); + GoFeatureFlagProvider provider = new GoFeatureFlagProvider(options); + OpenFeatureAPI.getInstance().setProvider(provider); + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + goffClient = api.getClient(); + } + + + @DisplayName("bool: should resolve a valid boolean flag with TARGETING_MATCH reason") + @Test + void boolShouldResolveAValidBooleanFlagWithTargetingMatchReason() { + String flagKey = "bool_targeting_match"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.TARGETING_MATCH.toString()) + .value(true) + .variant("True") + .build(); + FlagEvaluationDetails got = goffClient.getBooleanDetails(flagKey, false, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("bool: should use boolean default value if the flag is disabled") + @Test + void boolShouldUseBooleanDefaultValueIfTheFlagIsDisabled() { + String flagKey = "disabled_bool"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.DISABLED.toString()) + .value(false) + .build(); + FlagEvaluationDetails got = goffClient.getBooleanDetails(flagKey, false, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("bool: should error if we expect a boolean and got another type") + @Test + void boolShouldErrorIfWeExpectABooleanAndGotAnotherType() { + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(false) + .errorCode(ErrorCode.TYPE_MISMATCH) + .errorMessage("Flag value string_key had unexpected type class java.lang.String, expected class java.lang.Boolean.") + .build(); + FlagEvaluationDetails got = goffClient.getBooleanDetails("string_key", false, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("bool: should error if flag does not exists") + @Test + void boolShouldErrorIfFlagDoesNotExists() { + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(false) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag does_not_exists was not found in your configuration") + .build(); + FlagEvaluationDetails got = goffClient.getBooleanDetails("does_not_exists", false, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("string: should resolve a valid string flag with TARGETING_MATCH reason") + @Test + void stringShouldResolveAValidStringFlagWithTargetingMatchReason() { + String flagKey = "string_key"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.TARGETING_MATCH.toString()) + .flagKey(flagKey) + .value("CC0000") + .variant("True") + .build(); + FlagEvaluationDetails got = goffClient.getStringDetails(flagKey, "default", defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("string: should use string default value if the flag is disabled") + @Test + void stringShouldUseStringDefaultValueIfTheFlagIsDisabled() { + String flagKey = "disabled_string"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.DISABLED.toString()) + .value("default") + .build(); + FlagEvaluationDetails got = goffClient.getStringDetails(flagKey, "default", defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("string: should error if we expect a string and got another type") + @Test + void stringShouldErrorIfWeExpectAStringAndGotAnotherType() { + String flagKey = "bool_targeting_match"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value("default") + .errorCode(ErrorCode.TYPE_MISMATCH) + .errorMessage("Flag value bool_targeting_match had unexpected type class java.lang.Boolean, expected class java.lang.String.") + .build(); + FlagEvaluationDetails got = goffClient.getStringDetails(flagKey, "default", defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("string: should error if flag does not exists") + @Test + void stringShouldErrorIfFlagDoesNotExists() { + String flagKey = "does_not_exists"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value("default") + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag does_not_exists was not found in your configuration") + .build(); + FlagEvaluationDetails got = goffClient.getStringDetails(flagKey, "default", defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("double: should resolve a valid string flag with TARGETING_MATCH reason") + @Test + void doubleShouldResolveAValidStringFlagWithTargetingMatchReason() { + String flagKey = "double_key"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.TARGETING_MATCH.toString()) + .flagKey(flagKey) + .value(100.25) + .variant("True") + .build(); + FlagEvaluationDetails got = goffClient.getDoubleDetails(flagKey, 123.45, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("double: should use string default value if the flag is disabled") + @Test + void doubleShouldUseStringDefaultValueIfTheFlagIsDisabled() { + String flagKey = "disabled_float"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.DISABLED.toString()) + .value(123.45) + .build(); + FlagEvaluationDetails got = goffClient.getDoubleDetails(flagKey, 123.45, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("double: should error if we expect a string and got another type") + @Test + void doubleShouldErrorIfWeExpectAStringAndGotAnotherType() { + String flagKey = "bool_targeting_match"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(123.45) + .errorCode(ErrorCode.TYPE_MISMATCH) + .errorMessage("Flag value bool_targeting_match had unexpected type class java.lang.Boolean, expected class java.lang.Double.") + .build(); + FlagEvaluationDetails got = goffClient.getDoubleDetails(flagKey, 123.45, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("double: should error if flag does not exists") + @Test + void doubleShouldErrorIfFlagDoesNotExists() { + String flagKey = "does_not_exists"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(123.45) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag does_not_exists was not found in your configuration") + .build(); + FlagEvaluationDetails got = goffClient.getDoubleDetails(flagKey, 123.45, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("int: should resolve a valid string flag with TARGETING_MATCH reason") + @Test + void intShouldResolveAValidStringFlagWithTargetingMatchReason() { + String flagKey = "integer_key"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.TARGETING_MATCH.toString()) + .flagKey(flagKey) + .value(100) + .variant("True") + .build(); + FlagEvaluationDetails got = goffClient.getIntegerDetails(flagKey, 123, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("int: should use string default value if the flag is disabled") + @Test + void intShouldUseStringDefaultValueIfTheFlagIsDisabled() { + String flagKey = "disabled_int"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.DISABLED.toString()) + .value(123) + .build(); + FlagEvaluationDetails got = goffClient.getIntegerDetails(flagKey, 123, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("int: should error if we expect a string and got another type") + @Test + void intShouldErrorIfWeExpectAStringAndGotAnotherType() { + String flagKey = "bool_targeting_match"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(123) + .errorCode(ErrorCode.TYPE_MISMATCH) + .errorMessage("Flag value bool_targeting_match had unexpected type class java.lang.Boolean, expected class java.lang.Integer.") + .build(); + FlagEvaluationDetails got = goffClient.getIntegerDetails(flagKey, 123, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("int: should error if flag does not exists") + @Test + void intShouldErrorIfFlagDoesNotExists() { + String flagKey = "does_not_exists"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .value(123) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag does_not_exists was not found in your configuration") + .build(); + FlagEvaluationDetails got = goffClient.getIntegerDetails(flagKey, 123, defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("object: should resolve a valid object flag with TARGETING_MATCH reason") + @Test + void objectShouldResolveAValidObjectFlagWithTargetingMatchReason() { + String flagKey = "object_key"; + Structure expectedValue = new MutableStructure().add("test4", 1).add("test2", false).add("test3", 123.3).add("test", "test1"); + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.TARGETING_MATCH.toString()) + .flagKey(flagKey) + .value(new Value(expectedValue)) + .variant("True") + .build(); + FlagEvaluationDetails got = goffClient.getObjectDetails(flagKey, new Value(123), defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("object: should use object default value if the flag is disabled") + @Test + void objectShouldUseStringDefaultValueIfTheFlagIsDisabled() { + String flagKey = "disabled_int"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .flagKey(flagKey) + .reason(Reason.DISABLED.toString()) + .value(new Value(123)) + .build(); + FlagEvaluationDetails got = goffClient.getObjectDetails(flagKey, new Value(123), defaultEvaluationContext); + assertEquals(expected, got); + } + + @DisplayName("object: should error if flag does not exists") + @Test + void objectShouldErrorIfFlagDoesNotExists() { + String flagKey = "does_not_exists"; + FlagEvaluationDetails expected = FlagEvaluationDetails.builder() + .reason(Reason.ERROR.toString()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .errorMessage("Flag does_not_exists was not found in your configuration") + .value(new Value(123)) + .build(); + FlagEvaluationDetails got = goffClient.getObjectDetails(flagKey, new Value(123), defaultEvaluationContext); + assertEquals(expected, got); + } +} +