diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ClientSideCondition.java b/src/main/java/com/octopus/openfeature/provider/v4/ClientSideCondition.java new file mode 100644 index 0000000..473a377 --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ClientSideCondition.java @@ -0,0 +1,33 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Base type for a client-side rule condition, selected from the camelCase {@code type} discriminator + * when deserializing a v4 evaluation response. These types model the wire shape only. + * + *

A discriminator this version of the provider does not recognise — or an absent one — + * deserializes to {@link UnknownCondition} rather than failing, so a condition type + * introduced by a newer server degrades safely on an older client. + * + *

The conditions sit alongside the rest of the v4 types rather than in a {@code conditions} + * sub-package as the .NET provider has them. Java package access is not hierarchical, so a + * package-private condition there would be invisible to {@link ClientSideRule} in this package, and + * following that layout would mean making every condition public — part of the library's supported + * API, which is what keeping these types package-private is meant to avoid. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + visible = true, + defaultImpl = UnknownCondition.class +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = PercentageByContextCondition.class, name = ConditionTypeNames.PERCENTAGE_BY_CONTEXT), + @JsonSubTypes.Type(value = ContextAttributeIsOneOfCondition.class, name = ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_ONE_OF), + @JsonSubTypes.Type(value = ContextAttributeIsNotOneOfCondition.class, name = ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_NOT_ONE_OF) +}) +abstract class ClientSideCondition { +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ClientSideRule.java b/src/main/java/com/octopus/openfeature/provider/v4/ClientSideRule.java new file mode 100644 index 0000000..4e2537d --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ClientSideRule.java @@ -0,0 +1,32 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * A named rule the provider library still has to evaluate on the client side. The rule matches when + * every one of its conditions matches. + */ +final class ClientSideRule { + private final String name; + private final List conditions; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ClientSideRule( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "conditions", required = true) List conditions + ) { + this.name = name; + this.conditions = List.copyOf(conditions); + } + + public String getName() { + return name; + } + + public List getConditions() { + return conditions; + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ConditionTypeNames.java b/src/main/java/com/octopus/openfeature/provider/v4/ConditionTypeNames.java new file mode 100644 index 0000000..f8f8cff --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ConditionTypeNames.java @@ -0,0 +1,14 @@ +package com.octopus.openfeature.provider.v4; + +/** + * Discriminator values for the polymorphic v4 client-side conditions. These mirror the values in the + * evaluation response, so they must not drift from the server. + */ +final class ConditionTypeNames { + static final String CONTEXT_ATTRIBUTE_IS_NOT_ONE_OF = "context-attribute-is-not-one-of"; + static final String CONTEXT_ATTRIBUTE_IS_ONE_OF = "context-attribute-is-one-of"; + static final String PERCENTAGE_BY_CONTEXT = "percentage-by-context"; + + private ConditionTypeNames() { + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsNotOneOfCondition.java b/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsNotOneOfCondition.java new file mode 100644 index 0000000..12f9dae --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsNotOneOfCondition.java @@ -0,0 +1,33 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Matches when the context attribute {@code key} is not one of {@code values}. + */ +@JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. +final class ContextAttributeIsNotOneOfCondition extends ClientSideCondition { + private final String key; + private final List values; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ContextAttributeIsNotOneOfCondition( + @JsonProperty(value = "key", required = true) String key, + @JsonProperty(value = "values", required = true) List values + ) { + this.key = key; + this.values = List.copyOf(values); + } + + public String getKey() { + return key; + } + + public List getValues() { + return values; + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsOneOfCondition.java b/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsOneOfCondition.java new file mode 100644 index 0000000..dbc8e10 --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ContextAttributeIsOneOfCondition.java @@ -0,0 +1,33 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Matches when the context attribute {@code key} is one of {@code values}. + */ +@JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. +final class ContextAttributeIsOneOfCondition extends ClientSideCondition { + private final String key; + private final List values; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ContextAttributeIsOneOfCondition( + @JsonProperty(value = "key", required = true) String key, + @JsonProperty(value = "values", required = true) List values + ) { + this.key = key; + this.values = List.copyOf(values); + } + + public String getKey() { + return key; + } + + public List getValues() { + return values; + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/PercentageByContextCondition.java b/src/main/java/com/octopus/openfeature/provider/v4/PercentageByContextCondition.java new file mode 100644 index 0000000..0194643 --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/PercentageByContextCondition.java @@ -0,0 +1,24 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Matches when the OpenFeature targeting key falls within the {@code percentage}% rollout. + */ +@JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. +final class PercentageByContextCondition extends ClientSideCondition { + private final int percentage; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + PercentageByContextCondition( + @JsonProperty(value = "percentage", required = true) int percentage + ) { + this.percentage = percentage; + } + + public int getPercentage() { + return percentage; + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/ServerSideEvaluation.java b/src/main/java/com/octopus/openfeature/provider/v4/ServerSideEvaluation.java new file mode 100644 index 0000000..c9c0028 --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/ServerSideEvaluation.java @@ -0,0 +1,62 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Optional; + +/** + * A single feature flag as returned by the OctoToggle v4 evaluations endpoint. The endpoint returns + * an array of these. + * + *

A flag is returned in one of two shapes: + *

+ * Properties that do not apply to the returned shape are omitted from the JSON. + */ +final class ServerSideEvaluation { + private final String slug; + private final Boolean value; + private final String reason; + private final String evaluationKey; + private final List rules; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + ServerSideEvaluation( + @JsonProperty(value = "slug", required = true) String slug, + @JsonProperty("value") Boolean value, + @JsonProperty("reason") String reason, + @JsonProperty("evaluationKey") String evaluationKey, + @JsonProperty("rules") List rules + ) { + this.slug = slug; + this.value = value; + this.reason = reason; + this.evaluationKey = evaluationKey; + this.rules = rules == null ? null : List.copyOf(rules); + } + + public String getSlug() { + return slug; + } + + public Optional getValue() { + return Optional.ofNullable(value); + } + + public Optional getReason() { + return Optional.ofNullable(reason); + } + + public Optional getEvaluationKey() { + return Optional.ofNullable(evaluationKey); + } + + public Optional> getRules() { + return Optional.ofNullable(rules); + } +} diff --git a/src/main/java/com/octopus/openfeature/provider/v4/UnknownCondition.java b/src/main/java/com/octopus/openfeature/provider/v4/UnknownCondition.java new file mode 100644 index 0000000..a62aab4 --- /dev/null +++ b/src/main/java/com/octopus/openfeature/provider/v4/UnknownCondition.java @@ -0,0 +1,33 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Optional; + +/** + * A client-side condition whose {@code type} discriminator this version of the provider does not + * recognise, or which carried no discriminator at all. Rather than failing the whole evaluation + * response, an unrecognised condition is preserved as this type. It always evaluates to + * {@code false}, so a rule containing an unknown condition can never match — a newer server + * capability is safely treated as "not met" by an older client. + * + *

The raw payload of an unknown condition is not retained, only its discriminator. + */ +final class UnknownCondition extends ClientSideCondition { + private final String type; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + UnknownCondition( + @JsonProperty("type") String type + ) { + this.type = type; + } + + /** + * The unrecognised discriminator value, or empty if none was present. + */ + public Optional getType() { + return Optional.ofNullable(type); + } +} diff --git a/src/test/java/com/octopus/openfeature/provider/TestObjectMapper.java b/src/test/java/com/octopus/openfeature/provider/TestObjectMapper.java new file mode 100644 index 0000000..0a34a97 --- /dev/null +++ b/src/test/java/com/octopus/openfeature/provider/TestObjectMapper.java @@ -0,0 +1,17 @@ +package com.octopus.openfeature.provider; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Exposes the provider's package-private {@link OctopusObjectMapper} to tests in other packages, so + * they can exercise deserialization with the same mapper the client uses in production rather than + * a re-declared copy that could drift from it. + * + *

Test-only: this lives in the test sources and is never published. + */ +public final class TestObjectMapper { + public static final ObjectMapper INSTANCE = OctopusObjectMapper.INSTANCE; + + private TestObjectMapper() { + } +} diff --git a/src/test/java/com/octopus/openfeature/provider/v4/ServerSideEvaluationDeserializationTests.java b/src/test/java/com/octopus/openfeature/provider/v4/ServerSideEvaluationDeserializationTests.java new file mode 100644 index 0000000..b93caba --- /dev/null +++ b/src/test/java/com/octopus/openfeature/provider/v4/ServerSideEvaluationDeserializationTests.java @@ -0,0 +1,201 @@ +package com.octopus.openfeature.provider.v4; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.octopus.openfeature.provider.TestObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Exercises polymorphic JSON deserialization of the v4 evaluation response. Everything is + * deserialized with the provider's own {@code OctopusObjectMapper} — the same mapper the client + * uses in production — so discriminator matching, property binding and the absent-property + * behaviour are all covered end to end. + */ +class ServerSideEvaluationDeserializationTests { + + private final ObjectMapper objectMapper = TestObjectMapper.INSTANCE; + + private InputStream resource(String name) { + return getClass().getResourceAsStream(name); + } + + @Test + void shouldDeserializePercentageByContextConditionToConcreteType() throws Exception { + var condition = objectMapper.readValue( + resource("condition-percentage-by-context.json"), ClientSideCondition.class); + + assertThat(condition) + .isInstanceOfSatisfying(PercentageByContextCondition.class, + percentage -> assertThat(percentage.getPercentage()).isEqualTo(50)); + } + + @Test + void shouldDeserializeContextAttributeIsOneOfConditionToConcreteType() throws Exception { + var condition = objectMapper.readValue( + resource("condition-context-attribute-is-one-of.json"), ClientSideCondition.class); + + assertThat(condition) + .isInstanceOfSatisfying(ContextAttributeIsOneOfCondition.class, isOneOf -> { + assertThat(isOneOf.getKey()).isEqualTo("user-id"); + assertThat(isOneOf.getValues()).containsExactly("1234", "5678"); + }); + } + + @Test + void shouldDeserializeContextAttributeIsNotOneOfConditionToConcreteType() throws Exception { + var condition = objectMapper.readValue( + resource("condition-context-attribute-is-not-one-of.json"), ClientSideCondition.class); + + assertThat(condition) + .isInstanceOfSatisfying(ContextAttributeIsNotOneOfCondition.class, isNotOneOf -> { + assertThat(isNotOneOf.getKey()).isEqualTo("region"); + assertThat(isNotOneOf.getValues()).containsExactly("us", "eu"); + }); + } + + @Test + void shouldDeserializeMixedConditionListToConcreteTypes() throws Exception { + var conditions = objectMapper.readValue( + resource("condition-list-mixed.json"), + new TypeReference>() {} + ); + + assertThat(conditions).hasExactlyElementsOfTypes( + PercentageByContextCondition.class, + ContextAttributeIsOneOfCondition.class, + ContextAttributeIsNotOneOfCondition.class + ); + } + + @Test + void shouldDeserializeUnknownConditionTypeToUnknownConditionInsteadOfThrowing() throws Exception { + var condition = objectMapper.readValue( + resource("condition-unknown-type.json"), ClientSideCondition.class); + + assertThat(condition) + .isInstanceOfSatisfying(UnknownCondition.class, + unknown -> assertThat(unknown.getType()).hasValue("not-a-real-condition")); + } + + @Test + void shouldDeserializeConditionWithoutTypeDiscriminatorToUnknownCondition() throws Exception { + var condition = objectMapper.readValue( + resource("condition-missing-type.json"), ClientSideCondition.class); + + assertThat(condition) + .isInstanceOfSatisfying(UnknownCondition.class, + unknown -> assertThat(unknown.getType()).isEmpty()); + } + + @Test + void shouldPreserveUnknownConditionAlongsideKnownConditionsWithoutFailingTheResponse() throws Exception { + var evaluation = objectMapper.readValue( + resource("evaluation-with-unknown-condition.json"), ServerSideEvaluation.class); + + var conditions = evaluation.getRules().orElseThrow().get(0).getConditions(); + + assertThat(conditions.get(0)).isInstanceOf(PercentageByContextCondition.class); + assertThat(conditions.get(1)) + .isInstanceOfSatisfying(UnknownCondition.class, + unknown -> assertThat(unknown.getType()).hasValue("some-future-condition")); + } + + @Test + void shouldDeserializeServerResolvedEvaluation() throws Exception { + var evaluation = objectMapper.readValue( + resource("evaluation-server-resolved.json"), ServerSideEvaluation.class); + + assertThat(evaluation.getSlug()).isEqualTo("my-feature"); + assertThat(evaluation.getValue()).hasValue(true); + assertThat(evaluation.getReason()).hasValue("The flag is enabled for this environment."); + assertThat(evaluation.getEvaluationKey()).isEmpty(); + assertThat(evaluation.getRules()).isEmpty(); + } + + @Test + void shouldDeserializeEvaluationDeferredToTheClientWithPolymorphicConditions() throws Exception { + var evaluation = objectMapper.readValue( + resource("evaluation-deferred-to-client.json"), ServerSideEvaluation.class); + + assertThat(evaluation.getSlug()).isEqualTo("my-feature"); + assertThat(evaluation.getEvaluationKey()).hasValue("0f8fad5b-d9cb-469f-a165-70867728950e"); + assertThat(evaluation.getValue()).isEmpty(); + assertThat(evaluation.getReason()).isEmpty(); + + var rules = evaluation.getRules().orElseThrow(); + assertThat(rules).hasSize(1); + + var rule = rules.get(0); + assertThat(rule.getName()).isEqualTo("Rule 1"); + assertThat(rule.getConditions()).hasSize(2); + + assertThat(rule.getConditions().get(0)) + .isInstanceOfSatisfying(PercentageByContextCondition.class, + percentage -> assertThat(percentage.getPercentage()).isEqualTo(50)); + assertThat(rule.getConditions().get(1)) + .isInstanceOfSatisfying(ContextAttributeIsOneOfCondition.class, isOneOf -> { + assertThat(isOneOf.getKey()).isEqualTo("user-id"); + assertThat(isOneOf.getValues()).containsExactly("1234", "5678"); + }); + } + + @Test + void shouldDeserializeEvaluationsResponseAsListOfEvaluations() throws Exception { + var evaluations = objectMapper.readValue( + resource("evaluation-list.json"), + new TypeReference>() {} + ); + + assertThat(evaluations).hasSize(2); + + assertThat(evaluations.get(0).getSlug()).isEqualTo("resolved-feature"); + assertThat(evaluations.get(0).getValue()).hasValue(false); + assertThat(evaluations.get(0).getRules()).isEmpty(); + + assertThat(evaluations.get(1).getSlug()).isEqualTo("deferred-feature"); + assertThat(evaluations.get(1).getValue()).isEmpty(); + + var rules = evaluations.get(1).getRules().orElseThrow(); + assertThat(rules).hasSize(1); + assertThat(rules.get(0).getConditions().get(0)).isInstanceOf(PercentageByContextCondition.class); + } + + @Test + void shouldFailDeserializationWhenSlugIsMissing() { + assertThatThrownBy(() -> objectMapper.readValue( + resource("evaluation-missing-slug.json"), ServerSideEvaluation.class)) + .isInstanceOf(MismatchedInputException.class); + } + + @Test + void shouldIgnoreExtraneousProperties() throws Exception { + var evaluation = objectMapper.readValue( + resource("evaluation-with-extraneous-properties.json"), ServerSideEvaluation.class); + + assertThat(evaluation.getSlug()).isEqualTo("my-feature"); + + var conditions = evaluation.getRules().orElseThrow().get(0).getConditions(); + assertThat(conditions.get(0)) + .isInstanceOfSatisfying(PercentageByContextCondition.class, + percentage -> assertThat(percentage.getPercentage()).isEqualTo(50)); + } + + @Test + void shouldExposeConditionsAsImmutableLists() throws Exception { + var evaluation = objectMapper.readValue( + resource("evaluation-deferred-to-client.json"), ServerSideEvaluation.class); + + var rules = evaluation.getRules().orElseThrow(); + + assertThatThrownBy(() -> rules.clear()).isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> rules.get(0).getConditions().clear()) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-not-one-of.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-not-one-of.json new file mode 100644 index 0000000..5e68d06 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-not-one-of.json @@ -0,0 +1,8 @@ +{ + "type": "context-attribute-is-not-one-of", + "key": "region", + "values": [ + "us", + "eu" + ] +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-one-of.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-one-of.json new file mode 100644 index 0000000..92e2020 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-context-attribute-is-one-of.json @@ -0,0 +1,8 @@ +{ + "type": "context-attribute-is-one-of", + "key": "user-id", + "values": [ + "1234", + "5678" + ] +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-list-mixed.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-list-mixed.json new file mode 100644 index 0000000..2c04429 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-list-mixed.json @@ -0,0 +1,20 @@ +[ + { + "type": "percentage-by-context", + "percentage": 25 + }, + { + "type": "context-attribute-is-one-of", + "key": "user-id", + "values": [ + "1234" + ] + }, + { + "type": "context-attribute-is-not-one-of", + "key": "region", + "values": [ + "au" + ] + } +] diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-missing-type.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-missing-type.json new file mode 100644 index 0000000..42d7dfb --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-missing-type.json @@ -0,0 +1,3 @@ +{ + "percentage": 50 +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-percentage-by-context.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-percentage-by-context.json new file mode 100644 index 0000000..b29f47a --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-percentage-by-context.json @@ -0,0 +1,4 @@ +{ + "type": "percentage-by-context", + "percentage": 50 +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/condition-unknown-type.json b/src/test/resources/com/octopus/openfeature/provider/v4/condition-unknown-type.json new file mode 100644 index 0000000..0ce3b94 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/condition-unknown-type.json @@ -0,0 +1,4 @@ +{ + "type": "not-a-real-condition", + "percentage": 50 +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-deferred-to-client.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-deferred-to-client.json new file mode 100644 index 0000000..0adf650 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-deferred-to-client.json @@ -0,0 +1,23 @@ +{ + "slug": "my-feature", + "evaluationKey": "0f8fad5b-d9cb-469f-a165-70867728950e", + "rules": [ + { + "name": "Rule 1", + "conditions": [ + { + "type": "percentage-by-context", + "percentage": 50 + }, + { + "type": "context-attribute-is-one-of", + "key": "user-id", + "values": [ + "1234", + "5678" + ] + } + ] + } + ] +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-list.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-list.json new file mode 100644 index 0000000..d097b57 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-list.json @@ -0,0 +1,22 @@ +[ + { + "slug": "resolved-feature", + "value": false, + "reason": "The flag is disabled for this environment." + }, + { + "slug": "deferred-feature", + "evaluationKey": "0f8fad5b-d9cb-469f-a165-70867728950e", + "rules": [ + { + "name": "Rule 1", + "conditions": [ + { + "type": "percentage-by-context", + "percentage": 10 + } + ] + } + ] + } +] diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-missing-slug.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-missing-slug.json new file mode 100644 index 0000000..4ad3971 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-missing-slug.json @@ -0,0 +1,4 @@ +{ + "value": true, + "reason": "The flag is enabled for this environment." +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-server-resolved.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-server-resolved.json new file mode 100644 index 0000000..d8bc564 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-server-resolved.json @@ -0,0 +1,5 @@ +{ + "slug": "my-feature", + "value": true, + "reason": "The flag is enabled for this environment." +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-extraneous-properties.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-extraneous-properties.json new file mode 100644 index 0000000..93d7d64 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-extraneous-properties.json @@ -0,0 +1,22 @@ +{ + "slug": "my-feature", + "evaluationKey": "0f8fad5b-d9cb-469f-a165-70867728950e", + "rules": [ + { + "name": "Rule 1", + "extra": "rule-data", + "conditions": [ + { + "type": "percentage-by-context", + "percentage": 50, + "extra": "condition-data" + } + ] + } + ], + "foo": "bar", + "qux": 123, + "wux": { + "nested": "value" + } +} diff --git a/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-unknown-condition.json b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-unknown-condition.json new file mode 100644 index 0000000..be6a3b9 --- /dev/null +++ b/src/test/resources/com/octopus/openfeature/provider/v4/evaluation-with-unknown-condition.json @@ -0,0 +1,19 @@ +{ + "slug": "my-feature", + "evaluationKey": "0f8fad5b-d9cb-469f-a165-70867728950e", + "rules": [ + { + "name": "Rule 1", + "conditions": [ + { + "type": "percentage-by-context", + "percentage": 50 + }, + { + "type": "some-future-condition", + "someField": "someValue" + } + ] + } + ] +}