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