From c69b5a96dcbc9ffab9c487200e1646f7cb437c15 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 7 Aug 2023 18:46:04 +0300 Subject: [PATCH 01/11] Add InMemoryProvider Signed-off-by: liran2000 --- .../sdk/FlagEvaluationDetails.java | 1 + .../openfeature/sdk/e2e/StepDefinitions.java | 30 ++- .../dev/openfeature/sdk/testutils/Flag.java | 18 ++ .../dev/openfeature/sdk/testutils/Flags.java | 55 ++++++ .../sdk/testutils/InMemoryProvider.java | 181 ++++++++++++++++++ .../openfeature/sdk/testutils/ValueUtils.java | 74 +++++++ .../sdk/testutils/ValueUtilsTest.java | 48 +++++ .../resources/features/testing-flags.json | 114 +++++++++++ 8 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 src/test/java/dev/openfeature/sdk/testutils/Flag.java create mode 100644 src/test/java/dev/openfeature/sdk/testutils/Flags.java create mode 100644 src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java create mode 100644 src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java create mode 100644 src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java create mode 100644 src/test/resources/features/testing-flags.json diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index 78e04c71..b324c07c 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -40,6 +40,7 @@ public static FlagEvaluationDetails from(ProviderEvaluation providerEv .value(providerEval.getValue()) .variant(providerEval.getVariant()) .reason(providerEval.getReason()) + .errorMessage(providerEval.getErrorMessage()) .errorCode(providerEval.getErrorCode()) .flagMetadata(providerEval.getFlagMetadata()) .build(); diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 7048fc0b..c668986b 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -9,11 +9,17 @@ import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.testutils.Flag; +import dev.openfeature.sdk.testutils.Flags; +import dev.openfeature.sdk.testutils.InMemoryProvider; import io.cucumber.java.BeforeAll; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import lombok.SneakyThrows; +import java.io.File; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -47,13 +53,29 @@ public class StepDefinitions { private int typeErrorDefaultValue; private FlagEvaluationDetails typeErrorDetails; + @SneakyThrows @BeforeAll() @Given("an openfeature client is registered with cache disabled") public static void setup() { // TODO: when the FlagdProvider is updated to support caching, we might need to disable it here for this test to work as expected. - FlagdProvider provider = new FlagdProvider(); - provider.setDeadline(3000); // set a generous deadline, to prevent timeouts in actions + + Map flagsMap = new HashMap<>(); + Map variants = new HashMap<>(); + variants.put("on", true); + variants.put("off", false); + ClassLoader classLoader = StepDefinitions.class.getClassLoader(); + File file = new File(classLoader.getResource("features/testing-flags.json").getFile()); + Path resPath = file.toPath(); + String conf = new String(java.nio.file.Files.readAllBytes(resPath), "UTF8"); + Flags flags = Flags.builder().setConfigurationJson(conf).build(); + InMemoryProvider provider = new InMemoryProvider(conf); OpenFeatureAPI.getInstance().setProvider(provider); + + /* + TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 + */ + Thread.sleep(500); + client = OpenFeatureAPI.getInstance().getClient(); } @@ -233,7 +255,9 @@ public void an_a_flag_with_key_is_evaluated(String flagKey, String defaultValue) @Then("the resolved string response should be {string}") public void the_resolved_string_response_should_be(String expected) { - assertEquals(expected, this.contextAwareValue); + + // TODO: targeting context not supported at InMemoryProvider +// assertEquals(expected, this.contextAwareValue); } @Then("the resolved flag value is {string} when the context is empty") diff --git a/src/test/java/dev/openfeature/sdk/testutils/Flag.java b/src/test/java/dev/openfeature/sdk/testutils/Flag.java new file mode 100644 index 00000000..3f22f8bb --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/Flag.java @@ -0,0 +1,18 @@ +package dev.openfeature.sdk.testutils; + +import io.cucumber.core.internal.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +@Getter +public class Flag { + private Flags.State state; + private Map variants; + private String defaultVariant; +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/Flags.java b/src/test/java/dev/openfeature/sdk/testutils/Flags.java new file mode 100644 index 00000000..9b1b5851 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/Flags.java @@ -0,0 +1,55 @@ +package dev.openfeature.sdk.testutils; + +import io.cucumber.core.internal.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.cucumber.core.internal.com.fasterxml.jackson.core.JsonProcessingException; +import io.cucumber.core.internal.com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.ToString; + +import java.util.Map; + +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +public class Flags { + + public static class FlagsBuilder { + + private String configurationJson; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private FlagsBuilder() { + + } + + public FlagsBuilder setConfigurationJson(String configurationJson) { + this.configurationJson = configurationJson; + return this; + } + + public Flags build() throws JsonProcessingException { + return objectMapper.readValue(configurationJson, Flags.class); + } + + } + + public static FlagsBuilder builder() { + return new FlagsBuilder(); + } + + private Map flags; + + public enum State { + ENABLED, DISABLED + } + + public enum Variant { + on, off + } + + @Getter + public class Variants { + private Map variants; + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java b/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java new file mode 100644 index 00000000..80ff354e --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java @@ -0,0 +1,181 @@ +package dev.openfeature.sdk.testutils; + +import dev.openfeature.sdk.*; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * In-memory provider. + * + * Based on flagd configuration. + */ +@Slf4j +public class InMemoryProvider implements FeatureProvider { + + @Getter + private final String name = "InMemoryProvider"; + + private Flags flags; + + private String jsonConfig; + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + @Override + public Metadata getMetadata() { + return new Metadata() { + @Override + public String getName() { + return name; + } + }; + } + + public InMemoryProvider(String jsonConfig) { + this.jsonConfig = jsonConfig; + } + + public void initialize(EvaluationContext evaluationContext) throws Exception { + FeatureProvider.super.initialize(evaluationContext); + this.flags = Flags.builder().setConfigurationJson(jsonConfig).build(); + state = ProviderState.READY; + log.info("finishing initializing provider, state: {}", state); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + Flag flag = flags.getFlags().get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Boolean)) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } + boolean value = (boolean) flag.getVariants().get(flag.getDefaultVariant()); + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + Flag flag = flags.getFlags().get(key); + if (flag == null) { + ProviderEvaluation providerEvaluation = ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + return providerEvaluation; + } + if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof String)) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } + String value = (String) flag.getVariants().get(flag.getDefaultVariant()); + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + Flag flag = flags.getFlags().get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Integer)) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } + Integer value = (Integer) flag.getVariants().get(flag.getDefaultVariant()); + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + Flag flag = flags.getFlags().get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Double)) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } + Double value = (Double) flag.getVariants().get(flag.getDefaultVariant()); + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + + @SneakyThrows + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, + EvaluationContext invocationContext) { + Flag flag = flags.getFlags().get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + Object object = flag.getVariants().get(flag.getDefaultVariant()); + Value value = ValueUtils.convert(object); + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java new file mode 100644 index 00000000..fbae0674 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java @@ -0,0 +1,74 @@ +package dev.openfeature.sdk.testutils; + +import dev.openfeature.sdk.MutableStructure; +import dev.openfeature.sdk.Value; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Value utils. + */ +@UtilityClass +@Slf4j +public class ValueUtils { + + /** + * Convert object to Value. + * Supporting bean objects with field getters. + * Note: + * Not all objects may be supported. + * @param object + * @return + */ + @SneakyThrows + public static Value convert(Object object) { + if (object == null) { + return null; + } + if (ClassUtils.isPrimitiveOrWrapper(object.getClass()) || object instanceof String) { + return new Value(object); + } + if (object instanceof Map) { + Map map = (Map)object; + Map values = new HashMap<>(); + map.entrySet().stream().forEach(entry -> { + values.put(entry.getKey(), convert(entry.getValue())); + }); + return new Value(new MutableStructure(values)); + } + if (object instanceof List) { + List list = (List)object; + return new Value(list.stream().map(p -> convert(p)).collect(Collectors.toList())); + } + Map map = convertObjectToMap(object); + return convert(map); + } + + private static Map convertObjectToMap(Object object) throws IllegalAccessException, InvocationTargetException { + Map map = new HashMap<>(); + for (Field field: FieldUtils.getAllFields(object.getClass())) { + try { + String getterName = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); + Method getterMethod = object.getClass().getMethod(getterName); + Object value = getterMethod.invoke(object); + map.put(field.getName(), value); + } catch (NoSuchMethodException e) { + log.debug("Skipping field: {}", field.getName()); + } + + } + return map; + } + +} diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java new file mode 100644 index 00000000..e3488752 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java @@ -0,0 +1,48 @@ +package dev.openfeature.sdk.testutils; + +import dev.openfeature.sdk.Value; +import io.cucumber.core.internal.com.fasterxml.jackson.databind.ObjectMapper; +import lombok.*; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ValueUtilsTest { + + @Getter + @Builder + public static class Struct { + private int i; + private String s; + private Double d; + private Map> map = new HashMap<>(); + private List> list = new ArrayList<>(); + } + + @Data + @AllArgsConstructor + public static class Int { + private int value; + } + + @SneakyThrows + @Test + public void testObject() { + Map> map = new HashMap<>(); + Map innerMap = new HashMap<>(); + innerMap.put("innerKey1", new Int(4)); + map.put("key1", innerMap); + List innerList = new ArrayList<>(); + innerList.add(new Int(456)); + List> list = new ArrayList<>(); + list.add(innerList); + Struct struct = Struct.builder().i(3).d(1.2).s("str").map(map).list(list).build(); + Value value = ValueUtils.convert(struct); + assertEquals("Value(innerObject=MutableStructure(attributes={i=Value(innerObject=3), s=Value(innerObject=str), d=Value(innerObject=1.2), list=Value(innerObject=[Value(innerObject=[Value(innerObject=MutableStructure(attributes={value=Value(innerObject=456)}))])]), map=Value(innerObject=MutableStructure(attributes={key1=Value(innerObject=MutableStructure(attributes={innerKey1=Value(innerObject=MutableStructure(attributes={value=Value(innerObject=4)}))}))}))}))", value.toString()); + } +} \ No newline at end of file diff --git a/src/test/resources/features/testing-flags.json b/src/test/resources/features/testing-flags.json new file mode 100644 index 00000000..951253e3 --- /dev/null +++ b/src/test/resources/features/testing-flags.json @@ -0,0 +1,114 @@ +{ + "flags": { + "boolean-flag": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on" + }, + "string-flag": { + "state": "ENABLED", + "variants": { + "greeting": "hi", + "parting": "bye" + }, + "defaultVariant": "greeting" + }, + "integer-flag": { + "state": "ENABLED", + "variants": { + "one": 1, + "ten": 10 + }, + "defaultVariant": "ten" + }, + "float-flag": { + "state": "ENABLED", + "variants": { + "tenth": 0.1, + "half": 0.5 + }, + "defaultVariant": "half" + }, + "object-flag": { + "state": "ENABLED", + "variants": { + "empty": {}, + "template": { + "showImages": true, + "title": "Check out these pics!", + "imagesPerPage": 100 + } + }, + "defaultVariant": "template" + }, + "context-aware": { + "state": "ENABLED", + "variants": { + "internal": "INTERNAL", + "external": "EXTERNAL" + }, + "defaultVariant": "external", + "targeting": { + "if": [ + { + "and": [ + { + "==": [ + { + "var": [ + "fn" + ] + }, + "Sulisław" + ] + }, + { + "==": [ + { + "var": [ + "ln" + ] + }, + "Świętopełk" + ] + }, + { + "==": [ + { + "var": [ + "age" + ] + }, + 29 + ] + }, + { + "==": [ + { + "var": [ + "customer" + ] + }, + false + ] + } + ] + }, + "internal", + "external" + ] + } + }, + "wrong-flag": { + "state": "ENABLED", + "variants": { + "one": "uno", + "two": "dos" + }, + "defaultVariant": "one" + } + } +} \ No newline at end of file From 29c99ebf523e0dbf0d470fb5fe92194a653390bd Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 8 Aug 2023 09:30:47 +0300 Subject: [PATCH 02/11] Remove flagd dependencies Signed-off-by: liran2000 --- .github/workflows/pullrequest.yml | 7 - CONTRIBUTING.md | 14 -- pom.xml | 123 +++++------------- .../openfeature/sdk/e2e/StepDefinitions.java | 27 +--- .../dev/openfeature/sdk/testutils/Flags.java | 11 +- .../openfeature/sdk/testutils/ValueUtils.java | 18 ++- .../sdk/testutils/ValueUtilsTest.java | 12 +- .../resources/features/testing-flags.json | 58 --------- 8 files changed, 55 insertions(+), 215 deletions(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index f30513ac..8ad6c5e0 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -8,13 +8,6 @@ permissions: jobs: build: runs-on: ubuntu-latest - # TODO: this can be removed with https://github.com/open-feature/java-sdk/issues/523 - services: - flagd: - image: ghcr.io/open-feature/flagd-testbed:latest - ports: - - 8013:8013 - steps: - name: Check out the code uses: actions/checkout@96f53100ba2a5449eb71d2e6604bbcd94b9449b5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63c9d533..e05b50d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,20 +16,6 @@ If you think we might be out of date with the spec, you can check that by invoki If you're adding tests to cover something in the spec, use the `@Specification` annotation like you see throughout the test suites. -## End-to-End Tests - - - -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using [`flagd`](https://github.com/open-feature/flagd). These tests do not run with the default maven profile. If you'd like to run them locally, you can start the flagd testbed with - -``` -docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest -``` -and then run -``` -mvn test -P e2e-test -``` - ## Releasing See [releasing](./docs/release.md). diff --git a/pom.xml b/pom.xml index c8a8b0be..8cb877c0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,6 @@ 1.8 ${maven.compiler.source} 5.10.0 - - **/e2e/*.java ${groupId}.${artifactId} @@ -21,10 +19,10 @@ https://openfeature.dev - abrahms - Justin Abrahms - eBay - https://justin.abrah.ms/ + abrahms + Justin Abrahms + eBay + https://justin.abrah.ms/ @@ -120,9 +118,9 @@ - io.cucumber - cucumber-junit-platform-engine - test + io.cucumber + cucumber-junit-platform-engine + test @@ -140,38 +138,39 @@ - dev.openfeature.contrib.providers - flagd - 0.5.10 + org.awaitility + awaitility + 4.2.0 test - org.awaitility - awaitility - 4.2.0 + org.apache.commons + commons-lang3 + 3.13.0 test + - - io.cucumber - cucumber-bom - 7.13.0 - pom - import - - - - org.junit - junit-bom - 5.10.0 - pom - import - + + io.cucumber + cucumber-bom + 7.13.0 + pom + import + + + + org.junit + junit-bom + 5.10.0 + pom + import + @@ -203,7 +202,7 @@ - + maven-dependency-plugin 3.6.0 @@ -249,7 +248,7 @@ ${testExclusions} - + @@ -271,7 +270,7 @@ - prepare-agent + prepare-agent prepare-agent @@ -319,7 +318,7 @@ - + @@ -494,62 +493,6 @@ - - - - - - e2e-test - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.0 - - - update-test-harness-submodule - validate - - exec - - - - git - - submodule - update - --init - test-harness - - - - - copy-gherkin-tests - validate - - exec - - - - - cp - - test-harness/features/evaluation.feature - src/test/resources/features/ - - - - - - - - diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index c668986b..41a5d0a6 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,15 +1,6 @@ package dev.openfeature.sdk.e2e; -import dev.openfeature.contrib.providers.flagd.FlagdProvider; -import dev.openfeature.sdk.Client; -import dev.openfeature.sdk.EvaluationContext; -import dev.openfeature.sdk.FlagEvaluationDetails; -import dev.openfeature.sdk.ImmutableContext; -import dev.openfeature.sdk.OpenFeatureAPI; -import dev.openfeature.sdk.Reason; -import dev.openfeature.sdk.Structure; -import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.testutils.Flag; +import dev.openfeature.sdk.*; import dev.openfeature.sdk.testutils.Flags; import dev.openfeature.sdk.testutils.InMemoryProvider; import io.cucumber.java.BeforeAll; @@ -55,14 +46,8 @@ public class StepDefinitions { @SneakyThrows @BeforeAll() - @Given("an openfeature client is registered with cache disabled") + @Given("an openfeature client is registered") public static void setup() { - // TODO: when the FlagdProvider is updated to support caching, we might need to disable it here for this test to work as expected. - - Map flagsMap = new HashMap<>(); - Map variants = new HashMap<>(); - variants.put("on", true); - variants.put("off", false); ClassLoader classLoader = StepDefinitions.class.getClassLoader(); File file = new File(classLoader.getResource("features/testing-flags.json").getFile()); Path resPath = file.toPath(); @@ -71,9 +56,7 @@ public static void setup() { InMemoryProvider provider = new InMemoryProvider(conf); OpenFeatureAPI.getInstance().setProvider(provider); - /* - TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 - */ + // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 Thread.sleep(500); client = OpenFeatureAPI.getInstance().getClient(); @@ -289,7 +272,7 @@ public void then_the_default_string_value_should_be_returned() { public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) { assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason()); assertTrue(notFoundDetails.getErrorMessage().contains(errorCode)); - // TODO: add errorCode assertion once flagd provider is updated. + assertTrue(notFoundDetails.getErrorCode().name().equals(errorCode)); } // type mismatch @@ -310,7 +293,7 @@ public void then_the_default_integer_value_should_be_returned() { public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) { assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason()); assertTrue(typeErrorDetails.getErrorMessage().contains(errorCode)); - // TODO: add errorCode assertion once flagd provider is updated. + assertTrue(typeErrorDetails.getErrorCode().name().equals(errorCode)); } } diff --git a/src/test/java/dev/openfeature/sdk/testutils/Flags.java b/src/test/java/dev/openfeature/sdk/testutils/Flags.java index 9b1b5851..484deab8 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/Flags.java +++ b/src/test/java/dev/openfeature/sdk/testutils/Flags.java @@ -17,7 +17,7 @@ public static class FlagsBuilder { private String configurationJson; - private ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); private FlagsBuilder() { @@ -43,13 +43,4 @@ public static FlagsBuilder builder() { public enum State { ENABLED, DISABLED } - - public enum Variant { - on, off - } - - @Getter - public class Variants { - private Map variants; - } } diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java index fbae0674..572a1e0c 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java @@ -24,12 +24,12 @@ public class ValueUtils { /** - * Convert object to Value. + * Convert object to Value with limited support. * Supporting bean objects with field getters. * Note: - * Not all objects may be supported. - * @param object - * @return + * Not all objects are supported. + * @param object the object to convert + * @return Value representing the object */ @SneakyThrows public static Value convert(Object object) { @@ -40,16 +40,14 @@ public static Value convert(Object object) { return new Value(object); } if (object instanceof Map) { - Map map = (Map)object; + Map map = (Map)object; Map values = new HashMap<>(); - map.entrySet().stream().forEach(entry -> { - values.put(entry.getKey(), convert(entry.getValue())); - }); + map.forEach((k, v) -> values.put(k, convert(v))); return new Value(new MutableStructure(values)); } if (object instanceof List) { - List list = (List)object; - return new Value(list.stream().map(p -> convert(p)).collect(Collectors.toList())); + List list = (List)object; + return new Value(list.stream().map(ValueUtils::convert).collect(Collectors.toList())); } Map map = convertObjectToMap(object); return convert(map); diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java index e3488752..b575b91e 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java +++ b/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java @@ -1,7 +1,6 @@ package dev.openfeature.sdk.testutils; import dev.openfeature.sdk.Value; -import io.cucumber.core.internal.com.fasterxml.jackson.databind.ObjectMapper; import lombok.*; import org.junit.jupiter.api.Test; @@ -20,8 +19,8 @@ public static class Struct { private int i; private String s; private Double d; - private Map> map = new HashMap<>(); - private List> list = new ArrayList<>(); + private Map> map; + private List> list; } @Data @@ -43,6 +42,11 @@ public void testObject() { list.add(innerList); Struct struct = Struct.builder().i(3).d(1.2).s("str").map(map).list(list).build(); Value value = ValueUtils.convert(struct); - assertEquals("Value(innerObject=MutableStructure(attributes={i=Value(innerObject=3), s=Value(innerObject=str), d=Value(innerObject=1.2), list=Value(innerObject=[Value(innerObject=[Value(innerObject=MutableStructure(attributes={value=Value(innerObject=456)}))])]), map=Value(innerObject=MutableStructure(attributes={key1=Value(innerObject=MutableStructure(attributes={innerKey1=Value(innerObject=MutableStructure(attributes={value=Value(innerObject=4)}))}))}))}))", value.toString()); + assertEquals("Value(innerObject=MutableStructure(attributes={i=Value(innerObject=3), " + + "s=Value(innerObject=str), d=Value(innerObject=1.2), list=Value(innerObject=[Value(innerObject=" + + "[Value(innerObject=MutableStructure(attributes={value=Value(innerObject=456)}))])]), map=Value(" + + "innerObject=MutableStructure(attributes={key1=Value(innerObject=MutableStructure(attributes=" + + "{innerKey1=Value(innerObject=MutableStructure(attributes={value=Value(innerObject=4)}))}))}))}))", + value.toString()); } } \ No newline at end of file diff --git a/src/test/resources/features/testing-flags.json b/src/test/resources/features/testing-flags.json index 951253e3..9886cfd1 100644 --- a/src/test/resources/features/testing-flags.json +++ b/src/test/resources/features/testing-flags.json @@ -44,64 +44,6 @@ }, "defaultVariant": "template" }, - "context-aware": { - "state": "ENABLED", - "variants": { - "internal": "INTERNAL", - "external": "EXTERNAL" - }, - "defaultVariant": "external", - "targeting": { - "if": [ - { - "and": [ - { - "==": [ - { - "var": [ - "fn" - ] - }, - "Sulisław" - ] - }, - { - "==": [ - { - "var": [ - "ln" - ] - }, - "Świętopełk" - ] - }, - { - "==": [ - { - "var": [ - "age" - ] - }, - 29 - ] - }, - { - "==": [ - { - "var": [ - "customer" - ] - }, - false - ] - } - ] - }, - "internal", - "external" - ] - } - }, "wrong-flag": { "state": "ENABLED", "variants": { From 9af7d1d6e401411aad6bf8b7b2b32358ba1192b4 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 8 Aug 2023 09:49:28 +0300 Subject: [PATCH 03/11] handle PROVIDER_NOT_READY on evaluation Signed-off-by: liran2000 --- .../openfeature/sdk/e2e/StepDefinitions.java | 2 +- .../sdk/testutils/InMemoryProvider.java | 57 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 41a5d0a6..8445cbb8 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -46,7 +46,7 @@ public class StepDefinitions { @SneakyThrows @BeforeAll() - @Given("an openfeature client is registered") + @Given("an openfeature client is registered with cache disabled") public static void setup() { ClassLoader classLoader = StepDefinitions.class.getClassLoader(); File file = new File(classLoader.getResource("features/testing-flags.json").getFile()); diff --git a/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java b/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java index 80ff354e..12ca5e34 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java +++ b/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java @@ -41,11 +41,22 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { FeatureProvider.super.initialize(evaluationContext); this.flags = Flags.builder().setConfigurationJson(jsonConfig).build(); state = ProviderState.READY; - log.info("finishing initializing provider, state: {}", state); + log.info("finished initializing provider, state: {}", state); } @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } Flag flag = flags.getFlags().get(key); if (flag == null) { return ProviderEvaluation.builder() @@ -74,6 +85,17 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } Flag flag = flags.getFlags().get(key); if (flag == null) { ProviderEvaluation providerEvaluation = ProviderEvaluation.builder() @@ -103,6 +125,17 @@ public ProviderEvaluation getStringEvaluation(String key, String default @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } Flag flag = flags.getFlags().get(key); if (flag == null) { return ProviderEvaluation.builder() @@ -131,6 +164,17 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } Flag flag = flags.getFlags().get(key); if (flag == null) { return ProviderEvaluation.builder() @@ -161,6 +205,17 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } Flag flag = flags.getFlags().get(key); if (flag == null) { return ProviderEvaluation.builder() From 47d8127377630947af07680fa7a6941ca71c8dae Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 10 Aug 2023 20:09:12 +0300 Subject: [PATCH 04/11] in-memory provider updates - move to non-test package - add ContextEvaluator - update Flags structure - implement events provider - enrich tests - refactor Signed-off-by: liran2000 --- CONTRIBUTING.md | 4 + pom.xml | 7 - .../providers/memory/ContextEvaluator.java | 12 + .../sdk/providers/memory/Flag.java | 30 +++ .../sdk/providers/memory/Flags.java | 20 ++ .../providers/memory/InMemoryProvider.java | 184 ++++++++++++++ .../openfeature/sdk/e2e/StepDefinitions.java | 69 ++++- .../memory/InMemoryProviderTest.java | 155 ++++++++++++ .../dev/openfeature/sdk/testutils/Flag.java | 18 -- .../dev/openfeature/sdk/testutils/Flags.java | 46 ---- .../sdk/testutils/InMemoryProvider.java | 236 ------------------ .../openfeature/sdk/testutils/ValueUtils.java | 72 ------ .../sdk/testutils/ValueUtilsTest.java | 52 ---- 13 files changed, 461 insertions(+), 444 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/Flag.java create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/Flags.java create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java create mode 100644 src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java delete mode 100644 src/test/java/dev/openfeature/sdk/testutils/Flag.java delete mode 100644 src/test/java/dev/openfeature/sdk/testutils/Flags.java delete mode 100644 src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java delete mode 100644 src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java delete mode 100644 src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e05b50d2..d86ca62c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,10 @@ If you think we might be out of date with the spec, you can check that by invoki If you're adding tests to cover something in the spec, use the `@Specification` annotation like you see throughout the test suites. +## End-to-End Tests + +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. These tests do not run with the default maven profile. + ## Releasing See [releasing](./docs/release.md). diff --git a/pom.xml b/pom.xml index 8cb877c0..23c5ed3e 100644 --- a/pom.xml +++ b/pom.xml @@ -144,13 +144,6 @@ test - - org.apache.commons - commons-lang3 - 3.13.0 - test - - diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java b/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java new file mode 100644 index 00000000..02fa323c --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java @@ -0,0 +1,12 @@ +package dev.openfeature.sdk.providers.memory; + +import dev.openfeature.sdk.EvaluationContext; + +/** + * Context evaluator - use for resolving flag according to evaluation context, for handling targeting. + * @param expected value type + */ +public interface ContextEvaluator { + + T evaluate(Flag flag, EvaluationContext evaluationContext); +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java new file mode 100644 index 00000000..f82b5766 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -0,0 +1,30 @@ +package dev.openfeature.sdk.providers.memory; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import lombok.ToString; + +import java.util.Map; + +/** + * Flag representation. + */ +@ToString +@Builder +@Getter +public class Flag { + + /** + * Flag state. + */ + public enum State { + ENABLED, DISABLED + } + + private State state; + @Singular + private Map variants; + private String defaultVariant; + private ContextEvaluator contextEvaluator; +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java new file mode 100644 index 00000000..605366c1 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java @@ -0,0 +1,20 @@ +package dev.openfeature.sdk.providers.memory; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import lombok.ToString; + +import java.util.Map; + +/** + * Flags representation. + */ +@ToString +@Getter +@Builder +public class Flags { + + @Singular + private Map> flags; +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java new file mode 100644 index 00000000..817548c6 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -0,0 +1,184 @@ +package dev.openfeature.sdk.providers.memory; + +import dev.openfeature.sdk.*; +import dev.openfeature.sdk.exceptions.OpenFeatureError; +import dev.openfeature.sdk.exceptions.TypeMismatchError; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * In-memory provider. + */ +@Slf4j +public class InMemoryProvider extends EventProvider { + + @Getter + private final String name = "InMemoryProvider"; + + private Flags flags; + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + @Override + public Metadata getMetadata() { + return () -> name; + } + + public InMemoryProvider(Flags flags) { + this.flags = flags; + } + + /** + * Initialize the provider. + * @param evaluationContext evaluation context + * @throws Exception on error + */ + public void initialize(EvaluationContext evaluationContext) throws Exception { + super.initialize(evaluationContext); + state = ProviderState.READY; + log.info("finished initializing provider, state: {}", state); + ProviderEventDetails details = ProviderEventDetails.builder() + .message("provider is ready") + .build(); + emitProviderReady(details); + } + + /** + * Updating provider flags configuration, replacing existing flags. + * @param flags the flags to use instead of the previous flags. + */ + public void updateFlags(Flags flags) { + Set flagsChanged = new HashSet<>(); + flagsChanged.addAll(this.flags.getFlags().keySet()); + flagsChanged.addAll(flags.getFlags().keySet()); + this.flags = flags; + ProviderEventDetails details = ProviderEventDetails.builder() + .flagsChanged(new ArrayList<>(flagsChanged)) + .message("flags changed") + .build(); + emitProviderConfigurationChanged(details); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Boolean.class); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, String.class); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Integer.class); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Double.class); + } + + @SneakyThrows + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Value.class); + } + + private ProviderEvaluation getEvaluation( + String key, T defaultValue, EvaluationContext evaluationContext, Class expectedType + ) throws OpenFeatureError { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } + Flag flag = flags.getFlags().get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + T value; + if (flag.getContextEvaluator() != null) { + value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext); + } else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } else { + value = (T) flag.getVariants().get(flag.getDefaultVariant()); + } + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + + private static Value objectToValue(Object object) { + if (object instanceof Value) { + return (Value) object; + } else if (object == null) { + return null; + } else if (object instanceof String) { + return new Value((String) object); + } else if (object instanceof Boolean) { + return new Value((Boolean) object); + } else if (object instanceof Integer) { + return new Value((Integer) object); + } else if (object instanceof Double) { + return new Value((Double) object); + } else if (object instanceof Structure) { + return new Value((Structure) object); + } else if (object instanceof List) { + return new Value(((List) object).stream() + .map(InMemoryProvider::objectToValue) + .collect(Collectors.toList())); + } else if (object instanceof Instant) { + return new Value((Instant) object); + } else if (object instanceof Map) { + return new Value(mapToStructure((Map) object)); + } else { + throw new TypeMismatchError("Flag value " + object + " had unexpected type " + + object.getClass() + "."); + } + } + + /** + * transform a map to a Structure type. + * + * @param map map of objects + * @return a Structure object in the SDK format + */ + public static Structure mapToStructure(Map map) { + return new MutableStructure( + map.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); + } +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 8445cbb8..47752de8 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,19 +1,21 @@ package dev.openfeature.sdk.e2e; +import com.google.common.collect.ImmutableMap; import dev.openfeature.sdk.*; -import dev.openfeature.sdk.testutils.Flags; -import dev.openfeature.sdk.testutils.InMemoryProvider; +import dev.openfeature.sdk.providers.memory.ContextEvaluator; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.Flags; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; import io.cucumber.java.BeforeAll; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import lombok.SneakyThrows; -import java.io.File; -import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import static dev.openfeature.sdk.providers.memory.InMemoryProvider.mapToStructure; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -48,12 +50,55 @@ public class StepDefinitions { @BeforeAll() @Given("an openfeature client is registered with cache disabled") public static void setup() { - ClassLoader classLoader = StepDefinitions.class.getClassLoader(); - File file = new File(classLoader.getResource("features/testing-flags.json").getFile()); - Path resPath = file.toPath(); - String conf = new String(java.nio.file.Files.readAllBytes(resPath), "UTF8"); - Flags flags = Flags.builder().setConfigurationJson(conf).build(); - InMemoryProvider provider = new InMemoryProvider(conf); + Flags flags = Flags.builder() + .flag("boolean-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()) + .flag("string-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()) + .flag("integer-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()) + .flag("float-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()) + .flag("object-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()) + .flag("context-aware", Flag.builder().state(Flag.State.ENABLED) + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((ContextEvaluator) (flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()) + .flag("wrong-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()) + .build(); + InMemoryProvider provider = new InMemoryProvider(flags); OpenFeatureAPI.getInstance().setProvider(provider); // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 @@ -238,9 +283,7 @@ public void an_a_flag_with_key_is_evaluated(String flagKey, String defaultValue) @Then("the resolved string response should be {string}") public void the_resolved_string_response_should_be(String expected) { - - // TODO: targeting context not supported at InMemoryProvider -// assertEquals(expected, this.contextAwareValue); + assertEquals(expected, this.contextAwareValue); } @Then("the resolved flag value is {string} when the context is empty") diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java new file mode 100644 index 00000000..ee22ae1a --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -0,0 +1,155 @@ +package dev.openfeature.sdk.providers.memory; + +import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.*; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static dev.openfeature.sdk.providers.memory.InMemoryProvider.mapToStructure; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class InMemoryProviderTest { + + private static Client client; + + private static final AtomicBoolean isProviderReady = new AtomicBoolean(false); + + private static final AtomicBoolean isConfigurationChanged = new AtomicBoolean(false); + + @SneakyThrows + @BeforeAll + static void beforeAll() { + Flags flags = Flags.builder() + .flag("boolean-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()) + .flag("string-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()) + .flag("integer-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()) + .flag("float-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()) + .flag("object-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()) + .flag("context-aware", Flag.builder().state(Flag.State.ENABLED) + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()) + .flag("wrong-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()) + .build(); + InMemoryProvider provider = new InMemoryProvider(flags); + OpenFeatureAPI.getInstance().setProvider(provider); + + // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 + Thread.sleep(500); + + client = OpenFeatureAPI.getInstance().getClient(); + + client.onProviderReady(eventDetails -> isProviderReady.set(true)); + + client.onProviderConfigurationChanged(eventDetails -> isConfigurationChanged.set(true)); + provider.updateFlags(flags); + } + + @SneakyThrows + @Test + void eventsTest() { + assertTrue(isProviderReady.get()); + assertTrue(isConfigurationChanged.get()); + } + + @Test + void getBooleanEvaluation() { + assertTrue(client.getBooleanValue("boolean-flag", false)); + } + + @Test + void getStringEvaluation() { + assertEquals("hi", client.getStringValue("string-flag", "dummy")); + } + + @Test + void getIntegerEvaluation() { + assertEquals(10, client.getIntegerValue("integer-flag", 999)); + } + + @Test + void getDoubleEvaluation() { + assertEquals(0.5, client.getDoubleValue("float-flag", 9.99)); + } + + @Test + void getObjectEvaluation() { + Value expectedObject = new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + ))); + assertEquals(expectedObject, client.getObjectValue("object-flag", new Value(true))); + } + + @SneakyThrows + @Test + void mapToStructureTest() { + Map map = new HashMap<>(); + map.put("String", "str"); + map.put("Boolean", true); + map.put("Integer", 1); + map.put("Double", 1.1); + map.put("List", Collections.singletonList(new Value(1))); + map.put("Value", new Value((true))); + map.put("Instant", Instant.ofEpochSecond(0)); + map.put("Map", new HashMap<>()); + ImmutableContext immutableContext = new ImmutableContext(); + map.put("ImmutableContext", immutableContext); + Structure res = mapToStructure(map); + assertEquals(new Value("str"), res.getValue("String")); + assertEquals(new Value(true), res.getValue("Boolean")); + assertEquals(new Value(1), res.getValue("Integer")); + assertEquals(new Value(1.1), res.getValue("Double")); + assertEquals(new Value(Collections.singletonList(new Value(1))), res.getValue("List")); + assertEquals(new Value(true), res.getValue("Value")); + assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant")); + assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap()); + assertEquals(new Value(immutableContext), res.getValue("ImmutableContext")); + } + +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/testutils/Flag.java b/src/test/java/dev/openfeature/sdk/testutils/Flag.java deleted file mode 100644 index 3f22f8bb..00000000 --- a/src/test/java/dev/openfeature/sdk/testutils/Flag.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import io.cucumber.core.internal.com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; - -import java.util.Map; - -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -@NoArgsConstructor -@Getter -public class Flag { - private Flags.State state; - private Map variants; - private String defaultVariant; -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/Flags.java b/src/test/java/dev/openfeature/sdk/testutils/Flags.java deleted file mode 100644 index 484deab8..00000000 --- a/src/test/java/dev/openfeature/sdk/testutils/Flags.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import io.cucumber.core.internal.com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import io.cucumber.core.internal.com.fasterxml.jackson.core.JsonProcessingException; -import io.cucumber.core.internal.com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.ToString; - -import java.util.Map; - -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -@Getter -public class Flags { - - public static class FlagsBuilder { - - private String configurationJson; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private FlagsBuilder() { - - } - - public FlagsBuilder setConfigurationJson(String configurationJson) { - this.configurationJson = configurationJson; - return this; - } - - public Flags build() throws JsonProcessingException { - return objectMapper.readValue(configurationJson, Flags.class); - } - - } - - public static FlagsBuilder builder() { - return new FlagsBuilder(); - } - - private Map flags; - - public enum State { - ENABLED, DISABLED - } -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java b/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java deleted file mode 100644 index 12ca5e34..00000000 --- a/src/test/java/dev/openfeature/sdk/testutils/InMemoryProvider.java +++ /dev/null @@ -1,236 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import dev.openfeature.sdk.*; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -/** - * In-memory provider. - * - * Based on flagd configuration. - */ -@Slf4j -public class InMemoryProvider implements FeatureProvider { - - @Getter - private final String name = "InMemoryProvider"; - - private Flags flags; - - private String jsonConfig; - - @Getter - private ProviderState state = ProviderState.NOT_READY; - - @Override - public Metadata getMetadata() { - return new Metadata() { - @Override - public String getName() { - return name; - } - }; - } - - public InMemoryProvider(String jsonConfig) { - this.jsonConfig = jsonConfig; - } - - public void initialize(EvaluationContext evaluationContext) throws Exception { - FeatureProvider.super.initialize(evaluationContext); - this.flags = Flags.builder().setConfigurationJson(jsonConfig).build(); - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); - } - - @Override - public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; - } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); - } - Flag flag = flags.getFlags().get(key); - if (flag == null) { - return ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Boolean)) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(flag.getDefaultVariant()) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.TYPE_MISMATCH.name()) - .errorCode(ErrorCode.TYPE_MISMATCH) - .build(); - } - boolean value = (boolean) flag.getVariants().get(flag.getDefaultVariant()); - return ProviderEvaluation.builder() - .value(value) - .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) - .build(); - } - - @Override - public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; - } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); - } - Flag flag = flags.getFlags().get(key); - if (flag == null) { - ProviderEvaluation providerEvaluation = ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - return providerEvaluation; - } - if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof String)) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(flag.getDefaultVariant()) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.TYPE_MISMATCH.name()) - .errorCode(ErrorCode.TYPE_MISMATCH) - .build(); - } - String value = (String) flag.getVariants().get(flag.getDefaultVariant()); - return ProviderEvaluation.builder() - .value(value) - .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) - .build(); - } - - @Override - public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; - } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); - } - Flag flag = flags.getFlags().get(key); - if (flag == null) { - return ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Integer)) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(flag.getDefaultVariant()) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.TYPE_MISMATCH.name()) - .errorCode(ErrorCode.TYPE_MISMATCH) - .build(); - } - Integer value = (Integer) flag.getVariants().get(flag.getDefaultVariant()); - return ProviderEvaluation.builder() - .value(value) - .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) - .build(); - } - - @Override - public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; - } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); - } - Flag flag = flags.getFlags().get(key); - if (flag == null) { - return ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - if (!(flag.getVariants().get(flag.getDefaultVariant()) instanceof Double)) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(flag.getDefaultVariant()) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.TYPE_MISMATCH.name()) - .errorCode(ErrorCode.TYPE_MISMATCH) - .build(); - } - Double value = (Double) flag.getVariants().get(flag.getDefaultVariant()); - return ProviderEvaluation.builder() - .value(value) - .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) - .build(); - } - - @SneakyThrows - @Override - public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, - EvaluationContext invocationContext) { - if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; - } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); - } - Flag flag = flags.getFlags().get(key); - if (flag == null) { - return ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); - } - Object object = flag.getVariants().get(flag.getDefaultVariant()); - Value value = ValueUtils.convert(object); - return ProviderEvaluation.builder() - .value(value) - .variant(flag.getDefaultVariant()) - .reason(Reason.STATIC.toString()) - .build(); - } -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java deleted file mode 100644 index 572a1e0c..00000000 --- a/src/test/java/dev/openfeature/sdk/testutils/ValueUtils.java +++ /dev/null @@ -1,72 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import dev.openfeature.sdk.MutableStructure; -import dev.openfeature.sdk.Value; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ClassUtils; -import org.apache.commons.lang3.reflect.FieldUtils; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Value utils. - */ -@UtilityClass -@Slf4j -public class ValueUtils { - - /** - * Convert object to Value with limited support. - * Supporting bean objects with field getters. - * Note: - * Not all objects are supported. - * @param object the object to convert - * @return Value representing the object - */ - @SneakyThrows - public static Value convert(Object object) { - if (object == null) { - return null; - } - if (ClassUtils.isPrimitiveOrWrapper(object.getClass()) || object instanceof String) { - return new Value(object); - } - if (object instanceof Map) { - Map map = (Map)object; - Map values = new HashMap<>(); - map.forEach((k, v) -> values.put(k, convert(v))); - return new Value(new MutableStructure(values)); - } - if (object instanceof List) { - List list = (List)object; - return new Value(list.stream().map(ValueUtils::convert).collect(Collectors.toList())); - } - Map map = convertObjectToMap(object); - return convert(map); - } - - private static Map convertObjectToMap(Object object) throws IllegalAccessException, InvocationTargetException { - Map map = new HashMap<>(); - for (Field field: FieldUtils.getAllFields(object.getClass())) { - try { - String getterName = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); - Method getterMethod = object.getClass().getMethod(getterName); - Object value = getterMethod.invoke(object); - map.put(field.getName(), value); - } catch (NoSuchMethodException e) { - log.debug("Skipping field: {}", field.getName()); - } - - } - return map; - } - -} diff --git a/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java b/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java deleted file mode 100644 index b575b91e..00000000 --- a/src/test/java/dev/openfeature/sdk/testutils/ValueUtilsTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.openfeature.sdk.testutils; - -import dev.openfeature.sdk.Value; -import lombok.*; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ValueUtilsTest { - - @Getter - @Builder - public static class Struct { - private int i; - private String s; - private Double d; - private Map> map; - private List> list; - } - - @Data - @AllArgsConstructor - public static class Int { - private int value; - } - - @SneakyThrows - @Test - public void testObject() { - Map> map = new HashMap<>(); - Map innerMap = new HashMap<>(); - innerMap.put("innerKey1", new Int(4)); - map.put("key1", innerMap); - List innerList = new ArrayList<>(); - innerList.add(new Int(456)); - List> list = new ArrayList<>(); - list.add(innerList); - Struct struct = Struct.builder().i(3).d(1.2).s("str").map(map).list(list).build(); - Value value = ValueUtils.convert(struct); - assertEquals("Value(innerObject=MutableStructure(attributes={i=Value(innerObject=3), " + - "s=Value(innerObject=str), d=Value(innerObject=1.2), list=Value(innerObject=[Value(innerObject=" + - "[Value(innerObject=MutableStructure(attributes={value=Value(innerObject=456)}))])]), map=Value(" + - "innerObject=MutableStructure(attributes={key1=Value(innerObject=MutableStructure(attributes=" + - "{innerKey1=Value(innerObject=MutableStructure(attributes={value=Value(innerObject=4)}))}))}))}))", - value.toString()); - } -} \ No newline at end of file From 542395e97fc781e8bc7ed29a3d11a8da522f5eca Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 10 Aug 2023 20:23:23 +0300 Subject: [PATCH 05/11] Remove Flags structure Signed-off-by: liran2000 --- .../sdk/providers/memory/Flags.java | 20 ---- .../providers/memory/InMemoryProvider.java | 20 ++-- .../openfeature/sdk/e2e/StepDefinitions.java | 54 +--------- .../memory/InMemoryProviderTest.java | 102 +++++++++--------- 4 files changed, 65 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/Flags.java diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java deleted file mode 100644 index 605366c1..00000000 --- a/src/main/java/dev/openfeature/sdk/providers/memory/Flags.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.openfeature.sdk.providers.memory; - -import lombok.Builder; -import lombok.Getter; -import lombok.Singular; -import lombok.ToString; - -import java.util.Map; - -/** - * Flags representation. - */ -@ToString -@Getter -@Builder -public class Flags { - - @Singular - private Map> flags; -} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index 817548c6..6b7c8897 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -18,20 +18,20 @@ public class InMemoryProvider extends EventProvider { @Getter - private final String name = "InMemoryProvider"; + private static final String NAME = "InMemoryProvider"; - private Flags flags; + private Map> flags; @Getter private ProviderState state = ProviderState.NOT_READY; @Override public Metadata getMetadata() { - return () -> name; + return () -> NAME; } - public InMemoryProvider(Flags flags) { - this.flags = flags; + public InMemoryProvider(Map> flags) { + this.flags = new HashMap<>(flags); } /** @@ -53,11 +53,11 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { * Updating provider flags configuration, replacing existing flags. * @param flags the flags to use instead of the previous flags. */ - public void updateFlags(Flags flags) { + public void updateFlags(Map> flags) { Set flagsChanged = new HashSet<>(); - flagsChanged.addAll(this.flags.getFlags().keySet()); - flagsChanged.addAll(flags.getFlags().keySet()); - this.flags = flags; + flagsChanged.addAll(this.flags.keySet()); + flagsChanged.addAll(flags.keySet()); + this.flags = new HashMap<>(flags); ProviderEventDetails details = ProviderEventDetails.builder() .flagsChanged(new ArrayList<>(flagsChanged)) .message("flags changed") @@ -110,7 +110,7 @@ private ProviderEvaluation getEvaluation( .value(defaultValue) .build(); } - Flag flag = flags.getFlags().get(key); + Flag flag = flags.get(key); if (flag == null) { return ProviderEvaluation.builder() .value(defaultValue) diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 47752de8..f0e20e6c 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,10 +1,7 @@ package dev.openfeature.sdk.e2e; -import com.google.common.collect.ImmutableMap; import dev.openfeature.sdk.*; -import dev.openfeature.sdk.providers.memory.ContextEvaluator; import dev.openfeature.sdk.providers.memory.Flag; -import dev.openfeature.sdk.providers.memory.Flags; import dev.openfeature.sdk.providers.memory.InMemoryProvider; import io.cucumber.java.BeforeAll; import io.cucumber.java.en.Given; @@ -15,7 +12,7 @@ import java.util.HashMap; import java.util.Map; -import static dev.openfeature.sdk.providers.memory.InMemoryProvider.mapToStructure; +import static dev.openfeature.sdk.providers.memory.InMemoryProviderTest.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -50,54 +47,7 @@ public class StepDefinitions { @BeforeAll() @Given("an openfeature client is registered with cache disabled") public static void setup() { - Flags flags = Flags.builder() - .flag("boolean-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("on", true) - .variant("off", false) - .defaultVariant("on") - .build()) - .flag("string-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("greeting", "hi") - .variant("parting", "bye") - .defaultVariant("greeting") - .build()) - .flag("integer-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", 1) - .variant("ten", 10) - .defaultVariant("ten") - .build()) - .flag("float-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("tenth", 0.1) - .variant("half", 0.5) - .defaultVariant("half") - .build()) - .flag("object-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("empty", new HashMap<>()) - .variant("template", new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100) - )))) - .defaultVariant("template") - .build()) - .flag("context-aware", Flag.builder().state(Flag.State.ENABLED) - .variant("internal", "INTERNAL") - .variant("external", "EXTERNAL") - .defaultVariant("external") - .contextEvaluator((ContextEvaluator) (flag, evaluationContext) -> { - if (new Value(false).equals(evaluationContext.getValue("customer"))) { - return (String) flag.getVariants().get("internal"); - } else { - return (String) flag.getVariants().get(flag.getDefaultVariant()); - } - }) - .build()) - .flag("wrong-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", "uno") - .variant("two", "dos") - .defaultVariant("one") - .build()) - .build(); + Map> flags = buildFlags(); InMemoryProvider provider = new InMemoryProvider(flags); OpenFeatureAPI.getInstance().setProvider(provider); diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index ee22ae1a..62ad6d7c 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class InMemoryProviderTest { +public class InMemoryProviderTest { private static Client client; @@ -27,54 +27,7 @@ class InMemoryProviderTest { @SneakyThrows @BeforeAll static void beforeAll() { - Flags flags = Flags.builder() - .flag("boolean-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("on", true) - .variant("off", false) - .defaultVariant("on") - .build()) - .flag("string-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("greeting", "hi") - .variant("parting", "bye") - .defaultVariant("greeting") - .build()) - .flag("integer-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", 1) - .variant("ten", 10) - .defaultVariant("ten") - .build()) - .flag("float-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("tenth", 0.1) - .variant("half", 0.5) - .defaultVariant("half") - .build()) - .flag("object-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("empty", new HashMap<>()) - .variant("template", new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100) - )))) - .defaultVariant("template") - .build()) - .flag("context-aware", Flag.builder().state(Flag.State.ENABLED) - .variant("internal", "INTERNAL") - .variant("external", "EXTERNAL") - .defaultVariant("external") - .contextEvaluator((flag, evaluationContext) -> { - if (new Value(false).equals(evaluationContext.getValue("customer"))) { - return (String) flag.getVariants().get("internal"); - } else { - return (String) flag.getVariants().get(flag.getDefaultVariant()); - } - }) - .build()) - .flag("wrong-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", "uno") - .variant("two", "dos") - .defaultVariant("one") - .build()) - .build(); + Map> flags = buildFlags(); InMemoryProvider provider = new InMemoryProvider(flags); OpenFeatureAPI.getInstance().setProvider(provider); @@ -89,6 +42,57 @@ static void beforeAll() { provider.updateFlags(flags); } + public static Map> buildFlags() { + Map> flags = new HashMap<>(); + flags.put("boolean-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + flags.put("string-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()); + flags.put("integer-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()); + flags.put("float-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()); + flags.put("object-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()); + flags.put("context-aware", Flag.builder().state(Flag.State.ENABLED) + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()); + flags.put("wrong-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()); + return flags; + } + @SneakyThrows @Test void eventsTest() { From 4cf9ba40b589ad8c7559874a6eed8012dae70be5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 10 Aug 2023 21:31:00 +0300 Subject: [PATCH 06/11] Remove leftover Signed-off-by: liran2000 --- CONTRIBUTING.md | 2 +- .../resources/features/testing-flags.json | 56 ------------------- 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 src/test/resources/features/testing-flags.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d86ca62c..bf599995 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ If you're adding tests to cover something in the spec, use the `@Specification` ## End-to-End Tests -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. These tests do not run with the default maven profile. +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. ## Releasing diff --git a/src/test/resources/features/testing-flags.json b/src/test/resources/features/testing-flags.json deleted file mode 100644 index 9886cfd1..00000000 --- a/src/test/resources/features/testing-flags.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "flags": { - "boolean-flag": { - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - }, - "string-flag": { - "state": "ENABLED", - "variants": { - "greeting": "hi", - "parting": "bye" - }, - "defaultVariant": "greeting" - }, - "integer-flag": { - "state": "ENABLED", - "variants": { - "one": 1, - "ten": 10 - }, - "defaultVariant": "ten" - }, - "float-flag": { - "state": "ENABLED", - "variants": { - "tenth": 0.1, - "half": 0.5 - }, - "defaultVariant": "half" - }, - "object-flag": { - "state": "ENABLED", - "variants": { - "empty": {}, - "template": { - "showImages": true, - "title": "Check out these pics!", - "imagesPerPage": 100 - } - }, - "defaultVariant": "template" - }, - "wrong-flag": { - "state": "ENABLED", - "variants": { - "one": "uno", - "two": "dos" - }, - "defaultVariant": "one" - } - } -} \ No newline at end of file From f85cbb5a71be81810b31c2ba1da58d590c2078a5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Aug 2023 14:02:30 +0300 Subject: [PATCH 07/11] Move common methods Signed-off-by: liran2000 --- pom.xml | 53 +++++++++++++ .../dev/openfeature/sdk/MutableStructure.java | 15 ++++ src/main/java/dev/openfeature/sdk/Value.java | 37 ++++++++++ .../providers/memory/InMemoryProvider.java | 74 +++++++------------ .../openfeature/sdk/e2e/StepDefinitions.java | 11 ++- .../memory/InMemoryProviderTest.java | 64 +++------------- .../sdk/testutils/TestFlagsUtils.java | 73 ++++++++++++++++++ 7 files changed, 227 insertions(+), 100 deletions(-) create mode 100644 src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java diff --git a/pom.xml b/pom.xml index 23c5ed3e..95d23838 100644 --- a/pom.xml +++ b/pom.xml @@ -486,6 +486,59 @@ + + + e2e-test + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + update-test-harness-submodule + validate + + exec + + + + git + + submodule + update + --init + test-harness + + + + + copy-gherkin-tests + validate + + exec + + + + + cp + + test-harness/features/evaluation.feature + src/test/resources/features/ + + + + + + + + diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index 343fe2c9..576caf56 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -10,6 +10,8 @@ import java.util.Set; import java.util.stream.Collectors; +import static dev.openfeature.sdk.Value.objectToValue; + /** * {@link MutableStructure} represents a potentially nested object type which is used to represent * structured data. @@ -108,4 +110,17 @@ public Map asObjectMap() { e -> convertValue(getValue(e.getKey())) )); } + + /** + * transform a map to a Structure type. + * + * @param map map of objects + * @return a Structure object in the SDK format + */ + public static Structure mapToStructure(Map map) { + return new MutableStructure( + map.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); + } } diff --git a/src/main/java/dev/openfeature/sdk/Value.java b/src/main/java/dev/openfeature/sdk/Value.java index 672b65c1..2444f264 100644 --- a/src/main/java/dev/openfeature/sdk/Value.java +++ b/src/main/java/dev/openfeature/sdk/Value.java @@ -5,10 +5,13 @@ import java.util.Map; import java.util.stream.Collectors; +import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.EqualsAndHashCode; import lombok.SneakyThrows; import lombok.ToString; +import static dev.openfeature.sdk.MutableStructure.mapToStructure; + /** * Values serve as a generic return type for structure data from providers. * Providers may deal in JSON, protobuf, XML or some other data-interchange format. @@ -280,4 +283,38 @@ protected Value clone() { } return new Value(this.asObject()); } + + /** + * Wrap an object into a Value. + * + * @param object the object to wrap + * @return the wrapped object + */ + public static Value objectToValue(Object object) { + if (object instanceof Value) { + return (Value) object; + } else if (object == null) { + return null; + } else if (object instanceof String) { + return new Value((String) object); + } else if (object instanceof Boolean) { + return new Value((Boolean) object); + } else if (object instanceof Integer) { + return new Value((Integer) object); + } else if (object instanceof Double) { + return new Value((Double) object); + } else if (object instanceof Structure) { + return new Value((Structure) object); + } else if (object instanceof List) { + return new Value(((List) object).stream() + .map(o -> objectToValue(o)) + .collect(Collectors.toList())); + } else if (object instanceof Instant) { + return new Value((Instant) object); + } else if (object instanceof Map) { + return new Value(mapToStructure((Map) object)); + } else { + throw new TypeMismatchError("Flag value " + object + " had unexpected type " + object.getClass() + "."); + } + } } diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index 6b7c8897..45567a03 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -1,15 +1,25 @@ package dev.openfeature.sdk.providers.memory; -import dev.openfeature.sdk.*; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.ProviderState; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.exceptions.OpenFeatureError; -import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.ArrayList; /** * In-memory provider. @@ -65,6 +75,19 @@ public void updateFlags(Map> flags) { emitProviderConfigurationChanged(details); } + /** + * Updating provider flags configuration with adding or updating a flag. + * @param flag the flag to update. If a flag with this key already exists, new flag replaces it. + */ + public void updateFlag(String flagKey, Flag flag) { + this.flags.put(flagKey, flag); + ProviderEventDetails details = ProviderEventDetails.builder() + .flagsChanged(Arrays.asList(flagKey)) + .message("flag added/updated") + .build(); + emitProviderConfigurationChanged(details); + } + @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext evaluationContext) { @@ -140,45 +163,4 @@ private ProviderEvaluation getEvaluation( .build(); } - private static Value objectToValue(Object object) { - if (object instanceof Value) { - return (Value) object; - } else if (object == null) { - return null; - } else if (object instanceof String) { - return new Value((String) object); - } else if (object instanceof Boolean) { - return new Value((Boolean) object); - } else if (object instanceof Integer) { - return new Value((Integer) object); - } else if (object instanceof Double) { - return new Value((Double) object); - } else if (object instanceof Structure) { - return new Value((Structure) object); - } else if (object instanceof List) { - return new Value(((List) object).stream() - .map(InMemoryProvider::objectToValue) - .collect(Collectors.toList())); - } else if (object instanceof Instant) { - return new Value((Instant) object); - } else if (object instanceof Map) { - return new Value(mapToStructure((Map) object)); - } else { - throw new TypeMismatchError("Flag value " + object + " had unexpected type " - + object.getClass() + "."); - } - } - - /** - * transform a map to a Structure type. - * - * @param map map of objects - * @return a Structure object in the SDK format - */ - public static Structure mapToStructure(Map map) { - return new MutableStructure( - map.entrySet().stream() - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); - } } diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index f0e20e6c..650fa242 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,6 +1,13 @@ package dev.openfeature.sdk.e2e; -import dev.openfeature.sdk.*; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Structure; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.providers.memory.Flag; import dev.openfeature.sdk.providers.memory.InMemoryProvider; import io.cucumber.java.BeforeAll; @@ -12,7 +19,7 @@ import java.util.HashMap; import java.util.Map; -import static dev.openfeature.sdk.providers.memory.InMemoryProviderTest.buildFlags; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index 62ad6d7c..d523f16d 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -1,7 +1,11 @@ package dev.openfeature.sdk.providers.memory; import com.google.common.collect.ImmutableMap; -import dev.openfeature.sdk.*; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Structure; +import dev.openfeature.sdk.ImmutableContext; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -11,8 +15,10 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; -import static dev.openfeature.sdk.providers.memory.InMemoryProvider.mapToStructure; +import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -22,7 +28,7 @@ public class InMemoryProviderTest { private static final AtomicBoolean isProviderReady = new AtomicBoolean(false); - private static final AtomicBoolean isConfigurationChanged = new AtomicBoolean(false); + private static final AtomicInteger configurationChangedEventCount = new AtomicInteger(0); @SneakyThrows @BeforeAll @@ -38,66 +44,20 @@ static void beforeAll() { client.onProviderReady(eventDetails -> isProviderReady.set(true)); - client.onProviderConfigurationChanged(eventDetails -> isConfigurationChanged.set(true)); + client.onProviderConfigurationChanged(eventDetails -> configurationChangedEventCount.incrementAndGet()); provider.updateFlags(flags); - } - - public static Map> buildFlags() { - Map> flags = new HashMap<>(); - flags.put("boolean-flag", Flag.builder().state(Flag.State.ENABLED) + provider.updateFlag("addedFlag", Flag.builder().state(Flag.State.ENABLED) .variant("on", true) .variant("off", false) .defaultVariant("on") .build()); - flags.put("string-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("greeting", "hi") - .variant("parting", "bye") - .defaultVariant("greeting") - .build()); - flags.put("integer-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", 1) - .variant("ten", 10) - .defaultVariant("ten") - .build()); - flags.put("float-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("tenth", 0.1) - .variant("half", 0.5) - .defaultVariant("half") - .build()); - flags.put("object-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("empty", new HashMap<>()) - .variant("template", new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100) - )))) - .defaultVariant("template") - .build()); - flags.put("context-aware", Flag.builder().state(Flag.State.ENABLED) - .variant("internal", "INTERNAL") - .variant("external", "EXTERNAL") - .defaultVariant("external") - .contextEvaluator((flag, evaluationContext) -> { - if (new Value(false).equals(evaluationContext.getValue("customer"))) { - return (String) flag.getVariants().get("internal"); - } else { - return (String) flag.getVariants().get(flag.getDefaultVariant()); - } - }) - .build()); - flags.put("wrong-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", "uno") - .variant("two", "dos") - .defaultVariant("one") - .build()); - return flags; } @SneakyThrows @Test void eventsTest() { assertTrue(isProviderReady.get()); - assertTrue(isConfigurationChanged.get()); + assertEquals(2, configurationChangedEventCount.get()); } @Test diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java new file mode 100644 index 00000000..619b7a6c --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -0,0 +1,73 @@ +package dev.openfeature.sdk.testutils; + +import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.providers.memory.Flag; +import lombok.experimental.UtilityClass; + +import java.util.HashMap; +import java.util.Map; + +import static dev.openfeature.sdk.MutableStructure.mapToStructure; + +/** + * Test flags utils. + */ +@UtilityClass +public class TestFlagsUtils { + + /** + * Building flags for testing purposes. + * @return + */ + public static Map> buildFlags() { + Map> flags = new HashMap<>(); + flags.put("boolean-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + flags.put("string-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()); + flags.put("integer-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()); + flags.put("float-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()); + flags.put("object-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()); + flags.put("context-aware", Flag.builder().state(Flag.State.ENABLED) + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()); + flags.put("wrong-flag", Flag.builder().state(Flag.State.ENABLED) + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()); + return flags; + } +} From 1e508ca86eefd5da3125d40d000f42de27f1af97 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 12 Aug 2023 17:42:56 +0300 Subject: [PATCH 08/11] tests updates Signed-off-by: liran2000 --- .../dev/openfeature/sdk/MutableStructure.java | 2 +- .../sdk/providers/memory/Flag.java | 9 -- .../dev/openfeature/sdk/StructureTest.java | 42 ++++++++- .../memory/InMemoryProviderTest.java | 35 +------ .../sdk/testutils/TestFlagsUtils.java | 94 +++++++++---------- 5 files changed, 87 insertions(+), 95 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index 576caf56..204fb907 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -112,7 +112,7 @@ public Map asObjectMap() { } /** - * transform a map to a Structure type. + * Transform an object map to a {@link Structure} type. * * @param map map of objects * @return a Structure object in the SDK format diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java index f82b5766..c79f63bb 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -14,15 +14,6 @@ @Builder @Getter public class Flag { - - /** - * Flag state. - */ - public enum State { - ENABLED, DISABLED - } - - private State state; @Singular private Map variants; private String defaultVariant; diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java index f05f9302..697aa169 100644 --- a/src/test/java/dev/openfeature/sdk/StructureTest.java +++ b/src/test/java/dev/openfeature/sdk/StructureTest.java @@ -1,16 +1,20 @@ package dev.openfeature.sdk; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertTrue; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; +import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class StructureTest { @Test public void noArgShouldContainEmptyAttributes() { @@ -46,7 +50,7 @@ public class StructureTest { double DOUBLE_VAL = .5; Instant DATE_VAL = Instant.now(); MutableStructure STRUCT_VAL = new MutableStructure(); - List LIST_VAL = new ArrayList(); + List LIST_VAL = new ArrayList<>(); Value VALUE_VAL = new Value(); MutableStructure structure = new MutableStructure(); @@ -68,4 +72,32 @@ public class StructureTest { assertEquals(LIST_VAL, structure.getValue(LIST_KEY).asList()); assertTrue(structure.getValue(VALUE_KEY).isNull()); } + + @SneakyThrows + @Test + void mapToStructureTest() { + Map map = new HashMap<>(); + map.put("String", "str"); + map.put("Boolean", true); + map.put("Integer", 1); + map.put("Double", 1.1); + map.put("List", Collections.singletonList(new Value(1))); + map.put("Value", new Value((true))); + map.put("Instant", Instant.ofEpochSecond(0)); + map.put("Map", new HashMap<>()); + map.put("nullKey", null); + ImmutableContext immutableContext = new ImmutableContext(); + map.put("ImmutableContext", immutableContext); + Structure res = mapToStructure(map); + assertEquals(new Value("str"), res.getValue("String")); + assertEquals(new Value(true), res.getValue("Boolean")); + assertEquals(new Value(1), res.getValue("Integer")); + assertEquals(new Value(1.1), res.getValue("Double")); + assertEquals(new Value(Collections.singletonList(new Value(1))), res.getValue("List")); + assertEquals(new Value(true), res.getValue("Value")); + assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant")); + assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap()); + assertEquals(new Value(immutableContext), res.getValue("ImmutableContext")); + assertNull(res.getValue("nullKey")); + } } diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index d523f16d..8d9ae06c 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -1,18 +1,13 @@ package dev.openfeature.sdk.providers.memory; import com.google.common.collect.ImmutableMap; -import dev.openfeature.sdk.Value; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.OpenFeatureAPI; -import dev.openfeature.sdk.Structure; -import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.Value; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.time.Instant; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -46,7 +41,7 @@ static void beforeAll() { client.onProviderConfigurationChanged(eventDetails -> configurationChangedEventCount.incrementAndGet()); provider.updateFlags(flags); - provider.updateFlag("addedFlag", Flag.builder().state(Flag.State.ENABLED) + provider.updateFlag("addedFlag", Flag.builder() .variant("on", true) .variant("off", false) .defaultVariant("on") @@ -90,30 +85,4 @@ void getObjectEvaluation() { assertEquals(expectedObject, client.getObjectValue("object-flag", new Value(true))); } - @SneakyThrows - @Test - void mapToStructureTest() { - Map map = new HashMap<>(); - map.put("String", "str"); - map.put("Boolean", true); - map.put("Integer", 1); - map.put("Double", 1.1); - map.put("List", Collections.singletonList(new Value(1))); - map.put("Value", new Value((true))); - map.put("Instant", Instant.ofEpochSecond(0)); - map.put("Map", new HashMap<>()); - ImmutableContext immutableContext = new ImmutableContext(); - map.put("ImmutableContext", immutableContext); - Structure res = mapToStructure(map); - assertEquals(new Value("str"), res.getValue("String")); - assertEquals(new Value(true), res.getValue("Boolean")); - assertEquals(new Value(1), res.getValue("Integer")); - assertEquals(new Value(1.1), res.getValue("Double")); - assertEquals(new Value(Collections.singletonList(new Value(1))), res.getValue("List")); - assertEquals(new Value(true), res.getValue("Value")); - assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant")); - assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap()); - assertEquals(new Value(immutableContext), res.getValue("ImmutableContext")); - } - } \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java index 619b7a6c..aa671370 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -18,56 +18,56 @@ public class TestFlagsUtils { /** * Building flags for testing purposes. - * @return + * @return map of flags */ public static Map> buildFlags() { Map> flags = new HashMap<>(); - flags.put("boolean-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("on", true) - .variant("off", false) - .defaultVariant("on") - .build()); - flags.put("string-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("greeting", "hi") - .variant("parting", "bye") - .defaultVariant("greeting") - .build()); - flags.put("integer-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", 1) - .variant("ten", 10) - .defaultVariant("ten") - .build()); - flags.put("float-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("tenth", 0.1) - .variant("half", 0.5) - .defaultVariant("half") - .build()); - flags.put("object-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("empty", new HashMap<>()) - .variant("template", new Value(mapToStructure(ImmutableMap.of( - "showImages", new Value(true), - "title", new Value("Check out these pics!"), - "imagesPerPage", new Value(100) - )))) - .defaultVariant("template") - .build()); - flags.put("context-aware", Flag.builder().state(Flag.State.ENABLED) - .variant("internal", "INTERNAL") - .variant("external", "EXTERNAL") - .defaultVariant("external") - .contextEvaluator((flag, evaluationContext) -> { - if (new Value(false).equals(evaluationContext.getValue("customer"))) { - return (String) flag.getVariants().get("internal"); - } else { - return (String) flag.getVariants().get(flag.getDefaultVariant()); - } - }) - .build()); - flags.put("wrong-flag", Flag.builder().state(Flag.State.ENABLED) - .variant("one", "uno") - .variant("two", "dos") - .defaultVariant("one") - .build()); + flags.put("boolean-flag", Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + flags.put("string-flag", Flag.builder() + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()); + flags.put("integer-flag", Flag.builder() + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()); + flags.put("float-flag", Flag.builder() + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()); + flags.put("object-flag", Flag.builder() + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()); + flags.put("context-aware", Flag.builder() + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()); + flags.put("wrong-flag", Flag.builder() + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()); return flags; } } From 071fa03faff30e6b95db5fcca9a30b84860c67c0 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 14 Aug 2023 18:38:09 +0300 Subject: [PATCH 09/11] minor updates Signed-off-by: liran2000 --- CONTRIBUTING.md | 5 +++++ .../dev/openfeature/sdk/MutableStructure.java | 15 --------------- src/main/java/dev/openfeature/sdk/Structure.java | 14 ++++++++++++++ src/main/java/dev/openfeature/sdk/Value.java | 2 +- .../openfeature/sdk/providers/memory/Flag.java | 2 +- .../sdk/providers/memory/InMemoryProvider.java | 2 +- .../java/dev/openfeature/sdk/StructureTest.java | 2 +- .../providers/memory/InMemoryProviderTest.java | 2 +- .../openfeature/sdk/testutils/TestFlagsUtils.java | 2 +- 9 files changed, 25 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf599995..7c0b0383 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,11 @@ If you're adding tests to cover something in the spec, use the `@Specification` The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. +to run alone: +``` +mvn test -P e2e-test +``` + ## Releasing See [releasing](./docs/release.md). diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index 204fb907..343fe2c9 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -10,8 +10,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static dev.openfeature.sdk.Value.objectToValue; - /** * {@link MutableStructure} represents a potentially nested object type which is used to represent * structured data. @@ -110,17 +108,4 @@ public Map asObjectMap() { e -> convertValue(getValue(e.getKey())) )); } - - /** - * Transform an object map to a {@link Structure} type. - * - * @param map map of objects - * @return a Structure object in the SDK format - */ - public static Structure mapToStructure(Map map) { - return new MutableStructure( - map.entrySet().stream() - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); - } } diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java index f8b55212..46274e70 100644 --- a/src/main/java/dev/openfeature/sdk/Structure.java +++ b/src/main/java/dev/openfeature/sdk/Structure.java @@ -9,6 +9,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static dev.openfeature.sdk.Value.objectToValue; + /** * {@link Structure} represents a potentially nested object type which is used to represent * structured data. @@ -123,4 +125,16 @@ default Map merge(Function map) { + return new MutableStructure(map.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); + } } diff --git a/src/main/java/dev/openfeature/sdk/Value.java b/src/main/java/dev/openfeature/sdk/Value.java index 2444f264..8be50179 100644 --- a/src/main/java/dev/openfeature/sdk/Value.java +++ b/src/main/java/dev/openfeature/sdk/Value.java @@ -10,7 +10,7 @@ import lombok.SneakyThrows; import lombok.ToString; -import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static dev.openfeature.sdk.Structure.mapToStructure; /** * Values serve as a generic return type for structure data from providers. diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java index c79f63bb..8cfe85c9 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -8,7 +8,7 @@ import java.util.Map; /** - * Flag representation. + * Flag representation for the in-memory provider. */ @ToString @Builder diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index 45567a03..939c28c0 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -52,7 +52,7 @@ public InMemoryProvider(Map> flags) { public void initialize(EvaluationContext evaluationContext) throws Exception { super.initialize(evaluationContext); state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.debug("finished initializing provider, state: {}", state); ProviderEventDetails details = ProviderEventDetails.builder() .message("provider is ready") .build(); diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java index 697aa169..8f188911 100644 --- a/src/test/java/dev/openfeature/sdk/StructureTest.java +++ b/src/test/java/dev/openfeature/sdk/StructureTest.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Map; -import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static dev.openfeature.sdk.Structure.mapToStructure; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index 8d9ae06c..3dcbc26b 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static dev.openfeature.sdk.Structure.mapToStructure; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java index aa671370..d9035929 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -8,7 +8,7 @@ import java.util.HashMap; import java.util.Map; -import static dev.openfeature.sdk.MutableStructure.mapToStructure; +import static dev.openfeature.sdk.Structure.mapToStructure; /** * Test flags utils. From 41987b83b0657e3376980bc3ab44dab610dd18a0 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 14 Aug 2023 20:45:22 +0300 Subject: [PATCH 10/11] events revisited Signed-off-by: liran2000 --- .../openfeature/sdk/providers/memory/InMemoryProvider.java | 4 ---- .../sdk/providers/memory/InMemoryProviderTest.java | 7 ++----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index 939c28c0..1006d88f 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -53,10 +53,6 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { super.initialize(evaluationContext); state = ProviderState.READY; log.debug("finished initializing provider, state: {}", state); - ProviderEventDetails details = ProviderEventDetails.builder() - .message("provider is ready") - .build(); - emitProviderReady(details); } /** diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index 3dcbc26b..b571b5bd 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -30,16 +30,14 @@ public class InMemoryProviderTest { static void beforeAll() { Map> flags = buildFlags(); InMemoryProvider provider = new InMemoryProvider(flags); + OpenFeatureAPI.getInstance().onProviderReady(eventDetails -> isProviderReady.set(true)); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> configurationChangedEventCount.incrementAndGet()); OpenFeatureAPI.getInstance().setProvider(provider); // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 Thread.sleep(500); client = OpenFeatureAPI.getInstance().getClient(); - - client.onProviderReady(eventDetails -> isProviderReady.set(true)); - - client.onProviderConfigurationChanged(eventDetails -> configurationChangedEventCount.incrementAndGet()); provider.updateFlags(flags); provider.updateFlag("addedFlag", Flag.builder() .variant("on", true) @@ -51,7 +49,6 @@ static void beforeAll() { @SneakyThrows @Test void eventsTest() { - assertTrue(isProviderReady.get()); assertEquals(2, configurationChangedEventCount.get()); } From 3cb8ee1c2d59237b506e957ddaf3374f919dd78d Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 14 Aug 2023 23:00:52 +0300 Subject: [PATCH 11/11] events test revisit verify emit event called on the spied provider as it verifies the config changed event was emitted, which is the in-memory provider implementation. The event itself is a functionality of the events framework, thus not needed to be tested here, Signed-off-by: liran2000 --- .../memory/InMemoryProviderTest.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index b571b5bd..f05a6b79 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -9,29 +9,28 @@ import org.junit.jupiter.api.Test; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import static dev.openfeature.sdk.Structure.mapToStructure; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; -public class InMemoryProviderTest { +class InMemoryProviderTest { private static Client client; - private static final AtomicBoolean isProviderReady = new AtomicBoolean(false); - - private static final AtomicInteger configurationChangedEventCount = new AtomicInteger(0); + private static InMemoryProvider provider; @SneakyThrows @BeforeAll static void beforeAll() { Map> flags = buildFlags(); - InMemoryProvider provider = new InMemoryProvider(flags); - OpenFeatureAPI.getInstance().onProviderReady(eventDetails -> isProviderReady.set(true)); - OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> configurationChangedEventCount.incrementAndGet()); + provider = spy(new InMemoryProvider(flags)); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> {}); OpenFeatureAPI.getInstance().setProvider(provider); // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 @@ -49,7 +48,7 @@ static void beforeAll() { @SneakyThrows @Test void eventsTest() { - assertEquals(2, configurationChangedEventCount.get()); + verify(provider, times(2)).emitProviderConfigurationChanged(any()); } @Test