diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 1ebcb4b65..364ef65ae 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 63c9d533e..7c0b03834 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,14 +18,9 @@ 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`. -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 +to run alone: ``` mvn test -P e2e-test ``` diff --git a/pom.xml b/pom.xml index 098d6eb0a..09c52f91e 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 @@ -139,39 +137,33 @@ test - - dev.openfeature.contrib.providers - flagd - 0.5.10 - test - - org.awaitility awaitility 4.2.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 +195,7 @@ - + maven-dependency-plugin 3.6.0 @@ -249,7 +241,7 @@ ${testExclusions} - + @@ -271,7 +263,7 @@ - prepare-agent + prepare-agent prepare-agent @@ -319,7 +311,7 @@ - + @@ -496,14 +488,11 @@ - - - e2e-test - + diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index 78e04c718..b324c07cb 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/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java index f8b552125..46274e70e 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 672b65c1a..8be50179f 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.Structure.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/ContextEvaluator.java b/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java new file mode 100644 index 000000000..02fa323c2 --- /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 000000000..8cfe85c93 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -0,0 +1,21 @@ +package dev.openfeature.sdk.providers.memory; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import lombok.ToString; + +import java.util.Map; + +/** + * Flag representation for the in-memory provider. + */ +@ToString +@Builder +@Getter +public class Flag { + @Singular + private Map variants; + private String defaultVariant; + private ContextEvaluator contextEvaluator; +} 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 000000000..1006d88fb --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -0,0 +1,162 @@ +package dev.openfeature.sdk.providers.memory; + +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 lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +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. + */ +@Slf4j +public class InMemoryProvider extends EventProvider { + + @Getter + private static final String NAME = "InMemoryProvider"; + + private Map> flags; + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + @Override + public Metadata getMetadata() { + return () -> NAME; + } + + public InMemoryProvider(Map> flags) { + this.flags = new HashMap<>(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.debug("finished initializing provider, state: {}", state); + } + + /** + * Updating provider flags configuration, replacing existing flags. + * @param flags the flags to use instead of the previous flags. + */ + public void updateFlags(Map> flags) { + Set flagsChanged = new HashSet<>(); + 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") + .build(); + 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) { + 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.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(); + } + +} diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java index f05f93023..8f1889114 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.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; +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/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 7048fc0b8..650fa242b 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,22 +1,25 @@ package dev.openfeature.sdk.e2e; -import dev.openfeature.contrib.providers.flagd.FlagdProvider; -import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.Value; 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.Client; +import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Structure; -import dev.openfeature.sdk.Value; +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; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import lombok.SneakyThrows; import java.util.HashMap; import java.util.Map; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,13 +50,17 @@ 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> flags = buildFlags(); + 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(); } @@ -265,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 @@ -286,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/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java new file mode 100644 index 000000000..f05a6b79f --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -0,0 +1,84 @@ +package dev.openfeature.sdk.providers.memory; + +import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +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; + +class InMemoryProviderTest { + + private static Client client; + + private static InMemoryProvider provider; + + @SneakyThrows + @BeforeAll + static void beforeAll() { + Map> flags = buildFlags(); + 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 + Thread.sleep(500); + + client = OpenFeatureAPI.getInstance().getClient(); + provider.updateFlags(flags); + provider.updateFlag("addedFlag", Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + } + + @SneakyThrows + @Test + void eventsTest() { + verify(provider, times(2)).emitProviderConfigurationChanged(any()); + } + + @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))); + } + +} \ 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 new file mode 100644 index 000000000..d90359294 --- /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.Structure.mapToStructure; + +/** + * Test flags utils. + */ +@UtilityClass +public class TestFlagsUtils { + + /** + * Building flags for testing purposes. + * @return map of flags + */ + public static Map> buildFlags() { + Map> flags = new HashMap<>(); + 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; + } +}