diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java index 2593dc41..938c8563 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractManagedIdentitySource.java @@ -53,7 +53,13 @@ public ManagedIdentityResponse getManagedIdentityResponse( throw new RuntimeException(e); } catch (MsalClientException e) { if (e.getCause() instanceof SocketException) { - throw new MsalServiceException(e.getMessage(), MsalError.MANAGED_IDENTITY_UNREACHABLE_NETWORK, managedIdentitySourceType); + String message = e.getMessage(); + String correlationId = managedIdentityRequest.requestContext().correlationId(); + LOG.error(LogHelper.createMessage( + "[Managed Identity] Network unreachable: " + message, + correlationId)); + throw new MsalServiceException(message, MsalError.MANAGED_IDENTITY_UNREACHABLE_NETWORK, + correlationId); } throw e; @@ -74,18 +80,25 @@ public ManagedIdentityResponse handleResponse( return getSuccessfulResponse(response); } else { message = getMessageFromErrorResponse(response); - LOG.error("[Managed Identity] request failed, HttpStatusCode: {}, Error message: {}", - response.statusCode(), message); - throw new MsalServiceException(message, AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, managedIdentitySourceType); + String correlationId = managedIdentityRequest.requestContext().correlationId(); + LOG.error(LogHelper.createMessage( + String.format("[Managed Identity] request failed, HttpStatusCode: %s, Error message: %s", + response.statusCode(), message), + correlationId)); + throw new MsalServiceException(message, AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, + correlationId); } } catch (Exception e) { if (!(e instanceof MsalServiceException)) { message = String.format("[Managed Identity] Unexpected exception occurred when parsing the response, HttpStatusCode: %s, Error message: %s", response.statusCode(), e.getMessage()); + String correlationId = managedIdentityRequest.requestContext().correlationId(); + LOG.error(LogHelper.createMessage(message, correlationId)); + throw new MsalServiceException(message, AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, + correlationId); } else { throw e; } - throw new MsalServiceException(message, AuthenticationErrorCode.MANAGED_IDENTITY_REQUEST_FAILED, managedIdentitySourceType); } } @@ -103,7 +116,11 @@ protected ManagedIdentityResponse getSuccessfulResponse(IHttpResponse response) if (managedIdentityResponse == null || managedIdentityResponse.getAccessToken() == null || managedIdentityResponse.getAccessToken().isEmpty() || managedIdentityResponse.getExpiresOn() == null || managedIdentityResponse.getExpiresOn().isEmpty()) { - throw new MsalServiceException("[Managed Identity] Response is either null or insufficient for authentication.", MsalError.MANAGED_IDENTITY_REQUEST_FAILED, managedIdentitySourceType); + String message = "[Managed Identity] Response is either null or insufficient for authentication."; + String correlationId = managedIdentityRequest.requestContext().correlationId(); + LOG.error(LogHelper.createMessage(message, correlationId)); + throw new MsalServiceException(message, MsalError.MANAGED_IDENTITY_REQUEST_FAILED, + correlationId); } return managedIdentityResponse; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java index f475d607..4e0b04e7 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByAuthorizationGrantSupplier.java @@ -139,13 +139,21 @@ private Map getAuthorizationGrantIntegrated(String userName) thr params = getSAMLAuthGrantParameters(wsTrustResponse); } else if (userRealmResponse.isAccountManaged()) { + String message = "Password is required for managed user"; + clientApplication.log.error( + LogHelper.createMessage(message, msalRequest.requestContext().correlationId())); throw new MsalClientException( - "Password is required for managed user", - AuthenticationErrorCode.PASSWORD_REQUIRED_FOR_MANAGED_USER); + message, + AuthenticationErrorCode.PASSWORD_REQUIRED_FOR_MANAGED_USER, + msalRequest.requestContext().correlationId()); } else { + String message = "User Realm request failed"; + clientApplication.log.error( + LogHelper.createMessage(message, msalRequest.requestContext().correlationId())); throw new MsalClientException( - "User Realm request failed", - AuthenticationErrorCode.USER_REALM_DISCOVERY_FAILED); + message, + AuthenticationErrorCode.USER_REALM_DISCOVERY_FAILED, + msalRequest.requestContext().correlationId()); } return params; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByDeviceCodeFlowSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByDeviceCodeFlowSupplier.java index 3270fb00..11017e3d 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByDeviceCodeFlowSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByDeviceCodeFlowSupplier.java @@ -3,11 +3,15 @@ package com.microsoft.aad.msal4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.concurrent.TimeUnit; import static com.microsoft.aad.msal4j.AuthenticationErrorCode.AUTHORIZATION_PENDING; class AcquireTokenByDeviceCodeFlowSupplier extends AuthenticationResultSupplier { + private static final Logger LOG = LoggerFactory.getLogger(AcquireTokenByDeviceCodeFlowSupplier.class); private DeviceCodeFlowRequest deviceCodeFlowRequest; @@ -68,7 +72,10 @@ private AuthenticationResult acquireTokenWithDeviceCode(DeviceCode deviceCode, } } } - throw new MsalClientException("Expired Device code", AuthenticationErrorCode.CODE_EXPIRED); + String message = "Expired Device code"; + LOG.error(LogHelper.createMessage(message, deviceCodeFlowRequest.requestContext().correlationId())); + throw new MsalClientException(message, AuthenticationErrorCode.CODE_EXPIRED, + deviceCodeFlowRequest.requestContext().correlationId()); } private Long getCurrentSystemTimeInSeconds() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java index c6545cf7..a2ca9884 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByManagedIdentitySupplier.java @@ -27,9 +27,14 @@ class AcquireTokenByManagedIdentitySupplier extends AuthenticationResultSupplier AuthenticationResult execute() throws Exception { if (StringHelper.isNullOrBlank(managedIdentityParameters.resource)) { + String message = MsalErrorMessage.SCOPES_REQUIRED; + LOG.error(LogHelper.createMessage( + "[Managed Identity] " + message, + msalRequest.requestContext().correlationId())); throw new MsalClientException( + message, MsalError.RESOURCE_REQUIRED_MANAGED_IDENTITY, - MsalErrorMessage.SCOPES_REQUIRED); + msalRequest.requestContext().correlationId()); } TokenRequestExecutor tokenRequestExecutor = new TokenRequestExecutor( diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java index 00f97621..789f031e 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java @@ -41,7 +41,11 @@ AuthenticationResult execute() throws Exception { clientApplication.clientId()); if (res == null) { - throw new MsalClientException(AuthenticationErrorMessage.NO_TOKEN_IN_CACHE, AuthenticationErrorCode.CACHE_MISS); + String message = AuthenticationErrorMessage.NO_TOKEN_IN_CACHE; + clientApplication.log.info( + LogHelper.createMessage(message, silentRequest.requestContext().correlationId())); + throw new MsalClientException(message, AuthenticationErrorCode.CACHE_MISS, + silentRequest.requestContext().correlationId()); } //Some cached tokens were found, but this metadata will be overwritten if token needs to be refreshed @@ -71,7 +75,11 @@ AuthenticationResult execute() throws Exception { } if (res == null || StringHelper.isBlank(res.accessToken())) { - throw new MsalClientException(AuthenticationErrorMessage.NO_TOKEN_IN_CACHE, AuthenticationErrorCode.CACHE_MISS); + String message = AuthenticationErrorMessage.NO_TOKEN_IN_CACHE; + clientApplication.log.info( + LogHelper.createMessage(message, silentRequest.requestContext().correlationId())); + throw new MsalClientException(message, AuthenticationErrorCode.CACHE_MISS, + silentRequest.requestContext().correlationId()); } LOG.debug("Returning token from cache"); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java index 2f6da6ae..197e659a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java @@ -55,6 +55,10 @@ public IHttpResponse executeHttpRequest(HttpRequest httpRequest, } catch (Exception e) { httpEvent.setOauthErrorCode(AuthenticationErrorCode.UNKNOWN); + String message = LogHelper.createMessage( + "HTTP request execution failed: " + e.getMessage(), + requestContext.correlationId()); + LOG.error(message); throw new MsalClientException(e); } @@ -93,6 +97,10 @@ IHttpResponse executeHttpRequest(HttpRequest httpRequest, } catch (Exception e) { httpEvent.setOauthErrorCode(AuthenticationErrorCode.UNKNOWN); + String message = LogHelper.createMessage( + "HTTP request execution failed: " + e.getMessage(), + requestContext.correlationId()); + LOG.error(message); throw new MsalClientException(e); } @@ -113,6 +121,7 @@ IHttpResponse executeHttpRequest(HttpRequest httpRequest) { try { httpResponse = executeHttpRequestWithRetries(httpRequest, httpClient); } catch (Exception e) { + LOG.error("HTTP request execution failed: " + e.getMessage()); throw new MsalClientException(e); } @@ -173,7 +182,11 @@ private void checkForThrottling(RequestContext requestContext) { long retryInMs = ThrottlingCache.retryInMs(requestThumbprint); if (retryInMs > 0) { - throw new MsalThrottlingException(retryInMs); + String message = LogHelper.createMessage( + "Request throttled, retry after " + retryInMs + " ms", + requestContext.correlationId()); + LOG.warn(message); + throw new MsalThrottlingException(retryInMs, requestContext.correlationId()); } } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index fa73e518..a01e30d8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -3,6 +3,9 @@ package com.microsoft.aad.msal4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.InetAddress; import java.net.URI; import java.net.URL; @@ -14,6 +17,7 @@ import java.util.concurrent.atomic.AtomicReference; class InteractiveRequest extends MsalRequest { + private static final Logger LOG = LoggerFactory.getLogger(InteractiveRequest.class); private AtomicReference> futureReference; @@ -53,25 +57,34 @@ private void validateRedirectUrl(URI redirectUri) { //Validate URI scheme. Only http is valid, as determined by the HttpListener created in AcquireTokenByInteractiveFlowSupplier.startHttpListener() if (scheme == null || !scheme.equals("http")) { - throw new MsalClientException(String.format( - "Only http://localhost or http://localhost:port is supported for the redirect URI of an interactive request using a browser, but \"%s\" was found. For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request", scheme), - AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + String message = String.format( + "Only http://localhost or http://localhost:port is supported for the redirect URI of an interactive request using a browser, but \"%s\" was found. For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request", scheme); + LOG.error(LogHelper.createMessage(message, this.requestContext().correlationId())); + throw new MsalClientException(message, + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI, + this.requestContext().correlationId()); } //Ensure that the given redirect URI has a known address try { address = InetAddress.getByName(host); } catch (UnknownHostException e) { - throw new MsalClientException(String.format( - "Unknown host exception for host \"%s\". For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request", host), - AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + String message = String.format( + "Unknown host exception for host \"%s\". For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request", host); + LOG.error(LogHelper.createMessage(message, this.requestContext().correlationId())); + throw new MsalClientException(message, + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI, + this.requestContext().correlationId()); } //Ensure that the redirect URI is considered a loopback address if (address == null || !address.isLoopbackAddress()) { + String message = "Only loopback redirect URI is supported for interactive requests. For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request"; + LOG.error(LogHelper.createMessage(message, this.requestContext().correlationId())); throw new MsalClientException( - "Only loopback redirect URI is supported for interactive requests. For more information about redirect URI formats, see https://aka.ms/msal4j-interactive-request", - AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + message, + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI, + this.requestContext().correlationId()); } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalClientException.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalClientException.java index f30b9178..4d28eeeb 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalClientException.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalClientException.java @@ -25,4 +25,15 @@ public MsalClientException(final Throwable throwable) { public MsalClientException(final String message, final String errorCode) { super(message, errorCode); } + + /** + * Initializes a new instance of the exception class with a specified error message and correlation ID + * + * @param message the error message that explains the reason for the exception + * @param errorCode the error code + * @param correlationId the correlation ID for request tracking + */ + public MsalClientException(final String message, final String errorCode, final String correlationId) { + super(message, errorCode, correlationId); + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalException.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalException.java index d6b42067..064adec9 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalException.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalException.java @@ -15,6 +15,11 @@ public class MsalException extends RuntimeException { */ private String errorCode; + /** + * Correlation ID for request tracking + */ + private String correlationId; + /** * Initializes a new instance of the exception class * @@ -34,7 +39,24 @@ public MsalException(final String message, String errorCode) { this.errorCode = errorCode; } + /** + * Initializes a new instance of the exception class with correlation ID + * + * @param message the error message that explains the reason for the exception + * @param errorCode the error code + * @param correlationId the correlation ID for request tracking + */ + public MsalException(final String message, String errorCode, String correlationId) { + super(LogHelper.createMessage(message, correlationId)); + this.errorCode = errorCode; + this.correlationId = correlationId; + } + public String errorCode() { return this.errorCode; } + + public String correlationId() { + return this.correlationId; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceException.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceException.java index 016720fa..ca83bec0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceException.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalServiceException.java @@ -14,7 +14,6 @@ public class MsalServiceException extends MsalException { private Integer statusCode; private String statusMessage; - private String correlationId; private String claims; private Map> headers; private String managedIdentitySource; @@ -30,6 +29,17 @@ public MsalServiceException(final String message, final String error) { super(message, error); } + /** + * Initializes a new instance of the exception class with a specified error message and correlation ID + * + * @param message the error message that explains the reason for the exception + * @param error a simplified error code from {@link AuthenticationErrorCode} and used for references in documentation + * @param correlationId the correlation ID for request tracking + */ + public MsalServiceException(final String message, final String error, final String correlationId) { + super(message, error, correlationId); + } + /** * Initializes a new instance of the exception class * @@ -40,11 +50,10 @@ public MsalServiceException( final ErrorResponse errorResponse, final Map> httpHeaders) { - super(errorResponse.errorDescription, errorResponse.error()); + super(errorResponse.errorDescription, errorResponse.error(), errorResponse.correlation_id()); this.statusCode = errorResponse.statusCode(); this.statusMessage = errorResponse.statusMessage(); this.subError = errorResponse.subError(); - this.correlationId = errorResponse.correlation_id(); this.claims = errorResponse.claims(); this.headers = Collections.unmodifiableMap(httpHeaders); } @@ -70,9 +79,7 @@ public MsalServiceException( * @param discoveryResponse response object from instance discovery network call */ public MsalServiceException(final AadInstanceDiscoveryResponse discoveryResponse) { - super(discoveryResponse.errorDescription(), discoveryResponse.error()); - - this.correlationId = discoveryResponse.correlationId(); + super(discoveryResponse.errorDescription(), discoveryResponse.error(), discoveryResponse.correlationId()); } /** @@ -92,8 +99,9 @@ public String statusMessage() { /** * An ID that can be used to piece up a single authentication flow. */ + @Override public String correlationId() { - return this.correlationId; + return super.correlationId(); } /** diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalThrottlingException.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalThrottlingException.java index b019da18..04318c2c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalThrottlingException.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MsalThrottlingException.java @@ -20,6 +20,19 @@ public MsalThrottlingException(long retryInMs) { this.retryInMs = retryInMs; } + /** + * Constructor for MsalThrottlingException class with correlation ID + * + * @param retryInMs time to wait before retrying in milliseconds + * @param correlationId the correlation ID for request tracking + */ + public MsalThrottlingException(long retryInMs, String correlationId) { + super("Request was throttled according to instructions from STS. Retry in " + retryInMs + " ms.", + AuthenticationErrorCode.THROTTLED_REQUEST, correlationId); + + this.retryInMs = retryInMs; + } + /** * how long to wait before repeating request */ diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestContext.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestContext.java index 86bfdf11..ca1d3274 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestContext.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/RequestContext.java @@ -37,6 +37,11 @@ public RequestContext(AbstractApplicationBase clientApplication, this.publicApi = publicApi; this.authority = clientApplication.authority(); this.apiParameters = apiParameters; + + // Log the correlation ID when RequestContext is created + clientApplication.log.info(LogHelper.createMessage( + "Request initiated with PublicApi: " + publicApi, + this.correlationId)); } public RequestContext(AbstractApplicationBase clientApplication, diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index c6e7ca56..c18a1254 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -38,8 +38,11 @@ AuthenticationResult executeTokenRequest() throws IOException { OAuthHttpRequest createOauthHttpRequest() throws MalformedURLException { if (requestAuthority.tokenEndpointUrl() == null) { - throw new MsalClientException("The endpoint URI is not specified", - AuthenticationErrorCode.INVALID_ENDPOINT_URI); + String message = "The endpoint URI is not specified"; + LOG.error(LogHelper.createMessage(message, msalRequest.requestContext().correlationId())); + throw new MsalClientException(message, + AuthenticationErrorCode.INVALID_ENDPOINT_URI, + msalRequest.requestContext().correlationId()); } final OAuthHttpRequest oauthHttpRequest = new OAuthHttpRequest( diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CorrelationIdExceptionTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CorrelationIdExceptionTest.java new file mode 100644 index 00000000..0762bd23 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/CorrelationIdExceptionTest.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class to verify correlation IDs are included in exceptions + */ +class CorrelationIdExceptionTest { + + @Test + void testMsalExceptionWithCorrelationId() { + String testCorrelationId = "test-correlation-id-123"; + String message = "Test error message"; + String errorCode = "test_error"; + + MsalException exception = new MsalException(message, errorCode, testCorrelationId); + + assertNotNull(exception.correlationId(), "Correlation ID should not be null"); + assertEquals(testCorrelationId, exception.correlationId(), "Correlation ID should match"); + assertTrue(exception.getMessage().contains(testCorrelationId), + "Exception message should contain correlation ID"); + assertTrue(exception.getMessage().contains("[Correlation ID: " + testCorrelationId + "]"), + "Exception message should be formatted with correlation ID"); + } + + @Test + void testMsalClientExceptionWithCorrelationId() { + String testCorrelationId = "client-error-correlation-id"; + String message = "Client error occurred"; + String errorCode = "client_error"; + + MsalClientException exception = new MsalClientException(message, errorCode, testCorrelationId); + + assertNotNull(exception.correlationId(), "Correlation ID should not be null"); + assertEquals(testCorrelationId, exception.correlationId(), "Correlation ID should match"); + assertTrue(exception.getMessage().contains(testCorrelationId), + "Exception message should contain correlation ID"); + } + + @Test + void testMsalServiceExceptionWithCorrelationId() { + String testCorrelationId = "service-error-correlation-id"; + String message = "Service error occurred"; + String errorCode = "service_error"; + + MsalServiceException exception = new MsalServiceException(message, errorCode, testCorrelationId); + + assertNotNull(exception.correlationId(), "Correlation ID should not be null"); + assertEquals(testCorrelationId, exception.correlationId(), "Correlation ID should match"); + assertTrue(exception.getMessage().contains(testCorrelationId), + "Exception message should contain correlation ID"); + } + + @Test + void testMsalThrottlingExceptionWithCorrelationId() { + String testCorrelationId = "throttling-correlation-id"; + long retryInMs = 5000; + + MsalThrottlingException exception = new MsalThrottlingException(retryInMs, testCorrelationId); + + assertNotNull(exception.correlationId(), "Correlation ID should not be null"); + assertEquals(testCorrelationId, exception.correlationId(), "Correlation ID should match"); + assertTrue(exception.getMessage().contains(testCorrelationId), + "Exception message should contain correlation ID"); + assertEquals(retryInMs, exception.retryInMs(), "Retry time should match"); + } + + @Test + void testMsalExceptionWithoutCorrelationId() { + String message = "Test error message"; + String errorCode = "test_error"; + + MsalException exception = new MsalException(message, errorCode); + + assertNull(exception.correlationId(), "Correlation ID should be null when not provided"); + assertFalse(exception.getMessage().contains("[Correlation ID:"), + "Exception message should not contain correlation ID prefix"); + } + + @Test + void testLogHelperCreateMessage() { + String message = "Test message"; + String correlationId = "test-corr-id"; + + String formattedMessage = LogHelper.createMessage(message, correlationId); + + assertTrue(formattedMessage.contains("[Correlation ID: " + correlationId + "]"), + "Formatted message should contain correlation ID in correct format"); + assertTrue(formattedMessage.contains(message), + "Formatted message should contain original message"); + } +}