Repository: open-feature/java-sdk
Pull request #849 (2024-03-17 07:54:51)

fix: missing targeting key should return null
Problem:
getTargetingKey of missing targeting key causing NullPointerException.
Solution:
getTargetingKey of missing targeting key returns null.

failing _java-sdk-contrib_ [CI](https://github.com/open-feature/java-sdk-contrib/actions/runs/8314110946/job/22750876680?pr=722):
```
Error:  dev.openfeature.contrib.providers.flagd.resolver.process.targeting.OperatorTest.timestampPresent -- Time elapsed: 0.006 s <<< ERROR!
java.lang.NullPointerException
	at dev.openfeature.sdk.ImmutableContext.getTargetingKey(ImmutableContext.java:65)
	at dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator.apply(Operator.java:52)
```


When adopting #805, saw an issue when [checking missing targeting](https://github.com/open-feature/java-sdk-contrib/blob/main/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java#L25) key when it is required, causing the check to not work properly:
```
if (ctx.getTargetingKey() == null) {
    throw new TargetingKeyMissingError("targeting key is required.");
}
```

Related [test](https://github.com/open-feature/java-sdk-contrib/pull/722/files#diff-8c58451457555d525c75ac688495b1b2543885582f14a6d8e1a43bbd2cf845e6R122) check can be updated after this fix.


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
Attention: Patch coverage is `75.00000%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 95.28%. Comparing base [(`a763740`)](https://app.codecov.io/gh/open-feature/java-sdk/commit/a763740e6b45b0d51c219675cd9e1839def61b8e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) to head [(`2ad71d1`)](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).

| [Files](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Patch % | Lines |
|---|---|---|
| [.../main/java/dev/openfeature/sdk/MutableContext.java](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvc2RrL011dGFibGVDb250ZXh0LmphdmE=) | 50.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main     #849      +/-   ##
============================================
+ Coverage     95.15%   95.28%   +0.12%     
- Complexity      367      369       +2     
============================================
  Files            34       34              
  Lines           847      849       +2     
  Branches         50       52       +2     
============================================
+ Hits            806      809       +3     
+ Misses           22       21       -1     
  Partials         19       19              
```

| [Flag](https://app.codecov.io/gh/open-feature/java-sdk/pull/849/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/open-feature/java-sdk/pull/849/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | `95.28% <75.00%> (+0.12%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/open-feature/java-sdk/pull/849?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849)


Kavindu-Dodan: @liran2000  thanks a lot for the NPE fix :) 
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=849&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=849&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=849)



Patch:
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
index 632448a..67e3562 100644
--- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
@@ -62,7 +62,8 @@ public final class ImmutableContext implements EvaluationContext {
      */
     @Override
     public String getTargetingKey() {
-        return this.getValue(TARGETING_KEY).asString();
+        Value value = this.getValue(TARGETING_KEY);
+        return value == null ? null : value.asString();
     }
 
     /**
diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java
index 69c22b8..42b9ccb 100644
--- a/src/main/java/dev/openfeature/sdk/MutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/MutableContext.java
@@ -99,7 +99,8 @@ public class MutableContext implements EvaluationContext {
      */
     @Override
     public String getTargetingKey() {
-        return this.getValue(TARGETING_KEY).asString();
+        Value value = this.getValue(TARGETING_KEY);
+        return value == null ? null : value.asString();
     }
 
     /**


Test patch:
diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java
index 7cfedec..c3847b9 100644
--- a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java
+++ b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java
@@ -47,6 +47,12 @@ class ImmutableContextTest {
         EvaluationContext merge = ctx.merge(overriding);
         assertEquals("targeting_key", merge.getTargetingKey());
     }
+    @DisplayName("missing targeting key should return null")
+    @Test
+    void missingTargetingKeyShould() {
+        EvaluationContext ctx = new ImmutableContext();
+        assertEquals(null, ctx.getTargetingKey());
+    }
 
     @DisplayName("Merge should retain all the attributes from the existing context when overriding context is null")
     @Test
diff --git a/src/test/java/dev/openfeature/sdk/MutableContextTest.java b/src/test/java/dev/openfeature/sdk/MutableContextTest.java
new file mode 100644
index 0000000..655cc85
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/MutableContextTest.java
@@ -0,0 +1,105 @@
+package dev.openfeature.sdk;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+
+import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MutableContextTest {
+
+    @DisplayName("targeting key should be changed from the overriding context")
+    @Test
+    void shouldChangeTargetingKeyFromOverridingContext() {
+        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("overriding_key");
+        EvaluationContext merge = ctx.merge(overriding);
+        assertEquals("overriding_key", merge.getTargetingKey());
+    }
+
+    @DisplayName("targeting key should not changed from the overriding context if missing")
+    @Test
+    void shouldRetainTargetingKeyWhenOverridingContextTargetingKeyValueIsEmpty() {
+        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());
+    }
+    @DisplayName("missing targeting key should return null")
+    @Test
+    void missingTargetingKeyShould() {
+        EvaluationContext ctx = new MutableContext();
+        assertEquals(null, ctx.getTargetingKey());
+    }
+
+    @DisplayName("Merge should retain all the attributes from the existing context when overriding context is null")
+    @Test
+    void mergeShouldReturnAllTheValuesFromTheContextWhenOverridingContextIsNull() {
+        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 merge = ctx.merge(null);
+        assertEquals("targeting_key", merge.getTargetingKey());
+        assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
+    }
+
+    @DisplayName("Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
+    @Test
+    void mergeShouldRetainItsSubkeysWhenOverridingContextHasTheSameKey() {
+        HashMap<String, Value> attributes = new HashMap<>();
+        HashMap<String, Value> overridingAttributes = new HashMap<>();
+        HashMap<String, Value> key1Attributes = new HashMap<>();
+        HashMap<String, Value> ovKey1Attributes = new HashMap<>();
+
+        key1Attributes.put("key1_1", new Value("val1_1"));
+        attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
+        attributes.put("key2", new Value("val2"));
+        ovKey1Attributes.put("overriding_key1_1", new Value("overriding_val_1_1"));
+        overridingAttributes.put("key1", new Value(new ImmutableStructure(ovKey1Attributes)));
+        
+        EvaluationContext ctx = new MutableContext("targeting_key", attributes);
+        EvaluationContext overriding = new MutableContext("targeting_key", overridingAttributes);
+        EvaluationContext merge = ctx.merge(overriding);
+        assertEquals("targeting_key", merge.getTargetingKey());
+        assertArrayEquals(new Object[]{"key1", "key2", TARGETING_KEY}, merge.keySet().toArray());
+        
+        Value key1 = merge.getValue("key1");
+        assertTrue(key1.isStructure());
+        
+        Structure value = key1.asStructure();
+        assertArrayEquals(new Object[]{"key1_1","overriding_key1_1"}, value.keySet().toArray());
+    }
+    
+    @DisplayName("Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
+    @Test
+    void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() {
+        HashMap<String, Value> attributes = new HashMap<>();
+        HashMap<String, Value> key1Attributes = new HashMap<>();
+
+        key1Attributes.put("key1_1", new Value("val1_1"));
+        attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
+        attributes.put("key2", new Value("val2"));
+        
+        EvaluationContext ctx = new MutableContext(attributes);
+        EvaluationContext overriding = new MutableContext();
+        EvaluationContext merge = ctx.merge(overriding);
+        assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
+        
+        Value key1 = merge.getValue("key1");
+        assertTrue(key1.isStructure());
+        
+        Structure value = key1.asStructure();
+        assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray());
+    }
+}