diff --git a/examples/pom.xml b/examples/pom.xml index dae76a2..e4d6a18 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ io.harness.featureflags examples - 1.2.4 + 1.2.5 8 @@ -33,7 +33,7 @@ io.harness ff-java-server-sdk - 1.2.4 + 1.2.5 diff --git a/pom.xml b/pom.xml index 0955556..78ef544 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.harness ff-java-server-sdk - 1.2.4 + 1.2.5 jar Harness Feature Flag Java Server SDK Harness Feature Flag Java Server SDK diff --git a/src/main/java/io/harness/cf/client/connector/HarnessConnector.java b/src/main/java/io/harness/cf/client/connector/HarnessConnector.java index dddf78e..dbbcc00 100644 --- a/src/main/java/io/harness/cf/client/connector/HarnessConnector.java +++ b/src/main/java/io/harness/cf/client/connector/HarnessConnector.java @@ -32,7 +32,7 @@ public class HarnessConnector implements Connector, AutoCloseable { private final HarnessConfig options; private String token; - private String environment; + private String environmentUuid; private String cluster; private String environmentIdentifier; private String accountID; @@ -196,36 +196,50 @@ protected void processToken(@NonNull final String token) { Claim claim = gson.fromJson(decoded, Claim.class); log.debug("Claims successfully parsed from decoded payload"); - environment = claim.getEnvironment(); + environmentUuid = claim.getEnvironment(); cluster = claim.getClusterIdentifier(); - accountID = claim.getAccountID(); - environmentIdentifier = claim.getEnvironmentIdentifier(); + accountID = emptyToNull(claim.getAccountID()); + environmentIdentifier = getEnvOrUuidEnv(claim.getEnvironmentIdentifier(), environmentUuid); - api.getApiClient().addDefaultHeader("Harness-EnvironmentID", environmentIdentifier); - api.getApiClient().addDefaultHeader("Harness-AccountID", accountID); - metricsApi.getApiClient().addDefaultHeader("Harness-EnvironmentID", environmentIdentifier); - metricsApi.getApiClient().addDefaultHeader("Harness-AccountID", accountID); + if (environmentIdentifier != null) { + api.getApiClient().addDefaultHeader("Harness-EnvironmentID", environmentIdentifier); + metricsApi.getApiClient().addDefaultHeader("Harness-EnvironmentID", environmentIdentifier); + } + + if (accountID != null) { + api.getApiClient().addDefaultHeader("Harness-AccountID", accountID); + metricsApi.getApiClient().addDefaultHeader("Harness-AccountID", accountID); + } log.info( "Token successfully processed, environment {}, cluster {}, account {}, environmentIdentifier {}", - environment, + environmentUuid, cluster, accountID, environmentIdentifier); } + private String getEnvOrUuidEnv(String env, String envUuid) { + String envToReturn = emptyToNull(env); + return (envToReturn == null) ? emptyToNull(envUuid) : envToReturn; + } + + private String emptyToNull(String jsonValue) { + return (jsonValue != null && !jsonValue.trim().isEmpty()) ? jsonValue : null; + } + @Override public List getFlags() throws ConnectorException { final String requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID_KEY, requestId); - log.info("Fetching flags on env {} and cluster {}", this.environment, this.cluster); + log.info("Fetching flags on env {} and cluster {}", this.environmentUuid, this.cluster); List featureConfig = new ArrayList<>(); try { - featureConfig = api.getFeatureConfig(environment, cluster); + featureConfig = api.getFeatureConfig(environmentUuid, cluster); log.info( "Total configurations fetched: {} on env {} and cluster {}", featureConfig.size(), - this.environment, + this.environmentUuid, this.cluster); if (log.isTraceEnabled()) { log.trace("Got the following features: " + featureConfig); @@ -234,7 +248,7 @@ public List getFlags() throws ConnectorException { } catch (ApiException e) { log.error( "Exception was raised while fetching the flags on env {} and cluster {}", - this.environment, + this.environmentUuid, this.cluster, e); throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage()); @@ -248,21 +262,21 @@ public FeatureConfig getFlag(@NonNull final String identifier) throws ConnectorE final String requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID_KEY, requestId); log.debug( - "Fetch flag {} from env {} and cluster {}", identifier, this.environment, this.cluster); + "Fetch flag {} from env {} and cluster {}", identifier, this.environmentUuid, this.cluster); try { FeatureConfig featureConfigByIdentifier = - api.getFeatureConfigByIdentifier(identifier, environment, cluster); + api.getFeatureConfigByIdentifier(identifier, environmentUuid, cluster); log.debug( "Flag {} successfully fetched from env {} and cluster {}", identifier, - this.environment, + this.environmentUuid, this.cluster); return featureConfigByIdentifier; } catch (ApiException e) { log.error( "Exception was raised while fetching the flag {} on env {} and cluster {}", identifier, - this.environment, + this.environmentUuid, this.cluster, e); throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage()); @@ -276,20 +290,22 @@ public List getSegments() throws ConnectorException { final String requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID_KEY, requestId); log.debug( - "Fetching target groups on environment {} and cluster {}", this.environment, this.cluster); + "Fetching target groups on environment {} and cluster {}", + this.environmentUuid, + this.cluster); List allSegments = new ArrayList<>(); try { - allSegments = api.getAllSegments(environment, cluster); + allSegments = api.getAllSegments(environmentUuid, cluster); log.debug( "Total target groups fetched: {} on env {} and cluster {}", allSegments.size(), - this.environment, + this.environmentUuid, this.cluster); return allSegments; } catch (ApiException e) { log.error( "Exception was raised while fetching the target groups on env {} and cluster {} : httpCode={} message={}", - this.environment, + this.environmentUuid, this.cluster, e.getCode(), e.getMessage(), @@ -307,21 +323,22 @@ public Segment getSegment(@NonNull final String identifier) throws ConnectorExce log.debug( "Fetching the target group {} on environment {} and cluster {}", identifier, - this.environment, + this.environmentUuid, this.cluster); try { - Segment segmentByIdentifier = api.getSegmentByIdentifier(identifier, environment, cluster); + Segment segmentByIdentifier = + api.getSegmentByIdentifier(identifier, environmentUuid, cluster); log.debug( "Segment {} successfully fetched from env {} and cluster {}", identifier, - this.environment, + this.environmentUuid, this.cluster); return segmentByIdentifier; } catch (ApiException e) { log.error( "Exception was raised while fetching the target group {} on env {} and cluster {}", identifier, - this.environment, + this.environmentUuid, this.cluster, e); throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage()); @@ -334,17 +351,18 @@ public Segment getSegment(@NonNull final String identifier) throws ConnectorExce public void postMetrics(@NonNull final Metrics metrics) throws ConnectorException { final String requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID_KEY, requestId); - log.debug("Uploading metrics on environment {} and cluster {}", this.environment, this.cluster); + log.debug( + "Uploading metrics on environment {} and cluster {}", this.environmentUuid, this.cluster); try { - metricsApi.postMetrics(environment, cluster, metrics); + metricsApi.postMetrics(environmentUuid, cluster, metrics); log.debug( "Metrics uploaded successfully on environment {} and cluster {}", - this.environment, + this.environmentUuid, this.cluster); } catch (ApiException e) { log.error( "Exception was raised while uploading metrics on env {} and cluster {}", - this.environment, + this.environmentUuid, this.cluster, e); throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage()); @@ -366,8 +384,14 @@ public Service stream(@NonNull final Updater updater) throws ConnectorException map.put("Authorization", "Bearer " + token); map.put("API-Key", apiKey); map.put("Harness-SDK-Info", HARNESS_SDK_INFO); - map.put("Harness-EnvironmentID", environmentIdentifier); - map.put("Harness-AccountID", accountID); + + if (environmentIdentifier != null) { + map.put("Harness-EnvironmentID", environmentIdentifier); + } + + if (accountID != null) { + map.put("Harness-AccountID", accountID); + } log.info("Initialize new EventSource instance"); eventSource = @@ -431,4 +455,15 @@ private static boolean isNullOrEmpty(String string) { options, retryBackOffDelay); } + + HarnessConnector( + @NonNull String apiKey, + @NonNull HarnessConfig options, + ClientApi clientApi, + MetricsApi metricsApi) { + this.apiKey = apiKey; + this.options = options; + this.api = clientApi; + this.metricsApi = metricsApi; + } } diff --git a/src/test/java/io/harness/cf/client/api/CfClientTest.java b/src/test/java/io/harness/cf/client/api/CfClientTest.java index b9c23e7..b003b77 100644 --- a/src/test/java/io/harness/cf/client/api/CfClientTest.java +++ b/src/test/java/io/harness/cf/client/api/CfClientTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; class CfClientTest { @@ -511,6 +512,72 @@ void shouldRetryThenReAuthenticateWhen403IsReturnedOnGetAllSegments() throws Exc } } + @ParameterizedTest + @NullSource() + @ValueSource(strings = {"dummyAccId", "", " ", "\t", "\n", "\r"}) + void shouldTestVariousJwtAccountIDs(String nextAccountId) throws Exception { + BaseConfig config = + BaseConfig.builder() + .pollIntervalInSeconds(1) + .analyticsEnabled(false) + .streamEnabled(true) + .debug(false) + .build(); + + JwtMissingFieldsAuthDispatcher webserverDispatcher = + new JwtMissingFieldsAuthDispatcher("devEnv", nextAccountId); + + try (MockWebServer mockSvr = new MockWebServer()) { + mockSvr.setDispatcher(webserverDispatcher); + mockSvr.start(); + + try (CfClient client = + new CfClient( + makeConnectorWithMinimalRetryBackOff(mockSvr.getHostName(), mockSvr.getPort()), + config)) { + + client.waitForInitialization(); + webserverDispatcher.waitForAllEndpointsToBeCalled(15); + webserverDispatcher.getErrors().forEach(Throwable::printStackTrace); + + assertTrue(webserverDispatcher.getErrors().isEmpty()); + } + } + } + + @ParameterizedTest + @NullSource() + @ValueSource(strings = {"dummyAccId", "", " ", "\t", "\n", "\r"}) + void shouldTestVariousJwtEnvironmentIdentifiers(String nextEnvId) throws Exception { + BaseConfig config = + BaseConfig.builder() + .pollIntervalInSeconds(1) + .analyticsEnabled(false) + .streamEnabled(true) + .debug(false) + .build(); + + JwtMissingFieldsAuthDispatcher webserverDispatcher = + new JwtMissingFieldsAuthDispatcher(nextEnvId, "dummyAccount"); + + try (MockWebServer mockSvr = new MockWebServer()) { + mockSvr.setDispatcher(webserverDispatcher); + mockSvr.start(); + + try (CfClient client = + new CfClient( + makeConnectorWithMinimalRetryBackOff(mockSvr.getHostName(), mockSvr.getPort()), + config)) { + + client.waitForInitialization(); + webserverDispatcher.waitForAllEndpointsToBeCalled(15); + webserverDispatcher.getErrors().forEach(Throwable::printStackTrace); + + assertTrue(webserverDispatcher.getErrors().isEmpty()); + } + } + } + static class DummyCache implements Cache { @Override diff --git a/src/test/java/io/harness/cf/client/api/dispatchers/CannedResponses.java b/src/test/java/io/harness/cf/client/api/dispatchers/CannedResponses.java index ce87b7f..2860992 100644 --- a/src/test/java/io/harness/cf/client/api/dispatchers/CannedResponses.java +++ b/src/test/java/io/harness/cf/client/api/dispatchers/CannedResponses.java @@ -39,6 +39,12 @@ public static MockResponse makeAuthResponse(int httpCode) { return makeMockJsonResponse(httpCode, "{\"authToken\": \"" + makeDummyJwtToken() + "\"}"); } + public static MockResponse makeAuthResponse( + int httpCode, String envUuid, String env, String accountId) { + return makeMockJsonResponse( + httpCode, "{\"authToken\": \"" + makeDummyJwtToken(envUuid, env, accountId) + "\"}"); + } + public static MockResponse makeMockStreamResponse(int httpCode, Event... events) { final StringBuilder builder = new StringBuilder(); @@ -72,17 +78,35 @@ public static CannedResponses.Event makeFlagPatchEvent(String identifier, int ve } public static String makeDummyJwtToken() { + return makeDummyJwtToken( + "00000000-0000-0000-0000-000000000000", "Production", "aaaaa_BBBBB-cccccccccc"); + } + + public static String makeDummyJwtToken(String envUuid, String env, String accountID) { final String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; - final String payload = - "{\"environment\":\"00000000-0000-0000-0000-000000000000\"," - + "\"environmentIdentifier\":\"Production\"," - + "\"project\":\"00000000-0000-0000-0000-000000000000\"," + String payload = "{"; + + if (envUuid != null) { + payload += "\"environment\":\"" + envUuid + "\","; + } + + if (env != null) { + payload += "\"environmentIdentifier\":\"" + env + "\","; + } + + if (accountID != null) { + payload += "\"accountID\":\"" + accountID + "\","; + } + + payload += + "\"project\":\"00000000-0000-0000-0000-000000000000\"," + "\"projectIdentifier\":\"dev\"," - + "\"accountID\":\"aaaaa_BBBBB-cccccccccc\"," + "\"organization\":\"00000000-0000-0000-0000-000000000000\"," + "\"organizationIdentifier\":\"default\"," + "\"clusterIdentifier\":\"1\"," - + "\"key_type\":\"Server\"}"; + + "\"key_type\":\"Server\"" + + "}"; + final byte[] hmac256 = new byte[32]; return Base64.getEncoder().encodeToString(header.getBytes(StandardCharsets.UTF_8)) + "." diff --git a/src/test/java/io/harness/cf/client/api/dispatchers/JwtMissingFieldsAuthDispatcher.java b/src/test/java/io/harness/cf/client/api/dispatchers/JwtMissingFieldsAuthDispatcher.java new file mode 100644 index 0000000..48ce8df --- /dev/null +++ b/src/test/java/io/harness/cf/client/api/dispatchers/JwtMissingFieldsAuthDispatcher.java @@ -0,0 +1,128 @@ +package io.harness.cf.client.api.dispatchers; + +import static io.harness.cf.client.api.TestUtils.makeBasicFeatureJson; +import static io.harness.cf.client.api.TestUtils.makeSegmentsJson; +import static io.harness.cf.client.api.dispatchers.CannedResponses.*; +import static io.harness.cf.client.api.dispatchers.Endpoints.*; + +import io.harness.cf.client.api.testutils.PollingAtomicLong; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.Getter; +import lombok.SneakyThrows; +import okhttp3.Headers; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; +import org.jetbrains.annotations.NotNull; + +public class JwtMissingFieldsAuthDispatcher extends TestWebServerDispatcher { + private final AtomicInteger version = new AtomicInteger(2); + @Getter private final PollingAtomicLong endpointsHit; + @Getter private final List errors = new ArrayList<>(); + + private final String jwtEnvironmentIdentifier; + private final String jwtAccountId; + /* Set to null whatever fields you're testing in the JWT token */ + public JwtMissingFieldsAuthDispatcher(String jwtEnvironmentIdentifier, String jwtAccountId) { + this.jwtEnvironmentIdentifier = jwtEnvironmentIdentifier; + this.jwtAccountId = jwtAccountId; + endpointsHit = new PollingAtomicLong(5); + } + + @Override + @SneakyThrows + @NotNull + public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) { + System.out.printf( + "DISPATCH GOT ------> %s jwtEnvironmentIdentifier='%s' jwtAccountId='%s'\n", + recordedRequest.getPath(), jwtEnvironmentIdentifier, jwtAccountId); + + endpointsHit.incrementAndGet(); + + switch (Objects.requireNonNull(recordedRequest.getPath())) { + case AUTH_ENDPOINT: + return makeAuthResponse( + 200, "00000000-0000-0000-0000-000000000000", jwtEnvironmentIdentifier, jwtAccountId); + case FEATURES_ENDPOINT: + assertHeaders(recordedRequest); + return makeMockJsonResponse(200, makeBasicFeatureJson()); + case SEGMENTS_ENDPOINT: + assertHeaders(recordedRequest); + return makeMockJsonResponse(200, makeSegmentsJson()); + case STREAM_ENDPOINT: + assertHeaders(recordedRequest); + return makeMockStreamResponse( + 200, makeFlagPatchEvent("simplebool", version.getAndIncrement())); + case SIMPLE_BOOL_FLAG_ENDPOINT: + assertHeaders(recordedRequest); + return makeMockSingleBoolFlagResponse(200, "simplebool", "off", version.get()); + // TODO add metrics here + default: + throw new UnsupportedOperationException( + "ERROR: url not mapped " + recordedRequest.getPath()); + } + } + + private MockResponse makeAssertFailResp(String msg) { + return new MockResponse() + .setSocketPolicy(SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE) + .setResponseCode(-1) + .setStatus(msg); + } + + private void assertHeaders(RecordedRequest recordedRequest) { + final Headers headers = recordedRequest.getHeaders(); + final String url = recordedRequest.getPath(); + + System.out.print(headers); + + final String accountVal = headers.get("Harness-AccountID"); + if (jwtAccountId == null || jwtAccountId.trim().isEmpty()) { + if (accountVal != null) { + errors.add( + new RuntimeException( + String.format( + "Harness-AccountID=%s header should not be present on req '%s'", + accountVal, url))); + } + } else { + if (!jwtAccountId.equals(accountVal)) { + errors.add( + new RuntimeException( + String.format( + "Harness-AccountID=%s header does not match JWT accountID '%s' on req '%s'", + accountVal, jwtAccountId, url))); + } + } + + final String envIdVal = headers.get("Harness-EnvironmentID"); + if (jwtEnvironmentIdentifier == null || jwtEnvironmentIdentifier.trim().isEmpty()) { + if (!"00000000-0000-0000-0000-000000000000".equals(envIdVal)) { + errors.add( + new RuntimeException( + String.format( + "Harness-EnvironmentID=%s header should fallback to UUID when environmentIdentifier is null on req '%s'", + envIdVal, url))); + } + } else { + if (!jwtEnvironmentIdentifier.equals(envIdVal)) { + errors.add( + new RuntimeException( + String.format( + "Harness-EnvironmentID=%s does not match JWT environmentIdentifier '%s' on req '%s'", + envIdVal, jwtEnvironmentIdentifier, url))); + } + } + } + + public void waitForAllEndpointsToBeCalled(int waitTimeSeconds) throws InterruptedException { + endpointsHit.waitForMinimumValueToBeReached( + waitTimeSeconds, + "auth/feat/seg/stream/flag", + "Did not get minimum number of endpoint calls"); + Thread.sleep(500); // give time for resp to get back + } +} diff --git a/src/test/java/io/harness/cf/client/connector/HarnessConnectorTest.java b/src/test/java/io/harness/cf/client/connector/HarnessConnectorTest.java index 4cc52a3..3700ca6 100644 --- a/src/test/java/io/harness/cf/client/connector/HarnessConnectorTest.java +++ b/src/test/java/io/harness/cf/client/connector/HarnessConnectorTest.java @@ -1,11 +1,23 @@ package io.harness.cf.client.connector; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import io.harness.cf.ApiClient; +import io.harness.cf.api.ClientApi; +import io.harness.cf.api.MetricsApi; import io.harness.cf.client.api.MissingSdkKeyException; +import io.harness.cf.client.api.dispatchers.CannedResponses; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.stubbing.Answer; class HarnessConnectorTest { @@ -30,4 +42,136 @@ void shouldThrowExceptionWhenNullApiKeyIsGiven() { "Exception was not thrown"); assertInstanceOf(NullPointerException.class, thrown); } + + Answer makeCaptureHeadersAnswer(Map capturedHeaders) { + return params -> { + String h = String.valueOf((String) params.getArgument(0)); + String v = String.valueOf((String) params.getArgument(1)); + System.out.printf("adding %s=%s\n", h, v); + capturedHeaders.put(h, v); + return null; + }; + } + + void setupHeaderCaptures( + ClientApi mockClientApi, + Map capturedApiHeaders, + MetricsApi mockMetricsApi, + Map capturedMetricApiHeaders) { + final ApiClient mockInternalApiClient = mock(ApiClient.class); + when(mockClientApi.getApiClient()).thenReturn(mockInternalApiClient); + when(mockInternalApiClient.addDefaultHeader(anyString(), anyString())) + .thenAnswer(makeCaptureHeadersAnswer(capturedApiHeaders)); + + final ApiClient mockInternalMetricsApiClient = mock(ApiClient.class); + when(mockMetricsApi.getApiClient()).thenReturn(mockInternalMetricsApiClient); + when(mockInternalMetricsApiClient.addDefaultHeader(anyString(), anyString())) + .thenAnswer(makeCaptureHeadersAnswer(capturedMetricApiHeaders)); + } + + @ParameterizedTest + @NullSource() + @ValueSource(strings = {"", " ", "\t", "\n", "\r"}) + void shouldParseJwtTokenWithMissingAccountId(String accountId) { + final Map apiHeaders = new HashMap<>(); + final Map metricApiHeaders = new HashMap<>(); + + final ClientApi mockClientApi = mock(ClientApi.class); + final MetricsApi mockMetricsApi = mock(MetricsApi.class); + setupHeaderCaptures(mockClientApi, apiHeaders, mockMetricsApi, metricApiHeaders); + + final HarnessConnector connector = + new HarnessConnector( + "dummy_sdk_key", mock(HarnessConfig.class), mockClientApi, mockMetricsApi); + + final String token = CannedResponses.makeDummyJwtToken("dummyUUID", "dev", accountId); + connector.processToken(token); + + for (Map nextMap : Arrays.asList(apiHeaders, metricApiHeaders)) { + System.out.print(nextMap); + assertEquals(2, nextMap.size()); + assertEquals("Bearer " + token, nextMap.get("Authorization")); + assertEquals("dev", nextMap.get("Harness-EnvironmentID")); + assertFalse(nextMap.containsKey("Harness-AccountID")); + } + } + + @Test + void shouldAddHarnessEnvironmentIdHeader() { + final Map apiHeaders = new HashMap<>(); + final Map metricApiHeaders = new HashMap<>(); + + final ClientApi mockClientApi = mock(ClientApi.class); + final MetricsApi mockMetricsApi = mock(MetricsApi.class); + setupHeaderCaptures(mockClientApi, apiHeaders, mockMetricsApi, metricApiHeaders); + + final HarnessConnector connector = + new HarnessConnector( + "dummy_sdk_key", mock(HarnessConfig.class), mockClientApi, mockMetricsApi); + + final String token = CannedResponses.makeDummyJwtToken("dummyUUID", "non_uuid_env_name", "acc"); + connector.processToken(token); + + for (Map nextMap : Arrays.asList(apiHeaders, metricApiHeaders)) { + System.out.print(nextMap); + assertEquals(3, nextMap.size()); + assertEquals("Bearer " + token, nextMap.get("Authorization")); + assertEquals("non_uuid_env_name", nextMap.get("Harness-EnvironmentID")); + assertEquals("acc", nextMap.get("Harness-AccountID")); + } + } + + @ParameterizedTest + @NullSource() + @ValueSource(strings = {"", " ", "\t", "\n", "\r"}) + void shouldAddHarnessEnvironmentIdHeaderButFallbackToUuidEnvIfEnvNotPresent(String env) { + final Map apiHeaders = new HashMap<>(); + final Map metricApiHeaders = new HashMap<>(); + + final ClientApi mockClientApi = mock(ClientApi.class); + final MetricsApi mockMetricsApi = mock(MetricsApi.class); + setupHeaderCaptures(mockClientApi, apiHeaders, mockMetricsApi, metricApiHeaders); + + final HarnessConnector connector = + new HarnessConnector( + "dummy_sdk_key", mock(HarnessConfig.class), mockClientApi, mockMetricsApi); + + final String token = CannedResponses.makeDummyJwtToken("dummyUUID", env, "acc"); + connector.processToken(token); + + for (Map nextMap : Arrays.asList(apiHeaders, metricApiHeaders)) { + System.out.print(nextMap); + assertEquals(3, nextMap.size()); + assertEquals("Bearer " + token, nextMap.get("Authorization")); + assertEquals("dummyUUID", nextMap.get("Harness-EnvironmentID")); + assertEquals("acc", nextMap.get("Harness-AccountID")); + } + } + + @ParameterizedTest + @NullSource() + @ValueSource(strings = {"", " ", "\t", "\n", "\r"}) + void shouldNotAddHarnessEnvironmentIdHeaderIfNeitherEnvOrEnvUuidPresent(String env) { + final Map apiHeaders = new HashMap<>(); + final Map metricApiHeaders = new HashMap<>(); + + final ClientApi mockClientApi = mock(ClientApi.class); + final MetricsApi mockMetricsApi = mock(MetricsApi.class); + setupHeaderCaptures(mockClientApi, apiHeaders, mockMetricsApi, metricApiHeaders); + + final HarnessConnector connector = + new HarnessConnector( + "dummy_sdk_key", mock(HarnessConfig.class), mockClientApi, mockMetricsApi); + + final String token = CannedResponses.makeDummyJwtToken(null, env, "acc"); + connector.processToken(token); + + for (Map nextMap : Arrays.asList(apiHeaders, metricApiHeaders)) { + System.out.print(nextMap); + assertEquals(2, nextMap.size()); + assertEquals("Bearer " + token, nextMap.get("Authorization")); + assertEquals("acc", nextMap.get("Harness-AccountID")); + assertFalse(nextMap.containsKey("Harness-EnvironmentID")); + } + } }