From 14d3516d414e4627e8abd1ed07d021fd0bca8765 Mon Sep 17 00:00:00 2001 From: Andrew Bell <115623869+andybharness@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:14:47 +0100 Subject: [PATCH] [FFM-8688] - Handle accountID as an optional field in the JWT token (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FFM-8688] - Handle accountID as an optional field in the JWT token What Adding new unit tests + harden the existing JWT parsing code to handle information which may be optional when an ff-proxy is in use. If the info is not available the header will not be added. Also fixed env header to fall back to UUID when not present. Why The proxy doesn’t send accountID in the JWT token on auth, so we should make sure the code handles this correctly without bailing out when a proxy is in use. Testing New unit tests written + manual --- examples/pom.xml | 4 +- pom.xml | 2 +- .../cf/client/connector/HarnessConnector.java | 97 ++++++++---- .../harness/cf/client/api/CfClientTest.java | 67 ++++++++ .../api/dispatchers/CannedResponses.java | 36 ++++- .../JwtMissingFieldsAuthDispatcher.java | 128 +++++++++++++++ .../connector/HarnessConnectorTest.java | 148 +++++++++++++++++- 7 files changed, 440 insertions(+), 42 deletions(-) create mode 100644 src/test/java/io/harness/cf/client/api/dispatchers/JwtMissingFieldsAuthDispatcher.java diff --git a/examples/pom.xml b/examples/pom.xml index dae76a21..e4d6a182 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 09555562..78ef5447 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 dddf78ef..dbbcc000 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 b9c23e77..b003b771 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 ce87b7f1..28609924 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 00000000..48ce8df5 --- /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 4cc52a38..3700ca6f 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")); + } + } }