diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index 150f9c171..dc3a22b49 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -87,6 +87,9 @@ public String getTargetingKey() { @Override public EvaluationContext merge(EvaluationContext overridingContext) { if (overridingContext == null || overridingContext.isEmpty()) { + if (this.isEmpty()) { + return ImmutableContext.EMPTY; + } return new ImmutableContext(this.asUnmodifiableMap()); } if (this.isEmpty()) { diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java index 7d8210be4..bb049a76d 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk; import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -216,6 +217,69 @@ void immutableContextHashCodeIsStable() { assertEquals(first, second); } + @Nested + @DisplayName("ImmutableContext(String, Map) branch logic") + class ConstructorBranches { + + @Test + @DisplayName("non-null targeting key with empty attributes is preserved") + void nonNullTargetingKeyWithEmptyAttributesIsPreserved() { + ImmutableContext ctx = new ImmutableContext("key", Collections.emptyMap()); + assertThat(ctx.getTargetingKey()).isEqualTo("key"); + assertThat(ctx.keySet()).contains(TARGETING_KEY); + } + + @Test + @DisplayName("non-null targeting key with null attributes is preserved") + void nonNullTargetingKeyWithNullAttributesIsPreserved() { + ImmutableContext ctx = new ImmutableContext("key", null); + assertThat(ctx.getTargetingKey()).isEqualTo("key"); + } + + @Test + @DisplayName("null targeting key with empty attributes yields empty context") + void nullTargetingKeyWithEmptyAttributesYieldsEmptyContext() { + ImmutableContext ctx = new ImmutableContext((String) null, Collections.emptyMap()); + assertThat(ctx.getTargetingKey()).isNull(); + assertThat(ctx.isEmpty()).isTrue(); + } + } + + @Nested + @DisplayName("ImmutableContext.merge() empty short-circuit") + class MergeEmpty { + + @Test + @DisplayName("merging two empty contexts returns the EMPTY singleton") + void mergingTwoEmptyContextsReturnsEmptySingleton() { + EvaluationContext result = new ImmutableContext().merge(new ImmutableContext()); + assertThat(result).isSameAs(ImmutableContext.EMPTY); + } + + @Test + @DisplayName("merging empty context with null returns the EMPTY singleton") + void mergingEmptyContextWithNullReturnsEmptySingleton() { + EvaluationContext result = new ImmutableContext().merge(null); + assertThat(result).isSameAs(ImmutableContext.EMPTY); + } + + @Test + @DisplayName("merging non-empty context with null does not return EMPTY") + void mergingNonEmptyContextWithNullDoesNotReturnEmpty() { + EvaluationContext result = new ImmutableContext("key").merge(null); + assertThat(result).isNotSameAs(ImmutableContext.EMPTY); + assertThat(result.getTargetingKey()).isEqualTo("key"); + } + + @Test + @DisplayName("merging non-empty context with empty override does not return EMPTY") + void mergingNonEmptyContextWithEmptyOverrideDoesNotReturnEmpty() { + EvaluationContext result = new ImmutableContext("key").merge(new ImmutableContext()); + assertThat(result).isNotSameAs(ImmutableContext.EMPTY); + assertThat(result.getTargetingKey()).isEqualTo("key"); + } + } + @Nested class Equals { ImmutableContext ctx = new ImmutableContext("c", Map.of("a", new Value("b")));