Skip to content
3 changes: 2 additions & 1 deletion src/main/java/dev/openfeature/sdk/ImmutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ public ImmutableContext(Map<String, Value> attributes) {

/**
* Create an immutable context with given targetingKey and attributes provided.
* Empty string is a valid targeting key value.
*
* @param targetingKey targeting key
* @param attributes evaluation context attributes
*/
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
if (targetingKey != null && !targetingKey.isBlank()) {
if (targetingKey != null) {
this.structure = new ImmutableStructure(targetingKey, attributes);
} else {
this.structure = new ImmutableStructure(attributes);
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/dev/openfeature/sdk/MutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public MutableContext(Map<String, Value> attributes) {

/**
* Create a mutable context with given targetingKey and attributes provided. TargetingKey should be non-null
* and non-empty to be accepted.
* to be accepted. Empty string is a valid targeting key value.
*
* @param targetingKey targeting key
* @param attributes evaluation context attributes
*/
public MutableContext(String targetingKey, Map<String, Value> attributes) {
this.structure = new MutableStructure(new HashMap<>(attributes));
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
if (targetingKey != null) {
this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey));
}
}
Expand Down Expand Up @@ -85,10 +85,11 @@ public MutableContext add(String key, List<Value> value) {
}

/**
* Override or set targeting key for this mutable context. Value should be non-null and non-empty to be accepted.
* Override or set targeting key for this mutable context. Value should be non-null to be accepted.
* Empty string is a valid targeting key value.
*/
public MutableContext setTargetingKey(String targetingKey) {
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
if (targetingKey != null) {
this.add(TARGETING_KEY, targetingKey);
}
return this;
Expand Down
6 changes: 4 additions & 2 deletions src/test/java/dev/openfeature/sdk/EvalContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ void Immutable_context_merge_targeting_key() {
ctxMerged = ctx1.merge(ctx2);
assertEquals(key2, ctxMerged.getTargetingKey());

// Whitespace is now a valid targeting key and should override
ctx2 = new ImmutableContext(" ", new HashMap<>());
ctxMerged = ctx1.merge(ctx2);
assertEquals(key1, ctxMerged.getTargetingKey());
assertEquals(" ", ctxMerged.getTargetingKey());
}

@Test
Expand All @@ -200,9 +201,10 @@ void merge_targeting_key() {
ctxMerged = ctx1.merge(ctx2);
assertEquals(key2, ctxMerged.getTargetingKey());

// Whitespace is now a valid targeting key and should override
ctx2.setTargetingKey(" ");
ctxMerged = ctx1.merge(ctx2);
assertEquals(key2, ctxMerged.getTargetingKey());
assertEquals(" ", ctxMerged.getTargetingKey());
}

@Test
Expand Down
28 changes: 25 additions & 3 deletions src/test/java/dev/openfeature/sdk/ImmutableContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,17 @@ void shouldChangeTargetingKeyFromOverridingContext() {
assertEquals("overriding_key", merge.getTargetingKey());
}

@DisplayName("targeting key should not be changed from the overriding context if missing")
@DisplayName("targeting key should be changed from the overriding context even if empty string")
@Test
void shouldRetainTargetingKeyWhenOverridingContextTargetingKeyValueIsEmpty() {
void shouldOverrideTargetingKeyWhenOverridingContextTargetingKeyIsEmptyString() {
HashMap<String, Value> attributes = new HashMap<>();
attributes.put("key1", new Value("val1"));
attributes.put("key2", new Value("val2"));
EvaluationContext ctx = new ImmutableContext("targeting_key", attributes);
EvaluationContext overriding = new ImmutableContext("");
EvaluationContext merge = ctx.merge(overriding);
assertEquals("targeting_key", merge.getTargetingKey());
// Empty string is a valid targeting key and should override
assertEquals("", merge.getTargetingKey());
}

@DisplayName("missing targeting key should return null")
Expand All @@ -73,6 +74,27 @@ void missingTargetingKeyShould() {
assertNull(ctx.getTargetingKey());
}

@DisplayName("null targeting key in constructor should result in no targeting key")
@Test
void nullTargetingKeyInConstructorShouldResultInNoTargetingKey() {
EvaluationContext ctx = new ImmutableContext((String) null);
assertNull(ctx.getTargetingKey());
}

@DisplayName("empty string is a valid targeting key")
@Test
void emptyStringIsValidTargetingKey() {
EvaluationContext ctx = new ImmutableContext("");
assertEquals("", ctx.getTargetingKey());
}

@DisplayName("whitespace-only string is a valid targeting key")
@Test
void whitespaceOnlyStringIsValidTargetingKey() {
EvaluationContext ctx = new ImmutableContext(" ");
assertEquals(" ", ctx.getTargetingKey());
}

@DisplayName("Merge should retain all the attributes from the existing context when overriding context is null")
@Test
void mergeShouldReturnAllTheValuesFromTheContextWhenOverridingContextIsNull() {
Expand Down
42 changes: 39 additions & 3 deletions src/test/java/dev/openfeature/sdk/MutableContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ void shouldChangeTargetingKeyFromOverridingContext() {
assertEquals("overriding_key", merge.getTargetingKey());
}

@DisplayName("targeting key should not changed from the overriding context if missing")
@DisplayName("targeting key should be changed from the overriding context even if empty string")
@Test
void shouldRetainTargetingKeyWhenOverridingContextTargetingKeyValueIsEmpty() {
void shouldOverrideTargetingKeyWhenOverridingContextTargetingKeyIsEmptyString() {
HashMap<String, Value> attributes = new HashMap<>();
attributes.put("key1", new Value("val1"));
attributes.put("key2", new Value("val2"));
EvaluationContext ctx = new MutableContext("targeting_key", attributes);
EvaluationContext overriding = new MutableContext("");
EvaluationContext merge = ctx.merge(overriding);
assertEquals("targeting_key", merge.getTargetingKey());
// Empty string is a valid targeting key and should override
assertEquals("", merge.getTargetingKey());
}

@DisplayName("missing targeting key should return null")
Expand All @@ -59,6 +60,41 @@ void missingTargetingKeyShould() {
assertEquals(null, ctx.getTargetingKey());
}

@DisplayName("setTargetingKey with null should not set targeting key")
@Test
void setTargetingKeyWithNullShouldNotSet() {
MutableContext ctx = new MutableContext("original");
ctx.setTargetingKey(null);
// null should not override the existing targeting key
assertEquals("original", ctx.getTargetingKey());
}

@DisplayName("empty string is a valid targeting key via constructor")
@Test
void emptyStringIsValidTargetingKeyViaConstructor() {
EvaluationContext ctx = new MutableContext("");
assertEquals("", ctx.getTargetingKey());
}

@DisplayName("empty and whitespace-only strings are valid targeting keys via setter")
@Test
void emptyAndWhitespaceAreValidTargetingKeysViaSetter() {
MutableContext ctx = new MutableContext();

ctx.setTargetingKey("");
assertEquals("", ctx.getTargetingKey());

ctx.setTargetingKey(" ");
assertEquals(" ", ctx.getTargetingKey());
}

@DisplayName("whitespace-only string is a valid targeting key via constructor")
@Test
void whitespaceOnlyStringIsValidTargetingKeyViaConstructor() {
EvaluationContext ctx = new MutableContext(" ");
assertEquals(" ", ctx.getTargetingKey());
}

@DisplayName("Merge should retain all the attributes from the existing context when overriding context is null")
@Test
void mergeShouldReturnAllTheValuesFromTheContextWhenOverridingContextIsNull() {
Expand Down
Loading