From 5ef4a64141ea90534951bbc24877c05692986fd4 Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 14:26:58 -0700 Subject: [PATCH 1/7] Response tests --- .../aad/msal4j/ResponseParsingTest.java | 519 ++++++++++++++++++ .../aad/msal4j/WSTrustResponseTest.java | 145 ++++- 2 files changed, 640 insertions(+), 24 deletions(-) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java new file mode 100644 index 00000000..0ee998fc --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class ResponseParsingTest { + + // ========== ErrorResponse ========== + + @Test + void errorResponse_fromJson_allFieldsPopulated() throws IOException { + String json = "{\"error\":\"invalid_grant\"," + + "\"error_description\":\"Token expired\"," + + "\"error_codes\":[50076,70008]," + + "\"suberror\":\"basic_action\"," + + "\"trace_id\":\"abc-123\"," + + "\"timestamp\":\"2024-01-15 12:00:00Z\"," + + "\"correlation_id\":\"corr-456\"," + + "\"claims\":\"{\\\"access_token\\\":{\\\"nbf\\\":{\\\"essential\\\":true}}}\"}"; + + ErrorResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = ErrorResponse.fromJson(reader); + } + + assertEquals("invalid_grant", response.error()); + assertEquals("Token expired", response.errorDescription()); + assertArrayEquals(new long[]{50076, 70008}, response.errorCodes()); + assertEquals("basic_action", response.subError()); + assertEquals("abc-123", response.traceId()); + assertEquals("2024-01-15 12:00:00Z", response.timestamp()); + assertEquals("corr-456", response.correlation_id()); + assertTrue(response.claims().contains("access_token")); + } + + @Test + void errorResponse_fromJson_minimalFields() throws IOException { + String json = "{\"error\":\"server_error\"}"; + + ErrorResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = ErrorResponse.fromJson(reader); + } + + assertEquals("server_error", response.error()); + assertNull(response.errorDescription()); + assertNull(response.errorCodes()); + assertNull(response.subError()); + assertNull(response.traceId()); + assertNull(response.timestamp()); + assertNull(response.correlation_id()); + assertNull(response.claims()); + } + + @Test + void errorResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"error\":\"invalid_request\",\"unknown_field\":\"value\",\"another\":123}"; + + ErrorResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = ErrorResponse.fromJson(reader); + } + + assertEquals("invalid_request", response.error()); + } + + @Test + void errorResponse_fromJson_emptyErrorCodes() throws IOException { + String json = "{\"error\":\"test\",\"error_codes\":[]}"; + + ErrorResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = ErrorResponse.fromJson(reader); + } + + assertNotNull(response.errorCodes()); + assertEquals(0, response.errorCodes().length); + } + + @Test + void errorResponse_settersAndGetters_fullCoverage() { + ErrorResponse response = new ErrorResponse(); + + response.statusCode(401); + response.statusMessage("Unauthorized"); + response.error("invalid_token"); + response.errorDescription("The token is expired"); + response.errorCodes(new long[]{50013}); + response.subError("token_expired"); + response.traceId("trace-789"); + response.timestamp("2024-06-01"); + response.correlation_id("corr-id"); + response.claims("{\"id_token\":{}}"); + + assertEquals(401, response.statusCode().intValue()); + assertEquals("Unauthorized", response.statusMessage()); + assertEquals("invalid_token", response.error()); + assertEquals("The token is expired", response.errorDescription()); + assertArrayEquals(new long[]{50013}, response.errorCodes()); + assertEquals("token_expired", response.subError()); + assertEquals("trace-789", response.traceId()); + assertEquals("2024-06-01", response.timestamp()); + assertEquals("corr-id", response.correlation_id()); + assertEquals("{\"id_token\":{}}", response.claims()); + } + + @Test + void errorResponse_toJson_throwsDueToDoubleStartObject() { + // Bug: ErrorResponse.toJson() calls writeStartObject() twice (lines 68, 70) + // which causes an IllegalStateException from the JSON writer + ErrorResponse response = new ErrorResponse(); + response.statusCode(400); + response.error("invalid_grant"); + + assertThrows(IllegalStateException.class, () -> writeToJson(response), + "toJson has a double writeStartObject bug that causes IllegalStateException"); + } + + @Test + void errorResponse_toJson_nullErrorCodes_throwsDueToDoubleStartObject() { + // Same double writeStartObject bug affects all toJson calls + ErrorResponse response = new ErrorResponse(); + response.statusCode(500); + response.error("server_error"); + + assertThrows(IllegalStateException.class, () -> writeToJson(response)); + } + + // ========== UserDiscoveryResponse ========== + + @Test + void userDiscoveryResponse_fromJson_federatedAccount() throws IOException { + String json = "{\"ver\":\"1.0\"," + + "\"account_type\":\"Federated\"," + + "\"federation_metadata_url\":\"https://adfs.example.com/metadata\"," + + "\"federation_protocol\":\"WSTrust\"," + + "\"federation_active_auth_url\":\"https://adfs.example.com/active\"," + + "\"cloud_audience_urn\":\"urn:federation:MicrosoftOnline\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertEquals(1.0f, response.version(), 0.01f); + assertEquals("Federated", response.accountType()); + assertEquals("https://adfs.example.com/metadata", response.federationMetadataUrl()); + assertEquals("WSTrust", response.federationProtocol()); + assertEquals("https://adfs.example.com/active", response.federationActiveAuthUrl()); + assertEquals("urn:federation:MicrosoftOnline", response.cloudAudienceUrn()); + assertTrue(response.isAccountFederated()); + assertFalse(response.isAccountManaged()); + } + + @Test + void userDiscoveryResponse_fromJson_managedAccount() throws IOException { + String json = "{\"ver\":\"1.0\",\"account_type\":\"Managed\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertTrue(response.isAccountManaged()); + assertFalse(response.isAccountFederated()); + } + + @Test + void userDiscoveryResponse_fromJson_unknownAccountType() throws IOException { + String json = "{\"ver\":\"2.0\",\"account_type\":\"Unknown\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertFalse(response.isAccountFederated()); + assertFalse(response.isAccountManaged()); + } + + @Test + void userDiscoveryResponse_isAccountFederated_nullAccountType() { + // Default-constructed response has null accountType + UserDiscoveryResponse response = new UserDiscoveryResponse(); + + assertFalse(response.isAccountFederated()); + assertFalse(response.isAccountManaged()); + } + + @Test + void userDiscoveryResponse_isAccountFederated_caseInsensitive() throws IOException { + String json = "{\"ver\":\"1.0\",\"account_type\":\"fEdErAtEd\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertTrue(response.isAccountFederated()); + } + + @Test + void userDiscoveryResponse_isAccountManaged_caseInsensitive() throws IOException { + String json = "{\"ver\":\"1.0\",\"account_type\":\"MANAGED\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertTrue(response.isAccountManaged()); + } + + @Test + void userDiscoveryResponse_toJson_roundTrip() throws IOException { + String json = "{\"ver\":\"1.0\"," + + "\"account_type\":\"Federated\"," + + "\"federation_metadata_url\":\"https://adfs.example.com/metadata\"," + + "\"federation_protocol\":\"WSTrust\"," + + "\"federation_active_auth_url\":\"https://adfs.example.com/active\"," + + "\"cloud_audience_urn\":\"urn:federation:MicrosoftOnline\"}"; + + UserDiscoveryResponse original; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + original = UserDiscoveryResponse.fromJson(reader); + } + + String serialized = writeToJson(original); + + UserDiscoveryResponse roundTripped; + try (JsonReader reader = JsonProviders.createReader(new StringReader(serialized))) { + roundTripped = UserDiscoveryResponse.fromJson(reader); + } + + assertEquals(original.accountType(), roundTripped.accountType()); + assertEquals(original.federationProtocol(), roundTripped.federationProtocol()); + assertEquals(original.federationMetadataUrl(), roundTripped.federationMetadataUrl()); + assertEquals(original.federationActiveAuthUrl(), roundTripped.federationActiveAuthUrl()); + assertEquals(original.cloudAudienceUrn(), roundTripped.cloudAudienceUrn()); + } + + @Test + void userDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"ver\":\"1.0\",\"account_type\":\"Managed\",\"extra_field\":\"ignored\"}"; + + UserDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = UserDiscoveryResponse.fromJson(reader); + } + + assertTrue(response.isAccountManaged()); + } + + // ========== OidcDiscoveryResponse ========== + + @Test + void oidcDiscoveryResponse_fromJson_allEndpoints() throws IOException { + String json = "{\"authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\"," + + "\"token_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/token\"," + + "\"device_authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode\"," + + "\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"}"; + + OidcDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = OidcDiscoveryResponse.fromJson(reader); + } + + assertEquals("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", response.authorizationEndpoint()); + assertEquals("https://login.microsoftonline.com/common/oauth2/v2.0/token", response.tokenEndpoint()); + assertEquals("https://login.microsoftonline.com/common/oauth2/v2.0/devicecode", response.deviceCodeEndpoint()); + assertEquals("https://login.microsoftonline.com/{tenantid}/v2.0", response.issuer()); + } + + @Test + void oidcDiscoveryResponse_fromJson_partialFields() throws IOException { + String json = "{\"authorization_endpoint\":\"https://example.com/auth\"," + + "\"token_endpoint\":\"https://example.com/token\"}"; + + OidcDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = OidcDiscoveryResponse.fromJson(reader); + } + + assertEquals("https://example.com/auth", response.authorizationEndpoint()); + assertEquals("https://example.com/token", response.tokenEndpoint()); + assertNull(response.deviceCodeEndpoint()); + assertNull(response.issuer()); + } + + @Test + void oidcDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"authorization_endpoint\":\"https://example.com/auth\"," + + "\"jwks_uri\":\"https://example.com/keys\"," + + "\"response_types_supported\":[\"code\"]}"; + + OidcDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = OidcDiscoveryResponse.fromJson(reader); + } + + assertEquals("https://example.com/auth", response.authorizationEndpoint()); + } + + @Test + void oidcDiscoveryResponse_toJson_doesNotIncludeIssuer() throws IOException { + // Note: toJson() intentionally omits the issuer field — this test documents that behavior + String json = "{\"authorization_endpoint\":\"https://example.com/auth\"," + + "\"token_endpoint\":\"https://example.com/token\"," + + "\"device_authorization_endpoint\":\"https://example.com/device\"," + + "\"issuer\":\"https://example.com/issuer\"}"; + + OidcDiscoveryResponse response; + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + response = OidcDiscoveryResponse.fromJson(reader); + } + + String output = writeToJson(response); + + assertTrue(output.contains("authorization_endpoint")); + assertTrue(output.contains("token_endpoint")); + assertTrue(output.contains("device_authorization_endpoint")); + // toJson does not write the issuer field + assertFalse(output.contains("\"issuer\"")); + } + + // ========== RequestedClaimAdditionalInfo ========== + + @Test + void requestedClaimAdditionalInfo_constructorAndGetters() { + List values = Arrays.asList("val1", "val2"); + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, "single", values); + + assertTrue(info.isEssential()); + assertEquals("single", info.getValue()); + assertEquals(Arrays.asList("val1", "val2"), info.getValues()); + } + + @Test + void requestedClaimAdditionalInfo_setters() { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); + + info.setEssential(true); + info.setValue("updated"); + info.setValues(Arrays.asList("a", "b")); + + assertTrue(info.isEssential()); + assertEquals("updated", info.getValue()); + assertEquals(Arrays.asList("a", "b"), info.getValues()); + } + + @Test + void requestedClaimAdditionalInfo_fromJson_allFields() throws IOException { + // Wrap in outer object since fromJson expects to be positioned in a field context + String json = "{\"essential\":true,\"value\":\"test-value\",\"values\":[\"v1\",\"v2\"]}"; + + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + info = info.fromJson(reader); + } + + assertTrue(info.isEssential()); + assertEquals("test-value", info.getValue()); + assertEquals(Arrays.asList("v1", "v2"), info.getValues()); + } + + @Test + void requestedClaimAdditionalInfo_fromJson_essentialOnly() throws IOException { + String json = "{\"essential\":true}"; + + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + info = info.fromJson(reader); + } + + assertTrue(info.isEssential()); + assertNull(info.getValue()); + assertNull(info.getValues()); + } + + @Test + void requestedClaimAdditionalInfo_toJson_essentialTrue() throws IOException { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, null, null); + + String output = writeToJson(info); + + assertTrue(output.contains("\"essential\":true")); + assertFalse(output.contains("\"value\"")); + assertFalse(output.contains("\"values\"")); + } + + @Test + void requestedClaimAdditionalInfo_toJson_essentialFalseOmitted() throws IOException { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, "v", null); + + String output = writeToJson(info); + + // essential=false is omitted from serialization + assertFalse(output.contains("essential")); + assertTrue(output.contains("\"value\":\"v\"")); + } + + @Test + void requestedClaimAdditionalInfo_toJson_withValues() throws IOException { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo( + false, null, Arrays.asList("x", "y")); + + String output = writeToJson(info); + + assertTrue(output.contains("\"values\"")); + assertTrue(output.contains("\"x\"")); + assertTrue(output.contains("\"y\"")); + } + + @Test + void requestedClaimAdditionalInfo_toJson_emptyValuesOmitted() throws IOException { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo( + false, null, Arrays.asList()); + + String output = writeToJson(info); + + // Empty list should be omitted (values != null but isEmpty()) + assertFalse(output.contains("values")); + } + + // ========== RequestedClaim ========== + + @Test + void requestedClaim_constructorAndGetter() { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, "val", null); + RequestedClaim claim = new RequestedClaim("acr", info); + + assertEquals("acr", claim.name); + assertEquals(info, claim.getRequestedClaimAdditionalInfo()); + } + + @Test + void requestedClaim_defaultConstructor() { + RequestedClaim claim = new RequestedClaim(); + + assertNull(claim.name); + assertNull(claim.getRequestedClaimAdditionalInfo()); + } + + @Test + void requestedClaim_setter() { + RequestedClaim claim = new RequestedClaim(); + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); + + claim.setRequestedClaimAdditionalInfo(info); + + assertEquals(info, claim.getRequestedClaimAdditionalInfo()); + } + + @Test + void requestedClaim_any_returnsCorrectMap() { + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, "v", null); + RequestedClaim claim = new RequestedClaim("email", info); + + Map result = claim.any(); + + assertEquals(1, result.size()); + assertTrue(result.containsKey("email")); + assertEquals(info, result.get("email")); + } + + @Test + void requestedClaim_any_nullName() { + RequestedClaim claim = new RequestedClaim(null, null); + + Map result = claim.any(); + + assertEquals(1, result.size()); + assertTrue(result.containsKey(null)); + } + + @Test + void requestedClaim_toJson_withNameAndInfo_throwsDueToBadStringWrite() { + // Bug: RequestedClaim.toJson() calls writeString(name) inside an object context, + // which is invalid JSON (a raw string value where a field name is expected) + RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, null, null); + RequestedClaim claim = new RequestedClaim("sub", info); + + assertThrows(IllegalStateException.class, () -> writeToJson(claim), + "toJson writes a raw string in object context, causing IllegalStateException"); + } + + @Test + void requestedClaim_toJson_nullNameAndInfo_writesEmptyObject() throws IOException { + RequestedClaim claim = new RequestedClaim(null, null); + + String output = writeToJson(claim); + + // When name or info is null, toJson skips writing the content + assertEquals("{}", output); + } + + // ========== Helper ========== + + private > String writeToJson(T serializable) + throws IOException { + java.io.StringWriter sw = new java.io.StringWriter(); + try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { + serializable.toJson(writer); + } + return sw.toString(); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java index e8bbc9e7..43214e88 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java @@ -3,43 +3,140 @@ package com.microsoft.aad.msal4j; -import java.io.BufferedReader; -import java.io.FileReader; - +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.AfterAll; -import static org.junit.jupiter.api.Assertions.assertNotNull; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) +import static org.junit.jupiter.api.Assertions.*; + class WSTrustResponseTest { - @BeforeAll + private static final String AAD_TOKEN_ERROR_FILE = "/token-error.xml"; + + @BeforeEach void setup() { - System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); + System.setProperty("javax.xml.parsers.DocumentBuilderFactory", + "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); } - @AfterAll + @AfterEach void cleanup() { System.clearProperty("javax.xml.parsers.DocumentBuilderFactory"); } @Test - void testWSTrustResponseParseSuccess() throws Exception { - StringBuilder sb = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new FileReader((this - .getClass().getResource( - TestConfiguration.AAD_TOKEN_SUCCESS_FILE).getFile())))) { - String line = br.readLine(); + void parse_ValidToken_ReturnsResponseWithToken() throws Exception { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + + WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - while (line != null) { - sb.append(line); - sb.append(System.lineSeparator()); - line = br.readLine(); - } - } - WSTrustResponse response = WSTrustResponse.parse(sb.toString(), WSTrustVersion.WSTRUST13); assertNotNull(response); + assertNotNull(response.getToken(), "Parsed response should contain a token"); + assertNotNull(response.getTokenType(), "Parsed response should contain a token type"); + assertFalse(response.isErrorFound(), "Successful parse should not have error"); + } + + @Test + void parse_ValidToken_TokenTypeIsSaml1() throws Exception { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + + WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); + + assertEquals(WSTrustResponse.SAML1_ASSERTION, response.getTokenType()); + assertFalse(response.isTokenSaml2(), "SAML 1.0 token should not be detected as SAML 2"); + } + + @Test + void parse_ValidToken_TokenContainsSamlAssertion() throws Exception { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + + WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); + + assertTrue(response.getToken().contains("saml:Assertion"), + "Token should contain SAML assertion XML"); + } + + @Test + void parse_ErrorResponse_ThrowsMsalServiceException() { + String xml = TestHelper.readResource(WSTrustResponseTest.class, AAD_TOKEN_ERROR_FILE); + + MsalServiceException ex = assertThrows(MsalServiceException.class, + () -> WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13)); + + assertEquals(AuthenticationErrorCode.WSTRUST_SERVICE_ERROR, ex.errorCode()); + assertTrue(ex.getMessage().contains("ErrorCode:"), + "Exception message should contain error code info"); + assertTrue(ex.getMessage().contains("FaultMessage:"), + "Exception message should contain fault message info"); + } + + @Test + void parse_ErrorResponse_ContainsReasonAndSubcode() { + String xml = TestHelper.readResource(WSTrustResponseTest.class, AAD_TOKEN_ERROR_FILE); + + MsalServiceException ex = assertThrows(MsalServiceException.class, + () -> WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13)); + + // The error XML has subcode "a:RequestFailed" which splits to "RequestFailed" + assertTrue(ex.getMessage().contains("RequestFailed"), + "Exception should contain parsed error subcode"); + assertTrue(ex.getMessage().contains("MSIS3127"), + "Exception should contain the SOAP fault reason text"); + } + + @Test + void parse_UndefinedVersion_ThrowsException() { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + + // UNDEFINED version has empty XPaths which cause XPathExpressionException + assertThrows(Exception.class, + () -> WSTrustResponse.parse(xml, WSTrustVersion.UNDEFINED)); + } + + @Test + void isTokenSaml2_nonSaml1Type_returnsTrue() throws Exception { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); + + // Token from test XML is SAML 1.0 + assertFalse(response.isTokenSaml2()); + + // Any non-SAML1 type should return true for isTokenSaml2 + // We can verify the constant value + assertEquals("urn:oasis:names:tc:SAML:1.0:assertion", WSTrustResponse.SAML1_ASSERTION); + } + + @Test + void parse_ValidToken_GettersReturnExpectedValues() throws Exception { + String xml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + + WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); + + assertFalse(response.isErrorFound()); + assertNull(response.getFaultMessage()); + assertNull(response.getErrorCode()); + assertNotNull(response.getToken()); + assertFalse(response.getToken().isEmpty()); + } + + @Test + void innerXml_extractsChildContent() throws Exception { + // innerXml is package-private static, test it directly with a simple XML node + javax.xml.parsers.DocumentBuilder builder = + javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder(); + org.w3c.dom.Document doc = builder.parse( + new java.io.ByteArrayInputStream("text".getBytes())); + org.w3c.dom.Node root = doc.getDocumentElement(); + + String inner = WSTrustResponse.innerXml(root); + + assertTrue(inner.contains("text"), "innerXml should extract child content"); + assertTrue(inner.contains("child"), "innerXml should include child element tags"); } } From 6a048be1d657e0b32dd3ace125ee208fd1714071 Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 14:41:33 -0700 Subject: [PATCH 2/7] Credentials tests --- .../msal4j/ClientCertificatePkcs12Test.java | 2 - .../aad/msal4j/ClientCertificateTest.java | 85 +++++- .../aad/msal4j/ClientCredentialTest.java | 251 ++++++++++++++++++ 3 files changed, 333 insertions(+), 5 deletions(-) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificatePkcs12Test.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificatePkcs12Test.java index 5af514ac..5dd5a8d8 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificatePkcs12Test.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificatePkcs12Test.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -21,7 +20,6 @@ import java.util.Collections; @ExtendWith(MockitoExtension.class) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ClientCertificatePkcs12Test { private KeyStoreSpi keyStoreSpi; diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java index ba9c34ef..3398e64e 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCertificateTest.java @@ -5,9 +5,8 @@ import com.nimbusds.jwt.SignedJWT; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -20,11 +19,11 @@ import java.security.*; import java.security.cert.CertificateException; import java.security.interfaces.RSAPrivateKey; +import java.security.cert.X509Certificate; import java.util.*; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ClientCertificateTest { @Test @@ -338,4 +337,84 @@ public List getEncodedPublicKeyCertificateChain() { return Collections.emptyList(); } } + + // ========== ClientCertificate: SHA-1 Hash ========== + + @Test + void testPublicCertificateHash_Sha1() throws Exception { + IClientCertificate cert = ClientCredentialFactory.createFromCertificate( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + String sha1Hash = cert.publicCertificateHash(); + + assertNotNull(sha1Hash, "SHA-1 hash should not be null"); + assertFalse(sha1Hash.isEmpty(), "SHA-1 hash should not be empty"); + // Base64-encoded SHA-1 is 28 characters + assertEquals(28, sha1Hash.length(), "Base64-encoded SHA-1 should be 28 chars"); + } + + @Test + void testPublicCertificateHash_Sha256DiffersFromSha1() throws Exception { + IClientCertificate cert = ClientCredentialFactory.createFromCertificate( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + String sha1Hash = cert.publicCertificateHash(); + String sha256Hash = cert.publicCertificateHash256(); + + assertNotEquals(sha1Hash, sha256Hash, + "SHA-1 and SHA-256 hashes should be different"); + } + + // ========== ClientCertificate: Certificate Chain Encoding ========== + + @Test + void testGetEncodedPublicKeyCertificateChain_singleCert() throws Exception { + ClientCertificate cert = ClientCertificate.create( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + List chain = cert.getEncodedPublicKeyCertificateChain(); + + assertNotNull(chain); + assertEquals(1, chain.size(), "Single cert should produce chain of length 1"); + assertFalse(chain.get(0).isEmpty(), "Encoded cert should not be empty"); + } + + @Test + void testGetEncodedPublicKeyCertificateChain_multiCert() throws Exception { + // Create a chain with the same cert repeated (simulates a CA chain) + List certChain = Arrays.asList( + TestHelper.getX509Cert(), TestHelper.getX509Cert()); + ClientCertificate cert = new ClientCertificate(TestHelper.getPrivateKey(), certChain); + + List chain = cert.getEncodedPublicKeyCertificateChain(); + + assertEquals(2, chain.size(), "Chain with 2 certs should produce 2 encoded entries"); + } + + // ========== ClientCertificate: getAssertion ========== + + @Test + void testGetAssertion_nullAuthority_throwsNullPointerException() { + ClientCertificate cert = ClientCertificate.create( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + assertThrows(NullPointerException.class, + () -> cert.getAssertion(null, "client-id", false)); + } + + @Test + void testGetAssertion_aadAuthority_usesSha256() throws Exception { + ClientCertificate cert = ClientCertificate.create( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + Authority authority = Authority.createAuthority( + new java.net.URL("https://login.microsoftonline.com/tenant/")); + + String assertion = cert.getAssertion(authority, "client-id", false); + + assertNotNull(assertion, "Assertion should not be null"); + // Verify it's a valid JWT (3 dot-separated parts) + String[] parts = assertion.split("\\."); + assertEquals(3, parts.length, "JWT assertion should have 3 parts"); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCredentialTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCredentialTest.java index 5129d3a9..b4fbf27a 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCredentialTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ClientCredentialTest.java @@ -7,10 +7,13 @@ import java.util.HashMap; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -204,4 +207,252 @@ void acquireTokenClientCredentials_Callback() { assertNotEquals(assertion1, assertion2, "First and second assertions should be different"); assertNotEquals(assertion2, assertion3, "Second and third assertions should be different"); } + + // ========== ClientAssertion: Context-Aware Provider ========== + + @Test + void clientAssertion_contextAwareProvider_returnsAssertion() { + Function provider = options -> "context-assertion"; + ClientAssertion assertion = new ClientAssertion(provider); + + AssertionRequestOptions options = new AssertionRequestOptions("client-id", "https://endpoint", "/fmi"); + assertEquals("context-assertion", assertion.assertion(options)); + } + + @Test + void clientAssertion_contextAwareProvider_nullReturnThrows() { + Function provider = options -> null; + ClientAssertion assertion = new ClientAssertion(provider); + + AssertionRequestOptions options = new AssertionRequestOptions("client-id", "https://endpoint", null); + MsalClientException ex = assertThrows(MsalClientException.class, + () -> assertion.assertion(options)); + assertEquals(AuthenticationErrorCode.INVALID_JWT, ex.errorCode()); + } + + @Test + void clientAssertion_contextAwareProvider_emptyReturnThrows() { + Function provider = options -> ""; + ClientAssertion assertion = new ClientAssertion(provider); + + MsalClientException ex = assertThrows(MsalClientException.class, + () -> assertion.assertion(new AssertionRequestOptions(null, null, null))); + assertEquals(AuthenticationErrorCode.INVALID_JWT, ex.errorCode()); + } + + @Test + void clientAssertion_contextAwareProvider_exceptionWrapped() { + Function provider = options -> { + throw new RuntimeException("provider failed"); + }; + ClientAssertion assertion = new ClientAssertion(provider); + + MsalClientException ex = assertThrows(MsalClientException.class, + () -> assertion.assertion(new AssertionRequestOptions(null, null, null))); + assertTrue(ex.getCause() instanceof RuntimeException); + } + + @Test + void clientAssertion_contextAwareProvider_noArgAssertionDelegatesToOptions() { + // assertion() with no args should delegate to assertion(options) with empty options + Function provider = options -> "from-no-arg"; + ClientAssertion assertion = new ClientAssertion(provider); + + assertEquals("from-no-arg", assertion.assertion()); + } + + @Test + void clientAssertion_isContextAware_trueForFunctionProvider() { + Function provider = options -> "test"; + ClientAssertion assertion = new ClientAssertion(provider); + assertTrue(assertion.isContextAware()); + } + + @Test + void clientAssertion_isContextAware_falseForCallable() { + ClientAssertion assertion = new ClientAssertion((Callable) () -> "test"); + assertFalse(assertion.isContextAware()); + } + + @Test + void clientAssertion_isContextAware_falseForStaticString() { + ClientAssertion assertion = new ClientAssertion("static-assertion"); + assertFalse(assertion.isContextAware()); + } + + // ========== ClientAssertion: Callable Error Paths ========== + + @Test + void clientAssertion_callableReturnsNull_throwsMsalClientException() { + ClientAssertion assertion = new ClientAssertion((Callable) () -> null); + + MsalClientException ex = assertThrows(MsalClientException.class, assertion::assertion); + assertEquals(AuthenticationErrorCode.INVALID_JWT, ex.errorCode()); + } + + @Test + void clientAssertion_callableReturnsEmpty_throwsMsalClientException() { + ClientAssertion assertion = new ClientAssertion((Callable) () -> ""); + + MsalClientException ex = assertThrows(MsalClientException.class, assertion::assertion); + assertEquals(AuthenticationErrorCode.INVALID_JWT, ex.errorCode()); + } + + @Test + void clientAssertion_callableThrowsException_wrappedInMsalClientException() { + ClientAssertion assertion = new ClientAssertion((Callable) () -> { + throw new Exception("callable failed"); + }); + + MsalClientException ex = assertThrows(MsalClientException.class, assertion::assertion); + assertTrue(ex.getCause().getMessage().contains("callable failed")); + } + + @Test + void clientAssertion_nullCallable_throwsNullPointerException() { + assertThrows(NullPointerException.class, + () -> new ClientAssertion((Callable) null)); + } + + @Test + void clientAssertion_nullFunction_throwsNullPointerException() { + assertThrows(NullPointerException.class, + () -> new ClientAssertion((Function) null)); + } + + // ========== ClientAssertion: Equals & HashCode ========== + + @Test + void clientAssertion_equals_sameStaticAssertion() { + ClientAssertion a = new ClientAssertion("test-jwt"); + ClientAssertion b = new ClientAssertion("test-jwt"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void clientAssertion_equals_differentStaticAssertion() { + ClientAssertion a = new ClientAssertion("jwt-1"); + ClientAssertion b = new ClientAssertion("jwt-2"); + + assertNotEquals(a, b); + } + + @Test + void clientAssertion_equals_sameCallable() { + Callable callable = () -> "test"; + ClientAssertion a = new ClientAssertion(callable); + ClientAssertion b = new ClientAssertion(callable); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void clientAssertion_equals_differentCallables() { + ClientAssertion a = new ClientAssertion((Callable) () -> "test"); + ClientAssertion b = new ClientAssertion((Callable) () -> "test"); + + // Different callable instances are compared by identity, so they're not equal + assertNotEquals(a, b); + } + + @Test + void clientAssertion_equals_sameContextAwareProvider() { + Function provider = opts -> "test"; + ClientAssertion a = new ClientAssertion(provider); + ClientAssertion b = new ClientAssertion(provider); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void clientAssertion_equals_differentContextAwareProviders() { + ClientAssertion a = new ClientAssertion((Function) opts -> "test"); + ClientAssertion b = new ClientAssertion((Function) opts -> "test"); + + assertNotEquals(a, b); + } + + @Test + void clientAssertion_equals_selfAndNull() { + ClientAssertion a = new ClientAssertion("jwt"); + assertEquals(a, a); + assertFalse(a.equals(null)); + assertFalse(a.equals("not-a-ClientAssertion")); + } + + // ========== ClientSecret: Equals & HashCode ========== + + @Test + void clientSecret_equals_sameSecret() { + ClientSecret a = new ClientSecret("secret-1"); + ClientSecret b = new ClientSecret("secret-1"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void clientSecret_equals_differentSecret() { + ClientSecret a = new ClientSecret("secret-1"); + ClientSecret b = new ClientSecret("secret-2"); + + assertNotEquals(a, b); + } + + @Test + void clientSecret_equals_selfAndNull() { + ClientSecret a = new ClientSecret("secret"); + assertEquals(a, a); + assertFalse(a.equals(null)); + assertFalse(a.equals("not-a-ClientSecret")); + } + + // ========== ClientCredentialFactory ========== + + @Test + void clientCredentialFactory_createFromCertificateChain_validInput() { + ClientCertificate cert = ClientCertificate.create( + TestHelper.getPrivateKey(), TestHelper.getX509Cert()); + + assertNotNull(cert); + assertNotNull(cert.privateKey()); + } + + @Test + void clientCredentialFactory_createFromCertificateChain_nullKey() { + assertThrows(IllegalArgumentException.class, + () -> ClientCredentialFactory.createFromCertificateChain(null, + Collections.singletonList(TestHelper.getX509Cert()))); + } + + @Test + void clientCredentialFactory_createFromCertificateChain_nullChain() { + assertThrows(IllegalArgumentException.class, + () -> ClientCredentialFactory.createFromCertificateChain( + TestHelper.getPrivateKey(), null)); + } + + @Test + void clientCredentialFactory_createFromCertificateChain_emptyChain() { + assertThrows(IllegalArgumentException.class, + () -> ClientCredentialFactory.createFromCertificateChain( + TestHelper.getPrivateKey(), Collections.emptyList())); + } + + @Test + void clientCredentialFactory_createFromCallback_nullCallable() { + assertThrows(NullPointerException.class, + () -> ClientCredentialFactory.createFromCallback((Callable) null)); + } + + @Test + void clientCredentialFactory_createFromCallback_nullFunction() { + assertThrows(NullPointerException.class, + () -> ClientCredentialFactory.createFromCallback( + (Function) null)); + } } From 5acce6c2976de9b74b1e075847443bfc27bac75d Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 14:51:18 -0700 Subject: [PATCH 3/7] Instance discovery tests --- .../msal4j/InstanceDiscoveryParsingTest.java | 218 ++++++++++++++++ .../msal4j/ManagedIdentityParsingTest.java | 233 ++++++++++++++++++ .../SovereignCloudInstanceDiscoveryTest.java | 2 - 3 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java new file mode 100644 index 00000000..726c0580 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.*; + +class InstanceDiscoveryParsingTest { + + // ========== AadInstanceDiscoveryResponse ========== + + @Test + void aadInstanceDiscoveryResponse_fromJson_allFields() throws IOException { + String json = "{" + + "\"tenant_discovery_endpoint\":\"https://login.microsoftonline.com/tenant/.well-known/openid-configuration\"," + + "\"metadata\":[{" + + "\"preferred_network\":\"login.microsoftonline.com\"," + + "\"preferred_cache\":\"login.windows.net\"," + + "\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\"]" + + "}]," + + "\"error_description\":null," + + "\"error_codes\":null," + + "\"error\":null," + + "\"correlation_id\":\"corr-123\"" + + "}"; + + AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + assertEquals("https://login.microsoftonline.com/tenant/.well-known/openid-configuration", + response.tenantDiscoveryEndpoint()); + assertNotNull(response.metadata()); + assertEquals(1, response.metadata().size()); + assertEquals("login.microsoftonline.com", response.metadata().get(0).preferredNetwork()); + assertEquals("login.windows.net", response.metadata().get(0).preferredCache()); + assertEquals("corr-123", response.correlationId()); + } + + @Test + void aadInstanceDiscoveryResponse_fromJson_errorResponse() throws IOException { + String json = "{" + + "\"error\":\"invalid_instance\"," + + "\"error_description\":\"AADSTS50049: Unknown or invalid instance.\"," + + "\"error_codes\":[50049]," + + "\"correlation_id\":\"corr-err\"" + + "}"; + + AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + assertEquals("invalid_instance", response.error()); + assertEquals("AADSTS50049: Unknown or invalid instance.", response.errorDescription()); + assertNotNull(response.errorCodes()); + assertEquals(1, response.errorCodes().size()); + assertEquals(50049L, response.errorCodes().get(0).longValue()); + assertEquals("corr-err", response.correlationId()); + assertNull(response.tenantDiscoveryEndpoint()); + assertNull(response.metadata()); + } + + @Test + void aadInstanceDiscoveryResponse_fromJson_multipleMetadataEntries() throws IOException { + String json = "{" + + "\"tenant_discovery_endpoint\":\"https://endpoint\"," + + "\"metadata\":[" + + "{\"preferred_network\":\"login.microsoftonline.com\",\"preferred_cache\":\"login.windows.net\",\"aliases\":[\"login.microsoftonline.com\"]}," + + "{\"preferred_network\":\"login.chinacloudapi.cn\",\"preferred_cache\":\"login.chinacloudapi.cn\",\"aliases\":[\"login.chinacloudapi.cn\"]}" + + "]" + + "}"; + + AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + assertEquals(2, response.metadata().size()); + assertEquals("login.microsoftonline.com", response.metadata().get(0).preferredNetwork()); + assertEquals("login.chinacloudapi.cn", response.metadata().get(1).preferredNetwork()); + } + + @Test + void aadInstanceDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"error\":\"test\",\"api-version\":\"1.1\",\"extra\":true}"; + + AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + assertEquals("test", response.error()); + } + + @Test + void aadInstanceDiscoveryResponse_toJson_roundTrip() throws IOException { + String json = "{" + + "\"tenant_discovery_endpoint\":\"https://endpoint\"," + + "\"metadata\":[{" + + "\"preferred_network\":\"login.microsoftonline.com\"," + + "\"preferred_cache\":\"login.windows.net\"," + + "\"aliases\":[\"login.microsoftonline.com\"]" + + "}]," + + "\"error\":null," + + "\"correlation_id\":\"c-1\"" + + "}"; + + AadInstanceDiscoveryResponse original = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + String serialized = writeToJson(original); + + assertTrue(serialized.contains("tenant_discovery_endpoint")); + assertTrue(serialized.contains("login.microsoftonline.com")); + assertTrue(serialized.contains("correlation_id")); + } + + @Test + void aadInstanceDiscoveryResponse_getters_defaultNull() throws IOException { + String json = "{}"; + + AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + assertNull(response.tenantDiscoveryEndpoint()); + assertNull(response.metadata()); + assertNull(response.errorDescription()); + assertNull(response.errorCodes()); + assertNull(response.error()); + assertNull(response.correlationId()); + } + + // ========== InstanceDiscoveryMetadataEntry ========== + + @Test + void instanceDiscoveryMetadataEntry_fromJson_allFields() throws IOException { + String json = "{" + + "\"preferred_network\":\"login.microsoftonline.com\"," + + "\"preferred_cache\":\"login.windows.net\"," + + "\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\",\"login.microsoft.com\"]" + + "}"; + + InstanceDiscoveryMetadataEntry entry = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + + assertEquals("login.microsoftonline.com", entry.preferredNetwork()); + assertEquals("login.windows.net", entry.preferredCache()); + assertEquals(3, entry.aliases().size()); + assertTrue(entry.aliases().contains("login.microsoftonline.com")); + assertTrue(entry.aliases().contains("login.windows.net")); + assertTrue(entry.aliases().contains("login.microsoft.com")); + } + + @Test + void instanceDiscoveryMetadataEntry_constructorAndGetters() { + HashSet aliases = new HashSet<>(Arrays.asList("host1", "host2")); + InstanceDiscoveryMetadataEntry entry = new InstanceDiscoveryMetadataEntry( + "preferred-net", "preferred-cache", aliases); + + assertEquals("preferred-net", entry.preferredNetwork()); + assertEquals("preferred-cache", entry.preferredCache()); + assertEquals(aliases, entry.aliases()); + } + + @Test + void instanceDiscoveryMetadataEntry_defaultConstructor() { + InstanceDiscoveryMetadataEntry entry = new InstanceDiscoveryMetadataEntry(); + + assertNull(entry.preferredNetwork()); + assertNull(entry.preferredCache()); + assertNull(entry.aliases()); + } + + @Test + void instanceDiscoveryMetadataEntry_toJson_roundTrip() throws IOException { + String json = "{" + + "\"preferred_network\":\"login.microsoftonline.com\"," + + "\"preferred_cache\":\"login.windows.net\"," + + "\"aliases\":[\"login.microsoftonline.com\"]" + + "}"; + + InstanceDiscoveryMetadataEntry original = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + String serialized = writeToJson(original); + + InstanceDiscoveryMetadataEntry roundTripped = parseJson(serialized, InstanceDiscoveryMetadataEntry::fromJson); + + assertEquals(original.preferredNetwork(), roundTripped.preferredNetwork()); + assertEquals(original.preferredCache(), roundTripped.preferredCache()); + assertEquals(original.aliases(), roundTripped.aliases()); + } + + @Test + void instanceDiscoveryMetadataEntry_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"preferred_network\":\"host\",\"extra_field\":123}"; + + InstanceDiscoveryMetadataEntry entry = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + + assertEquals("host", entry.preferredNetwork()); + } + + // ========== Helpers ========== + + @FunctionalInterface + interface JsonParser { + T parse(JsonReader reader) throws IOException; + } + + private T parseJson(String json, JsonParser parser) throws IOException { + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + return parser.parse(reader); + } + } + + private > String writeToJson(T serializable) + throws IOException { + StringWriter sw = new StringWriter(); + try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { + serializable.toJson(writer); + } + return sw.toString(); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java new file mode 100644 index 00000000..d25b1997 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.*; + +class ManagedIdentityParsingTest { + + // ========== ManagedIdentityResponse ========== + + @Test + void managedIdentityResponse_fromJson_allFields() throws IOException { + String json = "{" + + "\"token_type\":\"Bearer\"," + + "\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOi...\"," + + "\"expires_on\":\"1717430400\"," + + "\"resource\":\"https://management.azure.com/\"," + + "\"client_id\":\"00000000-0000-0000-0000-000000000000\"" + + "}"; + + ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + + assertEquals("Bearer", response.getTokenType()); + assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOi...", response.getAccessToken()); + assertEquals("1717430400", response.getExpiresOn()); + assertEquals("https://management.azure.com/", response.getResource()); + assertEquals("00000000-0000-0000-0000-000000000000", response.getClientId()); + } + + @Test + void managedIdentityResponse_fromJson_partialFields() throws IOException { + String json = "{\"access_token\":\"token\",\"expires_on\":\"123456\"}"; + + ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + + assertEquals("token", response.getAccessToken()); + assertEquals("123456", response.getExpiresOn()); + assertNull(response.getTokenType()); + assertNull(response.getResource()); + assertNull(response.getClientId()); + } + + @Test + void managedIdentityResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"access_token\":\"tok\",\"extra_field\":\"ignored\",\"nested\":{\"a\":1}}"; + + ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + + assertEquals("tok", response.getAccessToken()); + } + + @Test + void managedIdentityResponse_toJson_allFields() throws IOException { + ManagedIdentityResponse response = new ManagedIdentityResponse(); + response.tokenType = "Bearer"; + response.accessToken = "test-token"; + response.expiresOn = "9999999"; + response.resource = "https://vault.azure.net/"; + response.clientId = "client-123"; + + String output = writeToJson(response); + + assertTrue(output.contains("\"token_type\":\"Bearer\"")); + assertTrue(output.contains("\"access_token\":\"test-token\"")); + assertTrue(output.contains("\"expires_on\":\"9999999\"")); + assertTrue(output.contains("\"resource\":\"https://vault.azure.net/\"")); + assertTrue(output.contains("\"client_id\":\"client-123\"")); + } + + @Test + void managedIdentityResponse_toJson_roundTrip() throws IOException { + String json = "{" + + "\"token_type\":\"Bearer\"," + + "\"access_token\":\"round-trip-token\"," + + "\"expires_on\":\"12345\"," + + "\"resource\":\"https://api.example.com/\"," + + "\"client_id\":\"cid-456\"" + + "}"; + + ManagedIdentityResponse original = parseJson(json, ManagedIdentityResponse::fromJson); + String serialized = writeToJson(original); + ManagedIdentityResponse roundTripped = parseJson(serialized, ManagedIdentityResponse::fromJson); + + assertEquals(original.getTokenType(), roundTripped.getTokenType()); + assertEquals(original.getAccessToken(), roundTripped.getAccessToken()); + assertEquals(original.getExpiresOn(), roundTripped.getExpiresOn()); + assertEquals(original.getResource(), roundTripped.getResource()); + assertEquals(original.getClientId(), roundTripped.getClientId()); + } + + @Test + void managedIdentityResponse_defaultConstructor_allNull() { + ManagedIdentityResponse response = new ManagedIdentityResponse(); + + assertNull(response.getTokenType()); + assertNull(response.getAccessToken()); + assertNull(response.getExpiresOn()); + assertNull(response.getResource()); + assertNull(response.getClientId()); + } + + // ========== ManagedIdentityErrorResponse ========== + + @Test + void managedIdentityErrorResponse_fromJson_simpleError() throws IOException { + String json = "{" + + "\"error\":\"invalid_resource\"," + + "\"error_description\":\"The resource requested is invalid.\"," + + "\"message\":\"Identity not found\"," + + "\"correlationId\":\"corr-789\"" + + "}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + + assertEquals("invalid_resource", response.getError()); + assertEquals("The resource requested is invalid.", response.getErrorDescription()); + assertEquals("Identity not found", response.getMessage()); + assertEquals("corr-789", response.getCorrelationId()); + } + + @Test + void managedIdentityErrorResponse_fromJson_nestedErrorObject() throws IOException { + // Some MI endpoints return error as a nested JSON object with code/message + String json = "{" + + "\"error\":{\"code\":\"ManagedIdentityCredential\",\"message\":\"Managed identity unavailable\"}," + + "\"correlationId\":\"corr-nested\"" + + "}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + + assertEquals("ManagedIdentityCredential", response.getError()); + assertEquals("Managed identity unavailable", response.getMessage()); + assertEquals("corr-nested", response.getCorrelationId()); + } + + @Test + void managedIdentityErrorResponse_fromJson_errorAsString() throws IOException { + String json = "{\"error\":\"unauthorized_client\"}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + + assertEquals("unauthorized_client", response.getError()); + } + + @Test + void managedIdentityErrorResponse_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"error\":\"test\",\"unknown_field\":42,\"nested_unknown\":{\"a\":1}}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + + assertEquals("test", response.getError()); + } + + @Test + void managedIdentityErrorResponse_fromJson_emptyObject() throws IOException { + String json = "{}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + + assertNull(response.getError()); + assertNull(response.getErrorDescription()); + assertNull(response.getMessage()); + assertNull(response.getCorrelationId()); + } + + @Test + void managedIdentityErrorResponse_toJson_allFields() throws IOException { + String json = "{" + + "\"message\":\"Identity not found\"," + + "\"correlationId\":\"c-1\"," + + "\"error\":\"not_found\"," + + "\"error_description\":\"desc\"" + + "}"; + + ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + String output = writeToJson(response); + + assertTrue(output.contains("\"message\":\"Identity not found\"")); + assertTrue(output.contains("\"correlationId\":\"c-1\"")); + assertTrue(output.contains("\"error\":\"not_found\"")); + assertTrue(output.contains("\"error_description\":\"desc\"")); + } + + @Test + void managedIdentityErrorResponse_toJson_roundTrip() throws IOException { + String json = "{" + + "\"message\":\"msg\"," + + "\"correlationId\":\"corr\"," + + "\"error\":\"err\"," + + "\"error_description\":\"desc\"" + + "}"; + + ManagedIdentityErrorResponse original = parseJson(json, ManagedIdentityErrorResponse::fromJson); + String serialized = writeToJson(original); + ManagedIdentityErrorResponse roundTripped = parseJson(serialized, ManagedIdentityErrorResponse::fromJson); + + assertEquals(original.getError(), roundTripped.getError()); + assertEquals(original.getErrorDescription(), roundTripped.getErrorDescription()); + assertEquals(original.getMessage(), roundTripped.getMessage()); + assertEquals(original.getCorrelationId(), roundTripped.getCorrelationId()); + } + + // ========== Helpers ========== + + @FunctionalInterface + interface JsonParser { + T parse(JsonReader reader) throws IOException; + } + + private T parseJson(String json, JsonParser parser) throws IOException { + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + return parser.parse(reader); + } + } + + private > String writeToJson(T serializable) + throws IOException { + StringWriter sw = new StringWriter(); + try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { + serializable.toJson(writer); + } + return sw.toString(); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java index 4f45515b..9b802289 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/SovereignCloudInstanceDiscoveryTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentCaptor; import java.util.ArrayList; @@ -24,7 +23,6 @@ * mock only the HTTP layer, and verify that all HTTP requests are routed to the * correct sovereign host — not to login.microsoftonline.com or any other host. */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class SovereignCloudInstanceDiscoveryTest { private static final String SOVEREIGN_HOST = "login.sovcloud-identity.fr"; From bc4929231ddeb5e7e2fd05ad8af6b699c0089077 Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 15:04:01 -0700 Subject: [PATCH 4/7] WSTrust tests --- .../aad/msal4j/WSTrustRequestTest.java | 60 ++- .../aad/msal4j/WsTrustFederationTest.java | 347 ++++++++++++++++++ 2 files changed, 405 insertions(+), 2 deletions(-) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustRequestTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustRequestTest.java index 31a953e2..53d080a5 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustRequestTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustRequestTest.java @@ -5,11 +5,9 @@ import org.apache.commons.text.StringEscapeUtils; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class WSTrustRequestTest { @Test @@ -54,4 +52,62 @@ void escapeXMLElementDataTest() { assertEquals(StringEscapeUtils.escapeXml10(DATA_TO_ESCAPE), WSTrustRequest.escapeXMLElementData(DATA_TO_ESCAPE)); } + + @Test + void buildMessage_wsTrust13_usesTrust13Namespaces() { + String msg = WSTrustRequest.buildMessage("https://adfs.example.com", "user", + "pass", WSTrustVersion.WSTRUST13, "urn:federation:MicrosoftOnline").toString(); + + assertTrue(msg.contains("http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue"), + "WSTrust 1.3 should use trust/200512 action"); + assertTrue(msg.contains("xmlns:trust='http://docs.oasis-open.org/ws-sx/ws-trust/200512'"), + "WSTrust 1.3 should use trust/200512 namespace"); + assertTrue(msg.contains("http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"), + "WSTrust 1.3 should use trust/200512 Bearer key type"); + } + + @Test + void buildMessage_wsTrust2005_uses2005Namespaces() { + String msg = WSTrustRequest.buildMessage("https://adfs.example.com", "user", + "pass", WSTrustVersion.WSTRUST2005, "urn:federation:MicrosoftOnline").toString(); + + assertTrue(msg.contains("http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue"), + "WSTrust 2005 should use ws/2005 action"); + assertTrue(msg.contains("xmlns:trust='http://schemas.xmlsoap.org/ws/2005/02/trust'"), + "WSTrust 2005 should use ws/2005 namespace"); + assertTrue(msg.contains("http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey"), + "WSTrust 2005 should use NoProofKey"); + } + + @Test + void buildMessage_withCredentials_includesSecurityHeader() { + String msg = WSTrustRequest.buildMessage("address", "testuser", + "testpass", WSTrustVersion.WSTRUST13, null).toString(); + + assertTrue(msg.contains("testuser")); + assertTrue(msg.contains("testpass")); + assertTrue(msg.contains("")); + assertTrue(msg.contains("")); + } + + @Test + void buildMessage_withSpecialCharsInCredentials_escapesXml() { + String msg = WSTrustRequest.buildMessage("address", "user&<>", + "pass'\"", WSTrustVersion.WSTRUST13, null).toString(); + + assertTrue(msg.contains("user&<>"), "Username should be XML-escaped"); + assertTrue(msg.contains("pass'""), "Password should be XML-escaped"); + } + + @Test + void escapeXMLElementData_noSpecialChars_returnsUnchanged() { + assertEquals("simple text 123", WSTrustRequest.escapeXMLElementData("simple text 123")); + } + + @Test + void escapeXMLElementData_emptyString() { + assertEquals("", WSTrustRequest.escapeXMLElementData("")); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java new file mode 100644 index 00000000..3213234f --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class WsTrustFederationTest { + + // ========== Credential ========== + + @Test + void credential_fromJson_allFields() throws IOException { + String json = "{" + + "\"home_account_id\":\"uid.utid\"," + + "\"environment\":\"login.microsoftonline.com\"," + + "\"client_id\":\"client-123\"," + + "\"secret\":\"secret-token\"," + + "\"user_assertion_hash\":\"hash-abc\"" + + "}"; + + Credential cred = parseJson(json, Credential::fromJson); + + assertEquals("uid.utid", cred.homeAccountId()); + assertEquals("login.microsoftonline.com", cred.environment()); + assertEquals("client-123", cred.clientId()); + assertEquals("secret-token", cred.secret()); + assertEquals("hash-abc", cred.userAssertionHash()); + } + + @Test + void credential_fromJson_partialFields() throws IOException { + String json = "{\"home_account_id\":\"uid\",\"environment\":\"env\"}"; + + Credential cred = parseJson(json, Credential::fromJson); + + assertEquals("uid", cred.homeAccountId()); + assertEquals("env", cred.environment()); + assertNull(cred.clientId()); + assertNull(cred.secret()); + assertNull(cred.userAssertionHash()); + } + + @Test + void credential_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"home_account_id\":\"uid\",\"extra\":123}"; + + Credential cred = parseJson(json, Credential::fromJson); + + assertEquals("uid", cred.homeAccountId()); + } + + @Test + void credential_settersAndGetters() { + Credential cred = new Credential(); + + cred.homeAccountId("home-1"); + cred.environment("env-1"); + cred.clientId("client-1"); + cred.secret("secret-1"); + cred.userAssertionHash("hash-1"); + + assertEquals("home-1", cred.homeAccountId()); + assertEquals("env-1", cred.environment()); + assertEquals("client-1", cred.clientId()); + assertEquals("secret-1", cred.secret()); + assertEquals("hash-1", cred.userAssertionHash()); + } + + @Test + void credential_toJson_allFields() throws IOException { + Credential cred = new Credential(); + cred.homeAccountId("uid.utid"); + cred.environment("login.microsoftonline.com"); + cred.clientId("cid"); + cred.secret("sec"); + cred.userAssertionHash("hash"); + + String output = writeToJson(cred); + + assertTrue(output.contains("\"home_account_id\":\"uid.utid\"")); + assertTrue(output.contains("\"environment\":\"login.microsoftonline.com\"")); + assertTrue(output.contains("\"client_id\":\"cid\"")); + assertTrue(output.contains("\"secret\":\"sec\"")); + assertTrue(output.contains("\"user_assertion_hash\":\"hash\"")); + } + + @Test + void credential_toJson_roundTrip() throws IOException { + String json = "{" + + "\"home_account_id\":\"uid\"," + + "\"environment\":\"env\"," + + "\"client_id\":\"cid\"," + + "\"secret\":\"sec\"," + + "\"user_assertion_hash\":\"hash\"" + + "}"; + + Credential original = parseJson(json, Credential::fromJson); + String serialized = writeToJson(original); + Credential roundTripped = parseJson(serialized, Credential::fromJson); + + assertEquals(original.homeAccountId(), roundTripped.homeAccountId()); + assertEquals(original.environment(), roundTripped.environment()); + assertEquals(original.clientId(), roundTripped.clientId()); + assertEquals(original.secret(), roundTripped.secret()); + assertEquals(original.userAssertionHash(), roundTripped.userAssertionHash()); + } + + // ========== BindingPolicy ========== + + @Test + void bindingPolicy_singleArgConstructor() { + BindingPolicy policy = new BindingPolicy("policy-value"); + + assertEquals("policy-value", policy.getValue()); + assertNull(policy.getUrl()); + assertNull(policy.getVersion()); + } + + @Test + void bindingPolicy_twoArgConstructor() { + BindingPolicy policy = new BindingPolicy("https://adfs.example.com/trust", WSTrustVersion.WSTRUST13); + + assertEquals("https://adfs.example.com/trust", policy.getUrl()); + assertEquals(WSTrustVersion.WSTRUST13, policy.getVersion()); + assertNull(policy.getValue()); + } + + @Test + void bindingPolicy_settersAndGetters() { + BindingPolicy policy = new BindingPolicy("initial"); + + policy.setValue("updated-value"); + policy.setUrl("https://new-url"); + policy.setVersion(WSTrustVersion.WSTRUST2005); + + assertEquals("updated-value", policy.getValue()); + assertEquals("https://new-url", policy.getUrl()); + assertEquals(WSTrustVersion.WSTRUST2005, policy.getVersion()); + } + + @Test + void bindingPolicy_versionUndefined() { + BindingPolicy policy = new BindingPolicy("https://url", WSTrustVersion.UNDEFINED); + + assertEquals(WSTrustVersion.UNDEFINED, policy.getVersion()); + } + + // ========== IntegratedWindowsAuthorizationGrant ========== + + @Test + void integratedWindowsAuthGrant_constructor() { + IntegratedWindowsAuthorizationGrant grant = new IntegratedWindowsAuthorizationGrant( + Collections.singleton("openid"), "user@domain.com", null); + + assertEquals("user@domain.com", grant.getUserName()); + assertNull(grant.toParameters()); + } + + @Test + void integratedWindowsAuthGrant_withClaims() { + ClaimsRequest claims = new ClaimsRequest(); + IntegratedWindowsAuthorizationGrant grant = new IntegratedWindowsAuthorizationGrant( + Collections.singleton("profile"), "admin@corp.net", claims); + + assertEquals("admin@corp.net", grant.getUserName()); + } + + // ========== IdToken ========== + + @Test + void idToken_fromJson_allFields() throws IOException { + String json = "{" + + "\"iss\":\"https://login.microsoftonline.com/tenant/v2.0\"," + + "\"sub\":\"sub-123\"," + + "\"aud\":\"client-id\"," + + "\"exp\":1717430400," + + "\"iat\":1717426800," + + "\"nbf\":1717426800," + + "\"name\":\"Test User\"," + + "\"preferred_username\":\"testuser@example.com\"," + + "\"oid\":\"oid-456\"," + + "\"tid\":\"tid-789\"," + + "\"upn\":\"testuser@example.com\"," + + "\"unique_name\":\"testuser\"" + + "}"; + + IdToken idToken = parseJson(json, IdToken::fromJson); + + assertEquals("https://login.microsoftonline.com/tenant/v2.0", idToken.issuer); + assertEquals("sub-123", idToken.subject); + assertEquals("client-id", idToken.audience); + assertEquals(1717430400L, idToken.expirationTime.longValue()); + assertEquals(1717426800L, idToken.issuedAt.longValue()); + assertEquals(1717426800L, idToken.notBefore.longValue()); + assertEquals("Test User", idToken.name); + assertEquals("testuser@example.com", idToken.preferredUsername); + assertEquals("oid-456", idToken.objectIdentifier); + assertEquals("tid-789", idToken.tenantIdentifier); + assertEquals("testuser@example.com", idToken.upn); + assertEquals("testuser", idToken.uniqueName); + } + + @Test + void idToken_fromJson_partialFields() throws IOException { + String json = "{\"iss\":\"issuer\",\"sub\":\"subject\",\"aud\":\"audience\"}"; + + IdToken idToken = parseJson(json, IdToken::fromJson); + + assertEquals("issuer", idToken.issuer); + assertEquals("subject", idToken.subject); + assertEquals("audience", idToken.audience); + assertNull(idToken.expirationTime); + assertNull(idToken.issuedAt); + assertNull(idToken.notBefore); + assertNull(idToken.name); + assertNull(idToken.preferredUsername); + assertNull(idToken.objectIdentifier); + assertNull(idToken.tenantIdentifier); + assertNull(idToken.upn); + assertNull(idToken.uniqueName); + } + + @Test + void idToken_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"iss\":\"issuer\",\"nonce\":\"abc\",\"email\":\"user@test.com\"}"; + + IdToken idToken = parseJson(json, IdToken::fromJson); + + assertEquals("issuer", idToken.issuer); + } + + @Test + void idToken_toJson_allFields() throws IOException { + IdToken idToken = new IdToken(); + idToken.issuer = "iss"; + idToken.subject = "sub"; + idToken.audience = "aud"; + idToken.expirationTime = 99999L; + idToken.issuedAt = 11111L; + idToken.notBefore = 11111L; + idToken.name = "User Name"; + idToken.preferredUsername = "user@example.com"; + idToken.objectIdentifier = "oid"; + idToken.tenantIdentifier = "tid"; + idToken.upn = "user@example.com"; + idToken.uniqueName = "unique"; + + String output = writeToJson(idToken); + + assertTrue(output.contains("\"iss\":\"iss\"")); + assertTrue(output.contains("\"sub\":\"sub\"")); + assertTrue(output.contains("\"aud\":\"aud\"")); + assertTrue(output.contains("\"name\":\"User Name\"")); + assertTrue(output.contains("\"preferred_username\":\"user@example.com\"")); + assertTrue(output.contains("\"oid\":\"oid\"")); + assertTrue(output.contains("\"tid\":\"tid\"")); + assertTrue(output.contains("\"upn\":\"user@example.com\"")); + assertTrue(output.contains("\"unique_name\":\"unique\"")); + } + + @Test + void idToken_toJson_roundTrip() throws IOException { + String json = "{" + + "\"iss\":\"issuer\"," + + "\"sub\":\"subject\"," + + "\"aud\":\"audience\"," + + "\"exp\":1234567890," + + "\"iat\":1234567800," + + "\"nbf\":1234567800," + + "\"name\":\"Test\"," + + "\"preferred_username\":\"test@test.com\"," + + "\"oid\":\"oid-1\"," + + "\"tid\":\"tid-1\"," + + "\"upn\":\"upn@test.com\"," + + "\"unique_name\":\"unique-1\"" + + "}"; + + IdToken original = parseJson(json, IdToken::fromJson); + String serialized = writeToJson(original); + IdToken roundTripped = parseJson(serialized, IdToken::fromJson); + + assertEquals(original.issuer, roundTripped.issuer); + assertEquals(original.subject, roundTripped.subject); + assertEquals(original.audience, roundTripped.audience); + assertEquals(original.expirationTime, roundTripped.expirationTime); + assertEquals(original.name, roundTripped.name); + assertEquals(original.preferredUsername, roundTripped.preferredUsername); + assertEquals(original.objectIdentifier, roundTripped.objectIdentifier); + assertEquals(original.tenantIdentifier, roundTripped.tenantIdentifier); + assertEquals(original.upn, roundTripped.upn); + assertEquals(original.uniqueName, roundTripped.uniqueName); + } + + // ========== WSTrustVersion ========== + + @Test + void wsTrustVersion_pathValues() { + assertFalse(WSTrustVersion.WSTRUST13.responseTokenTypePath().isEmpty()); + assertFalse(WSTrustVersion.WSTRUST13.responseSecurityTokenPath().isEmpty()); + + assertFalse(WSTrustVersion.WSTRUST2005.responseTokenTypePath().isEmpty()); + assertFalse(WSTrustVersion.WSTRUST2005.responseSecurityTokenPath().isEmpty()); + + assertTrue(WSTrustVersion.UNDEFINED.responseTokenTypePath().isEmpty()); + assertTrue(WSTrustVersion.UNDEFINED.responseSecurityTokenPath().isEmpty()); + } + + @Test + void wsTrustVersion_wsTrust13_containsCorrectPaths() { + assertTrue(WSTrustVersion.WSTRUST13.responseTokenTypePath() + .contains("RequestSecurityTokenResponseCollection")); + assertTrue(WSTrustVersion.WSTRUST13.responseSecurityTokenPath() + .contains("RequestedSecurityToken")); + } + + // ========== Helpers ========== + + @FunctionalInterface + interface JsonParser { + T parse(JsonReader reader) throws IOException; + } + + private T parseJson(String json, JsonParser parser) throws IOException { + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + return parser.parse(reader); + } + } + + private > String writeToJson(T serializable) + throws IOException { + StringWriter sw = new StringWriter(); + try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { + serializable.toJson(writer); + } + return sw.toString(); + } +} From 2dcf8db3b4878b4245f449ac18a9082fa0e24ea2 Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 15:18:01 -0700 Subject: [PATCH 5/7] Minor fixes --- .../msal4j/InstanceDiscoveryParsingTest.java | 50 ++----- .../msal4j/ManagedIdentityParsingTest.java | 60 +++------ .../aad/msal4j/ResponseParsingTest.java | 125 +++++------------- .../com/microsoft/aad/msal4j/TestHelper.java | 35 +++++ .../aad/msal4j/WSTrustResponseTest.java | 88 +++++------- .../aad/msal4j/WsTrustFederationTest.java | 54 ++------ 6 files changed, 140 insertions(+), 272 deletions(-) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java index 726c0580..80ee135d 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/InstanceDiscoveryParsingTest.java @@ -3,13 +3,9 @@ package com.microsoft.aad.msal4j; -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; import java.util.Arrays; import java.util.HashSet; @@ -34,7 +30,7 @@ void aadInstanceDiscoveryResponse_fromJson_allFields() throws IOException { + "\"correlation_id\":\"corr-123\"" + "}"; - AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + AadInstanceDiscoveryResponse response = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); assertEquals("https://login.microsoftonline.com/tenant/.well-known/openid-configuration", response.tenantDiscoveryEndpoint()); @@ -54,7 +50,7 @@ void aadInstanceDiscoveryResponse_fromJson_errorResponse() throws IOException { + "\"correlation_id\":\"corr-err\"" + "}"; - AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + AadInstanceDiscoveryResponse response = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); assertEquals("invalid_instance", response.error()); assertEquals("AADSTS50049: Unknown or invalid instance.", response.errorDescription()); @@ -76,7 +72,7 @@ void aadInstanceDiscoveryResponse_fromJson_multipleMetadataEntries() throws IOEx + "]" + "}"; - AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + AadInstanceDiscoveryResponse response = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); assertEquals(2, response.metadata().size()); assertEquals("login.microsoftonline.com", response.metadata().get(0).preferredNetwork()); @@ -87,7 +83,7 @@ void aadInstanceDiscoveryResponse_fromJson_multipleMetadataEntries() throws IOEx void aadInstanceDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"error\":\"test\",\"api-version\":\"1.1\",\"extra\":true}"; - AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + AadInstanceDiscoveryResponse response = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); assertEquals("test", response.error()); } @@ -105,8 +101,8 @@ void aadInstanceDiscoveryResponse_toJson_roundTrip() throws IOException { + "\"correlation_id\":\"c-1\"" + "}"; - AadInstanceDiscoveryResponse original = parseJson(json, AadInstanceDiscoveryResponse::fromJson); - String serialized = writeToJson(original); + AadInstanceDiscoveryResponse original = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); + String serialized = TestHelper.writeToJson(original); assertTrue(serialized.contains("tenant_discovery_endpoint")); assertTrue(serialized.contains("login.microsoftonline.com")); @@ -117,7 +113,7 @@ void aadInstanceDiscoveryResponse_toJson_roundTrip() throws IOException { void aadInstanceDiscoveryResponse_getters_defaultNull() throws IOException { String json = "{}"; - AadInstanceDiscoveryResponse response = parseJson(json, AadInstanceDiscoveryResponse::fromJson); + AadInstanceDiscoveryResponse response = TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); assertNull(response.tenantDiscoveryEndpoint()); assertNull(response.metadata()); @@ -137,7 +133,7 @@ void instanceDiscoveryMetadataEntry_fromJson_allFields() throws IOException { + "\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\",\"login.microsoft.com\"]" + "}"; - InstanceDiscoveryMetadataEntry entry = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + InstanceDiscoveryMetadataEntry entry = TestHelper.parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); assertEquals("login.microsoftonline.com", entry.preferredNetwork()); assertEquals("login.windows.net", entry.preferredCache()); @@ -175,10 +171,10 @@ void instanceDiscoveryMetadataEntry_toJson_roundTrip() throws IOException { + "\"aliases\":[\"login.microsoftonline.com\"]" + "}"; - InstanceDiscoveryMetadataEntry original = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); - String serialized = writeToJson(original); + InstanceDiscoveryMetadataEntry original = TestHelper.parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + String serialized = TestHelper.writeToJson(original); - InstanceDiscoveryMetadataEntry roundTripped = parseJson(serialized, InstanceDiscoveryMetadataEntry::fromJson); + InstanceDiscoveryMetadataEntry roundTripped = TestHelper.parseJson(serialized, InstanceDiscoveryMetadataEntry::fromJson); assertEquals(original.preferredNetwork(), roundTripped.preferredNetwork()); assertEquals(original.preferredCache(), roundTripped.preferredCache()); @@ -189,30 +185,8 @@ void instanceDiscoveryMetadataEntry_toJson_roundTrip() throws IOException { void instanceDiscoveryMetadataEntry_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"preferred_network\":\"host\",\"extra_field\":123}"; - InstanceDiscoveryMetadataEntry entry = parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); + InstanceDiscoveryMetadataEntry entry = TestHelper.parseJson(json, InstanceDiscoveryMetadataEntry::fromJson); assertEquals("host", entry.preferredNetwork()); } - - // ========== Helpers ========== - - @FunctionalInterface - interface JsonParser { - T parse(JsonReader reader) throws IOException; - } - - private T parseJson(String json, JsonParser parser) throws IOException { - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - return parser.parse(reader); - } - } - - private > String writeToJson(T serializable) - throws IOException { - StringWriter sw = new StringWriter(); - try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { - serializable.toJson(writer); - } - return sw.toString(); - } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java index d25b1997..ea39bb27 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityParsingTest.java @@ -3,13 +3,9 @@ package com.microsoft.aad.msal4j; -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; import static org.junit.jupiter.api.Assertions.*; @@ -27,7 +23,7 @@ void managedIdentityResponse_fromJson_allFields() throws IOException { + "\"client_id\":\"00000000-0000-0000-0000-000000000000\"" + "}"; - ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + ManagedIdentityResponse response = TestHelper.parseJson(json, ManagedIdentityResponse::fromJson); assertEquals("Bearer", response.getTokenType()); assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOi...", response.getAccessToken()); @@ -40,7 +36,7 @@ void managedIdentityResponse_fromJson_allFields() throws IOException { void managedIdentityResponse_fromJson_partialFields() throws IOException { String json = "{\"access_token\":\"token\",\"expires_on\":\"123456\"}"; - ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + ManagedIdentityResponse response = TestHelper.parseJson(json, ManagedIdentityResponse::fromJson); assertEquals("token", response.getAccessToken()); assertEquals("123456", response.getExpiresOn()); @@ -53,7 +49,7 @@ void managedIdentityResponse_fromJson_partialFields() throws IOException { void managedIdentityResponse_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"access_token\":\"tok\",\"extra_field\":\"ignored\",\"nested\":{\"a\":1}}"; - ManagedIdentityResponse response = parseJson(json, ManagedIdentityResponse::fromJson); + ManagedIdentityResponse response = TestHelper.parseJson(json, ManagedIdentityResponse::fromJson); assertEquals("tok", response.getAccessToken()); } @@ -67,7 +63,7 @@ void managedIdentityResponse_toJson_allFields() throws IOException { response.resource = "https://vault.azure.net/"; response.clientId = "client-123"; - String output = writeToJson(response); + String output = TestHelper.writeToJson(response); assertTrue(output.contains("\"token_type\":\"Bearer\"")); assertTrue(output.contains("\"access_token\":\"test-token\"")); @@ -86,9 +82,9 @@ void managedIdentityResponse_toJson_roundTrip() throws IOException { + "\"client_id\":\"cid-456\"" + "}"; - ManagedIdentityResponse original = parseJson(json, ManagedIdentityResponse::fromJson); - String serialized = writeToJson(original); - ManagedIdentityResponse roundTripped = parseJson(serialized, ManagedIdentityResponse::fromJson); + ManagedIdentityResponse original = TestHelper.parseJson(json, ManagedIdentityResponse::fromJson); + String serialized = TestHelper.writeToJson(original); + ManagedIdentityResponse roundTripped = TestHelper.parseJson(serialized, ManagedIdentityResponse::fromJson); assertEquals(original.getTokenType(), roundTripped.getTokenType()); assertEquals(original.getAccessToken(), roundTripped.getAccessToken()); @@ -119,7 +115,7 @@ void managedIdentityErrorResponse_fromJson_simpleError() throws IOException { + "\"correlationId\":\"corr-789\"" + "}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); assertEquals("invalid_resource", response.getError()); assertEquals("The resource requested is invalid.", response.getErrorDescription()); @@ -135,7 +131,7 @@ void managedIdentityErrorResponse_fromJson_nestedErrorObject() throws IOExceptio + "\"correlationId\":\"corr-nested\"" + "}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); assertEquals("ManagedIdentityCredential", response.getError()); assertEquals("Managed identity unavailable", response.getMessage()); @@ -146,7 +142,7 @@ void managedIdentityErrorResponse_fromJson_nestedErrorObject() throws IOExceptio void managedIdentityErrorResponse_fromJson_errorAsString() throws IOException { String json = "{\"error\":\"unauthorized_client\"}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); assertEquals("unauthorized_client", response.getError()); } @@ -155,7 +151,7 @@ void managedIdentityErrorResponse_fromJson_errorAsString() throws IOException { void managedIdentityErrorResponse_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"error\":\"test\",\"unknown_field\":42,\"nested_unknown\":{\"a\":1}}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); assertEquals("test", response.getError()); } @@ -164,7 +160,7 @@ void managedIdentityErrorResponse_fromJson_unknownFieldsSkipped() throws IOExcep void managedIdentityErrorResponse_fromJson_emptyObject() throws IOException { String json = "{}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); assertNull(response.getError()); assertNull(response.getErrorDescription()); @@ -181,8 +177,8 @@ void managedIdentityErrorResponse_toJson_allFields() throws IOException { + "\"error_description\":\"desc\"" + "}"; - ManagedIdentityErrorResponse response = parseJson(json, ManagedIdentityErrorResponse::fromJson); - String output = writeToJson(response); + ManagedIdentityErrorResponse response = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); + String output = TestHelper.writeToJson(response); assertTrue(output.contains("\"message\":\"Identity not found\"")); assertTrue(output.contains("\"correlationId\":\"c-1\"")); @@ -199,35 +195,13 @@ void managedIdentityErrorResponse_toJson_roundTrip() throws IOException { + "\"error_description\":\"desc\"" + "}"; - ManagedIdentityErrorResponse original = parseJson(json, ManagedIdentityErrorResponse::fromJson); - String serialized = writeToJson(original); - ManagedIdentityErrorResponse roundTripped = parseJson(serialized, ManagedIdentityErrorResponse::fromJson); + ManagedIdentityErrorResponse original = TestHelper.parseJson(json, ManagedIdentityErrorResponse::fromJson); + String serialized = TestHelper.writeToJson(original); + ManagedIdentityErrorResponse roundTripped = TestHelper.parseJson(serialized, ManagedIdentityErrorResponse::fromJson); assertEquals(original.getError(), roundTripped.getError()); assertEquals(original.getErrorDescription(), roundTripped.getErrorDescription()); assertEquals(original.getMessage(), roundTripped.getMessage()); assertEquals(original.getCorrelationId(), roundTripped.getCorrelationId()); } - - // ========== Helpers ========== - - @FunctionalInterface - interface JsonParser { - T parse(JsonReader reader) throws IOException; - } - - private T parseJson(String json, JsonParser parser) throws IOException { - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - return parser.parse(reader); - } - } - - private > String writeToJson(T serializable) - throws IOException { - StringWriter sw = new StringWriter(); - try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { - serializable.toJson(writer); - } - return sw.toString(); - } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java index 0ee998fc..95a1420f 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java @@ -3,12 +3,9 @@ package com.microsoft.aad.msal4j; -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.StringReader; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -30,10 +27,7 @@ void errorResponse_fromJson_allFieldsPopulated() throws IOException { + "\"correlation_id\":\"corr-456\"," + "\"claims\":\"{\\\"access_token\\\":{\\\"nbf\\\":{\\\"essential\\\":true}}}\"}"; - ErrorResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = ErrorResponse.fromJson(reader); - } + ErrorResponse response = TestHelper.parseJson(json, ErrorResponse::fromJson); assertEquals("invalid_grant", response.error()); assertEquals("Token expired", response.errorDescription()); @@ -49,10 +43,7 @@ void errorResponse_fromJson_allFieldsPopulated() throws IOException { void errorResponse_fromJson_minimalFields() throws IOException { String json = "{\"error\":\"server_error\"}"; - ErrorResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = ErrorResponse.fromJson(reader); - } + ErrorResponse response = TestHelper.parseJson(json, ErrorResponse::fromJson); assertEquals("server_error", response.error()); assertNull(response.errorDescription()); @@ -68,10 +59,7 @@ void errorResponse_fromJson_minimalFields() throws IOException { void errorResponse_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"error\":\"invalid_request\",\"unknown_field\":\"value\",\"another\":123}"; - ErrorResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = ErrorResponse.fromJson(reader); - } + ErrorResponse response = TestHelper.parseJson(json, ErrorResponse::fromJson); assertEquals("invalid_request", response.error()); } @@ -80,10 +68,7 @@ void errorResponse_fromJson_unknownFieldsSkipped() throws IOException { void errorResponse_fromJson_emptyErrorCodes() throws IOException { String json = "{\"error\":\"test\",\"error_codes\":[]}"; - ErrorResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = ErrorResponse.fromJson(reader); - } + ErrorResponse response = TestHelper.parseJson(json, ErrorResponse::fromJson); assertNotNull(response.errorCodes()); assertEquals(0, response.errorCodes().length); @@ -124,7 +109,7 @@ void errorResponse_toJson_throwsDueToDoubleStartObject() { response.statusCode(400); response.error("invalid_grant"); - assertThrows(IllegalStateException.class, () -> writeToJson(response), + assertThrows(IllegalStateException.class, () -> TestHelper.writeToJson(response), "toJson has a double writeStartObject bug that causes IllegalStateException"); } @@ -135,7 +120,7 @@ void errorResponse_toJson_nullErrorCodes_throwsDueToDoubleStartObject() { response.statusCode(500); response.error("server_error"); - assertThrows(IllegalStateException.class, () -> writeToJson(response)); + assertThrows(IllegalStateException.class, () -> TestHelper.writeToJson(response)); } // ========== UserDiscoveryResponse ========== @@ -149,10 +134,7 @@ void userDiscoveryResponse_fromJson_federatedAccount() throws IOException { + "\"federation_active_auth_url\":\"https://adfs.example.com/active\"," + "\"cloud_audience_urn\":\"urn:federation:MicrosoftOnline\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertEquals(1.0f, response.version(), 0.01f); assertEquals("Federated", response.accountType()); @@ -168,10 +150,7 @@ void userDiscoveryResponse_fromJson_federatedAccount() throws IOException { void userDiscoveryResponse_fromJson_managedAccount() throws IOException { String json = "{\"ver\":\"1.0\",\"account_type\":\"Managed\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertTrue(response.isAccountManaged()); assertFalse(response.isAccountFederated()); @@ -181,10 +160,7 @@ void userDiscoveryResponse_fromJson_managedAccount() throws IOException { void userDiscoveryResponse_fromJson_unknownAccountType() throws IOException { String json = "{\"ver\":\"2.0\",\"account_type\":\"Unknown\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertFalse(response.isAccountFederated()); assertFalse(response.isAccountManaged()); @@ -203,10 +179,7 @@ void userDiscoveryResponse_isAccountFederated_nullAccountType() { void userDiscoveryResponse_isAccountFederated_caseInsensitive() throws IOException { String json = "{\"ver\":\"1.0\",\"account_type\":\"fEdErAtEd\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertTrue(response.isAccountFederated()); } @@ -215,10 +188,7 @@ void userDiscoveryResponse_isAccountFederated_caseInsensitive() throws IOExcepti void userDiscoveryResponse_isAccountManaged_caseInsensitive() throws IOException { String json = "{\"ver\":\"1.0\",\"account_type\":\"MANAGED\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertTrue(response.isAccountManaged()); } @@ -232,17 +202,11 @@ void userDiscoveryResponse_toJson_roundTrip() throws IOException { + "\"federation_active_auth_url\":\"https://adfs.example.com/active\"," + "\"cloud_audience_urn\":\"urn:federation:MicrosoftOnline\"}"; - UserDiscoveryResponse original; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - original = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse original = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); - String serialized = writeToJson(original); + String serialized = TestHelper.writeToJson(original); - UserDiscoveryResponse roundTripped; - try (JsonReader reader = JsonProviders.createReader(new StringReader(serialized))) { - roundTripped = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse roundTripped = TestHelper.parseJson(serialized, UserDiscoveryResponse::fromJson); assertEquals(original.accountType(), roundTripped.accountType()); assertEquals(original.federationProtocol(), roundTripped.federationProtocol()); @@ -255,10 +219,7 @@ void userDiscoveryResponse_toJson_roundTrip() throws IOException { void userDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"ver\":\"1.0\",\"account_type\":\"Managed\",\"extra_field\":\"ignored\"}"; - UserDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = UserDiscoveryResponse.fromJson(reader); - } + UserDiscoveryResponse response = TestHelper.parseJson(json, UserDiscoveryResponse::fromJson); assertTrue(response.isAccountManaged()); } @@ -272,10 +233,7 @@ void oidcDiscoveryResponse_fromJson_allEndpoints() throws IOException { + "\"device_authorization_endpoint\":\"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode\"," + "\"issuer\":\"https://login.microsoftonline.com/{tenantid}/v2.0\"}"; - OidcDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = OidcDiscoveryResponse.fromJson(reader); - } + OidcDiscoveryResponse response = TestHelper.parseJson(json, OidcDiscoveryResponse::fromJson); assertEquals("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", response.authorizationEndpoint()); assertEquals("https://login.microsoftonline.com/common/oauth2/v2.0/token", response.tokenEndpoint()); @@ -288,10 +246,7 @@ void oidcDiscoveryResponse_fromJson_partialFields() throws IOException { String json = "{\"authorization_endpoint\":\"https://example.com/auth\"," + "\"token_endpoint\":\"https://example.com/token\"}"; - OidcDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = OidcDiscoveryResponse.fromJson(reader); - } + OidcDiscoveryResponse response = TestHelper.parseJson(json, OidcDiscoveryResponse::fromJson); assertEquals("https://example.com/auth", response.authorizationEndpoint()); assertEquals("https://example.com/token", response.tokenEndpoint()); @@ -305,10 +260,7 @@ void oidcDiscoveryResponse_fromJson_unknownFieldsSkipped() throws IOException { + "\"jwks_uri\":\"https://example.com/keys\"," + "\"response_types_supported\":[\"code\"]}"; - OidcDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = OidcDiscoveryResponse.fromJson(reader); - } + OidcDiscoveryResponse response = TestHelper.parseJson(json, OidcDiscoveryResponse::fromJson); assertEquals("https://example.com/auth", response.authorizationEndpoint()); } @@ -321,12 +273,9 @@ void oidcDiscoveryResponse_toJson_doesNotIncludeIssuer() throws IOException { + "\"device_authorization_endpoint\":\"https://example.com/device\"," + "\"issuer\":\"https://example.com/issuer\"}"; - OidcDiscoveryResponse response; - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - response = OidcDiscoveryResponse.fromJson(reader); - } + OidcDiscoveryResponse response = TestHelper.parseJson(json, OidcDiscoveryResponse::fromJson); - String output = writeToJson(response); + String output = TestHelper.writeToJson(response); assertTrue(output.contains("authorization_endpoint")); assertTrue(output.contains("token_endpoint")); @@ -365,10 +314,8 @@ void requestedClaimAdditionalInfo_fromJson_allFields() throws IOException { // Wrap in outer object since fromJson expects to be positioned in a field context String json = "{\"essential\":true,\"value\":\"test-value\",\"values\":[\"v1\",\"v2\"]}"; - RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - info = info.fromJson(reader); - } + RequestedClaimAdditionalInfo info = TestHelper.parseJson(json, + reader -> new RequestedClaimAdditionalInfo(false, null, null).fromJson(reader)); assertTrue(info.isEssential()); assertEquals("test-value", info.getValue()); @@ -379,10 +326,8 @@ void requestedClaimAdditionalInfo_fromJson_allFields() throws IOException { void requestedClaimAdditionalInfo_fromJson_essentialOnly() throws IOException { String json = "{\"essential\":true}"; - RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, null, null); - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - info = info.fromJson(reader); - } + RequestedClaimAdditionalInfo info = TestHelper.parseJson(json, + reader -> new RequestedClaimAdditionalInfo(false, null, null).fromJson(reader)); assertTrue(info.isEssential()); assertNull(info.getValue()); @@ -393,7 +338,7 @@ void requestedClaimAdditionalInfo_fromJson_essentialOnly() throws IOException { void requestedClaimAdditionalInfo_toJson_essentialTrue() throws IOException { RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, null, null); - String output = writeToJson(info); + String output = TestHelper.writeToJson(info); assertTrue(output.contains("\"essential\":true")); assertFalse(output.contains("\"value\"")); @@ -404,7 +349,7 @@ void requestedClaimAdditionalInfo_toJson_essentialTrue() throws IOException { void requestedClaimAdditionalInfo_toJson_essentialFalseOmitted() throws IOException { RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(false, "v", null); - String output = writeToJson(info); + String output = TestHelper.writeToJson(info); // essential=false is omitted from serialization assertFalse(output.contains("essential")); @@ -416,7 +361,7 @@ void requestedClaimAdditionalInfo_toJson_withValues() throws IOException { RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo( false, null, Arrays.asList("x", "y")); - String output = writeToJson(info); + String output = TestHelper.writeToJson(info); assertTrue(output.contains("\"values\"")); assertTrue(output.contains("\"x\"")); @@ -428,7 +373,7 @@ void requestedClaimAdditionalInfo_toJson_emptyValuesOmitted() throws IOException RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo( false, null, Arrays.asList()); - String output = writeToJson(info); + String output = TestHelper.writeToJson(info); // Empty list should be omitted (values != null but isEmpty()) assertFalse(output.contains("values")); @@ -492,7 +437,7 @@ void requestedClaim_toJson_withNameAndInfo_throwsDueToBadStringWrite() { RequestedClaimAdditionalInfo info = new RequestedClaimAdditionalInfo(true, null, null); RequestedClaim claim = new RequestedClaim("sub", info); - assertThrows(IllegalStateException.class, () -> writeToJson(claim), + assertThrows(IllegalStateException.class, () -> TestHelper.writeToJson(claim), "toJson writes a raw string in object context, causing IllegalStateException"); } @@ -500,20 +445,10 @@ void requestedClaim_toJson_withNameAndInfo_throwsDueToBadStringWrite() { void requestedClaim_toJson_nullNameAndInfo_writesEmptyObject() throws IOException { RequestedClaim claim = new RequestedClaim(null, null); - String output = writeToJson(claim); + String output = TestHelper.writeToJson(claim); // When name or info is null, toJson skips writing the content assertEquals("{}", output); } - // ========== Helper ========== - - private > String writeToJson(T serializable) - throws IOException { - java.io.StringWriter sw = new java.io.StringWriter(); - try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { - serializable.toJson(writer); - } - return sw.toString(); - } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java index f7cc377f..5852454a 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/TestHelper.java @@ -3,9 +3,16 @@ package com.microsoft.aad.msal4j; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -351,4 +358,32 @@ static PrivateKey getPrivateKey() { return privateKey; } + + // --- JSON parsing/serialization helpers --- + + @FunctionalInterface + interface JsonParser { + T parse(JsonReader reader) throws IOException; + } + + /** + * Parses a JSON string into an object using the provided parser function. + * Handles JsonReader lifecycle automatically. + */ + static T parseJson(String json, JsonParser parser) throws IOException { + try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { + return parser.parse(reader); + } + } + + /** + * Serializes a JsonSerializable object to a JSON string. + */ + static > String writeToJson(T serializable) throws IOException { + StringWriter sw = new StringWriter(); + try (JsonWriter writer = JsonProviders.createWriter(sw)) { + serializable.toJson(writer); + } + return sw.toString(); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java index 43214e88..c8f84ffa 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WSTrustResponseTest.java @@ -13,10 +13,19 @@ class WSTrustResponseTest { private static final String AAD_TOKEN_ERROR_FILE = "/token-error.xml"; + private String successXml; + private String errorXml; + private WSTrustResponse successResponse; + @BeforeEach - void setup() { + void setup() throws Exception { System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); + + successXml = TestHelper.readResource(WSTrustResponseTest.class, + TestConfiguration.AAD_TOKEN_SUCCESS_FILE); + errorXml = TestHelper.readResource(WSTrustResponseTest.class, AAD_TOKEN_ERROR_FILE); + successResponse = WSTrustResponse.parse(successXml, WSTrustVersion.WSTRUST13); } @AfterEach @@ -25,46 +34,29 @@ void cleanup() { } @Test - void parse_ValidToken_ReturnsResponseWithToken() throws Exception { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - - WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - - assertNotNull(response); - assertNotNull(response.getToken(), "Parsed response should contain a token"); - assertNotNull(response.getTokenType(), "Parsed response should contain a token type"); - assertFalse(response.isErrorFound(), "Successful parse should not have error"); + void parse_ValidToken_ReturnsResponseWithToken() { + assertNotNull(successResponse); + assertNotNull(successResponse.getToken(), "Parsed response should contain a token"); + assertNotNull(successResponse.getTokenType(), "Parsed response should contain a token type"); + assertFalse(successResponse.isErrorFound(), "Successful parse should not have error"); } @Test - void parse_ValidToken_TokenTypeIsSaml1() throws Exception { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - - WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - - assertEquals(WSTrustResponse.SAML1_ASSERTION, response.getTokenType()); - assertFalse(response.isTokenSaml2(), "SAML 1.0 token should not be detected as SAML 2"); + void parse_ValidToken_TokenTypeIsSaml1() { + assertEquals(WSTrustResponse.SAML1_ASSERTION, successResponse.getTokenType()); + assertFalse(successResponse.isTokenSaml2(), "SAML 1.0 token should not be detected as SAML 2"); } @Test - void parse_ValidToken_TokenContainsSamlAssertion() throws Exception { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - - WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - - assertTrue(response.getToken().contains("saml:Assertion"), + void parse_ValidToken_TokenContainsSamlAssertion() { + assertTrue(successResponse.getToken().contains("saml:Assertion"), "Token should contain SAML assertion XML"); } @Test void parse_ErrorResponse_ThrowsMsalServiceException() { - String xml = TestHelper.readResource(WSTrustResponseTest.class, AAD_TOKEN_ERROR_FILE); - MsalServiceException ex = assertThrows(MsalServiceException.class, - () -> WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13)); + () -> WSTrustResponse.parse(errorXml, WSTrustVersion.WSTRUST13)); assertEquals(AuthenticationErrorCode.WSTRUST_SERVICE_ERROR, ex.errorCode()); assertTrue(ex.getMessage().contains("ErrorCode:"), @@ -75,12 +67,9 @@ void parse_ErrorResponse_ThrowsMsalServiceException() { @Test void parse_ErrorResponse_ContainsReasonAndSubcode() { - String xml = TestHelper.readResource(WSTrustResponseTest.class, AAD_TOKEN_ERROR_FILE); - MsalServiceException ex = assertThrows(MsalServiceException.class, - () -> WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13)); + () -> WSTrustResponse.parse(errorXml, WSTrustVersion.WSTRUST13)); - // The error XML has subcode "a:RequestFailed" which splits to "RequestFailed" assertTrue(ex.getMessage().contains("RequestFailed"), "Exception should contain parsed error subcode"); assertTrue(ex.getMessage().contains("MSIS3127"), @@ -89,40 +78,27 @@ void parse_ErrorResponse_ContainsReasonAndSubcode() { @Test void parse_UndefinedVersion_ThrowsException() { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - // UNDEFINED version has empty XPaths which cause XPathExpressionException assertThrows(Exception.class, - () -> WSTrustResponse.parse(xml, WSTrustVersion.UNDEFINED)); + () -> WSTrustResponse.parse(successXml, WSTrustVersion.UNDEFINED)); } @Test - void isTokenSaml2_nonSaml1Type_returnsTrue() throws Exception { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - + void isTokenSaml2_nonSaml1Type_returnsTrue() { // Token from test XML is SAML 1.0 - assertFalse(response.isTokenSaml2()); + assertFalse(successResponse.isTokenSaml2()); - // Any non-SAML1 type should return true for isTokenSaml2 - // We can verify the constant value + // Verify the constant value assertEquals("urn:oasis:names:tc:SAML:1.0:assertion", WSTrustResponse.SAML1_ASSERTION); } @Test - void parse_ValidToken_GettersReturnExpectedValues() throws Exception { - String xml = TestHelper.readResource(WSTrustResponseTest.class, - TestConfiguration.AAD_TOKEN_SUCCESS_FILE); - - WSTrustResponse response = WSTrustResponse.parse(xml, WSTrustVersion.WSTRUST13); - - assertFalse(response.isErrorFound()); - assertNull(response.getFaultMessage()); - assertNull(response.getErrorCode()); - assertNotNull(response.getToken()); - assertFalse(response.getToken().isEmpty()); + void parse_ValidToken_GettersReturnExpectedValues() { + assertFalse(successResponse.isErrorFound()); + assertNull(successResponse.getFaultMessage()); + assertNull(successResponse.getErrorCode()); + assertNotNull(successResponse.getToken()); + assertFalse(successResponse.getToken().isEmpty()); } @Test diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java index 3213234f..98388e51 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/WsTrustFederationTest.java @@ -3,13 +3,9 @@ package com.microsoft.aad.msal4j; -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; import java.util.Collections; import static org.junit.jupiter.api.Assertions.*; @@ -28,7 +24,7 @@ void credential_fromJson_allFields() throws IOException { + "\"user_assertion_hash\":\"hash-abc\"" + "}"; - Credential cred = parseJson(json, Credential::fromJson); + Credential cred = TestHelper.parseJson(json, Credential::fromJson); assertEquals("uid.utid", cred.homeAccountId()); assertEquals("login.microsoftonline.com", cred.environment()); @@ -41,7 +37,7 @@ void credential_fromJson_allFields() throws IOException { void credential_fromJson_partialFields() throws IOException { String json = "{\"home_account_id\":\"uid\",\"environment\":\"env\"}"; - Credential cred = parseJson(json, Credential::fromJson); + Credential cred = TestHelper.parseJson(json, Credential::fromJson); assertEquals("uid", cred.homeAccountId()); assertEquals("env", cred.environment()); @@ -54,7 +50,7 @@ void credential_fromJson_partialFields() throws IOException { void credential_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"home_account_id\":\"uid\",\"extra\":123}"; - Credential cred = parseJson(json, Credential::fromJson); + Credential cred = TestHelper.parseJson(json, Credential::fromJson); assertEquals("uid", cred.homeAccountId()); } @@ -85,7 +81,7 @@ void credential_toJson_allFields() throws IOException { cred.secret("sec"); cred.userAssertionHash("hash"); - String output = writeToJson(cred); + String output = TestHelper.writeToJson(cred); assertTrue(output.contains("\"home_account_id\":\"uid.utid\"")); assertTrue(output.contains("\"environment\":\"login.microsoftonline.com\"")); @@ -104,9 +100,9 @@ void credential_toJson_roundTrip() throws IOException { + "\"user_assertion_hash\":\"hash\"" + "}"; - Credential original = parseJson(json, Credential::fromJson); - String serialized = writeToJson(original); - Credential roundTripped = parseJson(serialized, Credential::fromJson); + Credential original = TestHelper.parseJson(json, Credential::fromJson); + String serialized = TestHelper.writeToJson(original); + Credential roundTripped = TestHelper.parseJson(serialized, Credential::fromJson); assertEquals(original.homeAccountId(), roundTripped.homeAccountId()); assertEquals(original.environment(), roundTripped.environment()); @@ -194,7 +190,7 @@ void idToken_fromJson_allFields() throws IOException { + "\"unique_name\":\"testuser\"" + "}"; - IdToken idToken = parseJson(json, IdToken::fromJson); + IdToken idToken = TestHelper.parseJson(json, IdToken::fromJson); assertEquals("https://login.microsoftonline.com/tenant/v2.0", idToken.issuer); assertEquals("sub-123", idToken.subject); @@ -214,7 +210,7 @@ void idToken_fromJson_allFields() throws IOException { void idToken_fromJson_partialFields() throws IOException { String json = "{\"iss\":\"issuer\",\"sub\":\"subject\",\"aud\":\"audience\"}"; - IdToken idToken = parseJson(json, IdToken::fromJson); + IdToken idToken = TestHelper.parseJson(json, IdToken::fromJson); assertEquals("issuer", idToken.issuer); assertEquals("subject", idToken.subject); @@ -234,7 +230,7 @@ void idToken_fromJson_partialFields() throws IOException { void idToken_fromJson_unknownFieldsSkipped() throws IOException { String json = "{\"iss\":\"issuer\",\"nonce\":\"abc\",\"email\":\"user@test.com\"}"; - IdToken idToken = parseJson(json, IdToken::fromJson); + IdToken idToken = TestHelper.parseJson(json, IdToken::fromJson); assertEquals("issuer", idToken.issuer); } @@ -255,7 +251,7 @@ void idToken_toJson_allFields() throws IOException { idToken.upn = "user@example.com"; idToken.uniqueName = "unique"; - String output = writeToJson(idToken); + String output = TestHelper.writeToJson(idToken); assertTrue(output.contains("\"iss\":\"iss\"")); assertTrue(output.contains("\"sub\":\"sub\"")); @@ -285,9 +281,9 @@ void idToken_toJson_roundTrip() throws IOException { + "\"unique_name\":\"unique-1\"" + "}"; - IdToken original = parseJson(json, IdToken::fromJson); - String serialized = writeToJson(original); - IdToken roundTripped = parseJson(serialized, IdToken::fromJson); + IdToken original = TestHelper.parseJson(json, IdToken::fromJson); + String serialized = TestHelper.writeToJson(original); + IdToken roundTripped = TestHelper.parseJson(serialized, IdToken::fromJson); assertEquals(original.issuer, roundTripped.issuer); assertEquals(original.subject, roundTripped.subject); @@ -322,26 +318,4 @@ void wsTrustVersion_wsTrust13_containsCorrectPaths() { assertTrue(WSTrustVersion.WSTRUST13.responseSecurityTokenPath() .contains("RequestedSecurityToken")); } - - // ========== Helpers ========== - - @FunctionalInterface - interface JsonParser { - T parse(JsonReader reader) throws IOException; - } - - private T parseJson(String json, JsonParser parser) throws IOException { - try (JsonReader reader = JsonProviders.createReader(new StringReader(json))) { - return parser.parse(reader); - } - } - - private > String writeToJson(T serializable) - throws IOException { - StringWriter sw = new StringWriter(); - try (com.azure.json.JsonWriter writer = JsonProviders.createWriter(sw)) { - serializable.toJson(writer); - } - return sw.toString(); - } } From 95c9d981a67e1d212198b741af37eaf4db293b6f Mon Sep 17 00:00:00 2001 From: avdunn Date: Mon, 8 Jun 2026 16:19:00 -0700 Subject: [PATCH 6/7] Misc. tests --- .../aad/msal4j/AcquireTokenSilentlyTest.java | 92 ++++++++ .../aad/msal4j/ParameterBuilderTest.java | 19 ++ .../aad/msal4j/ResponseParsingTest.java | 212 ++++++++++++++++++ 3 files changed, 323 insertions(+) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java index 036a4f04..bf6163bc 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java @@ -197,4 +197,96 @@ private void assertRefreshedToken(IAuthenticationResult result, String expectedT assertEquals(expectedToken, result.accessToken()); assertEquals(expectedReason, result.metadata().cacheRefreshReason()); } + + // ========== SilentRequestHelper ========== + + @Test + void getCacheRefreshReason_claimsPresent_returnsClaims() { + SilentParameters params = SilentParameters.builder( + Collections.singleton("scope"), + mock(IAccount.class)) + .claims(new ClaimsRequest()) + .build(); + + AuthenticationResult cachedResult = mock(AuthenticationResult.class); + when(cachedResult.accessToken()).thenReturn("valid-token"); + when(cachedResult.expiresOn()).thenReturn(System.currentTimeMillis() / 1000 + 3600); + + org.slf4j.Logger log = mock(org.slf4j.Logger.class); + + assertEquals(CacheRefreshReason.CLAIMS, + SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); + } + + @Test + void getCacheRefreshReason_expiredToken_returnsExpired() { + SilentParameters params = SilentParameters.builder( + Collections.singleton("scope"), + mock(IAccount.class)) + .build(); + + AuthenticationResult cachedResult = mock(AuthenticationResult.class); + when(cachedResult.accessToken()).thenReturn("expired-token"); + when(cachedResult.expiresOn()).thenReturn(System.currentTimeMillis() / 1000 - 600); + + org.slf4j.Logger log = mock(org.slf4j.Logger.class); + + assertEquals(CacheRefreshReason.EXPIRED, + SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); + } + + @Test + void getCacheRefreshReason_proactiveRefresh_returnsProactiveRefresh() { + SilentParameters params = SilentParameters.builder( + Collections.singleton("scope"), + mock(IAccount.class)) + .build(); + + long now = System.currentTimeMillis() / 1000; + AuthenticationResult cachedResult = mock(AuthenticationResult.class); + when(cachedResult.accessToken()).thenReturn("valid-token"); + when(cachedResult.expiresOn()).thenReturn(now + 3600); + when(cachedResult.refreshOn()).thenReturn(now - 600); + + org.slf4j.Logger log = mock(org.slf4j.Logger.class); + + assertEquals(CacheRefreshReason.PROACTIVE_REFRESH, + SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); + } + + @Test + void getCacheRefreshReason_noAccessTokenWithRefreshToken_returnsNoCachedAccessToken() { + SilentParameters params = SilentParameters.builder( + Collections.singleton("scope"), + mock(IAccount.class)) + .build(); + + AuthenticationResult cachedResult = mock(AuthenticationResult.class); + when(cachedResult.accessToken()).thenReturn(null); + when(cachedResult.refreshToken()).thenReturn("refresh-token-value"); + + org.slf4j.Logger log = mock(org.slf4j.Logger.class); + + assertEquals(CacheRefreshReason.NO_CACHED_ACCESS_TOKEN, + SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); + } + + @Test + void getCacheRefreshReason_validToken_returnsNotApplicable() { + SilentParameters params = SilentParameters.builder( + Collections.singleton("scope"), + mock(IAccount.class)) + .build(); + + long now = System.currentTimeMillis() / 1000; + AuthenticationResult cachedResult = mock(AuthenticationResult.class); + when(cachedResult.accessToken()).thenReturn("valid-token"); + when(cachedResult.expiresOn()).thenReturn(now + 3600); + when(cachedResult.refreshOn()).thenReturn(null); + + org.slf4j.Logger log = mock(org.slf4j.Logger.class); + + assertEquals(CacheRefreshReason.NOT_APPLICABLE, + SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ParameterBuilderTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ParameterBuilderTest.java index 6d5014b7..b5ac5533 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ParameterBuilderTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ParameterBuilderTest.java @@ -860,4 +860,23 @@ void managedIdentity_BlankClaims_Throws() { assertThrows(IllegalArgumentException.class, () -> ManagedIdentityParameters.builder("https://management.azure.com").claims(" ")); } + + // ========== ParameterValidationUtils ========== + + @Test + void validateNotEmpty_set_nullThrows() { + assertThrows(IllegalArgumentException.class, + () -> ParameterValidationUtils.validateNotEmpty("scopes", (Set) null)); + } + + @Test + void validateNotEmpty_set_emptyThrows() { + assertThrows(IllegalArgumentException.class, + () -> ParameterValidationUtils.validateNotEmpty("scopes", new HashSet<>())); + } + + @Test + void validateNotEmpty_set_nonEmptyPasses() { + ParameterValidationUtils.validateNotEmpty("scopes", Collections.singleton("openid")); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java index 95a1420f..6768dffd 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java @@ -7,10 +7,14 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class ResponseParsingTest { @@ -451,4 +455,212 @@ void requestedClaim_toJson_nullNameAndInfo_writesEmptyObject() throws IOExceptio assertEquals("{}", output); } + // ========== ClientInfo ========== + + @Test + void clientInfo_createFromJson_validBase64() { + String json = "{\"uid\":\"user-id\",\"utid\":\"tenant-id\"}"; + String base64 = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + ClientInfo info = ClientInfo.createFromJson(base64); + + assertNotNull(info); + assertEquals("user-id", info.getUniqueIdentifier()); + assertEquals("tenant-id", info.getUniqueTenantIdentifier()); + assertEquals("user-id.tenant-id", info.toAccountIdentifier()); + } + + @Test + void clientInfo_createFromJson_blankInput_returnsNull() { + assertNull(ClientInfo.createFromJson(null)); + assertNull(ClientInfo.createFromJson("")); + assertNull(ClientInfo.createFromJson(" ")); + } + + @Test + void clientInfo_toJson_roundTrip() throws IOException { + String json = "{\"uid\":\"uid-1\",\"utid\":\"utid-1\"}"; + ClientInfo original = TestHelper.parseJson(json, ClientInfo::fromJson); + + String serialized = TestHelper.writeToJson(original); + + assertTrue(serialized.contains("\"uid\":\"uid-1\"")); + assertTrue(serialized.contains("\"utid\":\"utid-1\"")); + + ClientInfo roundTripped = TestHelper.parseJson(serialized, ClientInfo::fromJson); + assertEquals(original.getUniqueIdentifier(), roundTripped.getUniqueIdentifier()); + assertEquals(original.getUniqueTenantIdentifier(), roundTripped.getUniqueTenantIdentifier()); + } + + @Test + void clientInfo_fromJson_unknownFieldsSkipped() throws IOException { + String json = "{\"uid\":\"u\",\"utid\":\"t\",\"extra\":\"ignored\"}"; + + ClientInfo info = TestHelper.parseJson(json, ClientInfo::fromJson); + + assertEquals("u", info.getUniqueIdentifier()); + assertEquals("t", info.getUniqueTenantIdentifier()); + } + + // ========== MsalServiceException ========== + + @Test + void msalServiceException_errorResponseConstructor() { + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.statusCode(401); + errorResponse.statusMessage("Unauthorized"); + errorResponse.error("invalid_token"); + errorResponse.errorDescription("Token is expired"); + errorResponse.subError("token_expired"); + errorResponse.correlation_id("corr-123"); + errorResponse.claims("{\"access_token\":{}}"); + + Map> headers = new HashMap<>(); + headers.put("WWW-Authenticate", Collections.singletonList("Bearer")); + + MsalServiceException ex = new MsalServiceException(errorResponse, headers); + + assertEquals(401, ex.statusCode().intValue()); + assertEquals("Unauthorized", ex.statusMessage()); + assertEquals("invalid_token", ex.errorCode()); + assertEquals("Token is expired", ex.getMessage()); + assertEquals("token_expired", ex.subError()); + assertEquals("corr-123", ex.correlationId()); + assertEquals("{\"access_token\":{}}", ex.claims()); + assertNotNull(ex.headers()); + assertTrue(ex.headers().containsKey("WWW-Authenticate")); + } + + @Test + void msalServiceException_discoveryResponseConstructor() throws IOException { + String json = "{\"error\":\"invalid_instance\"," + + "\"error_description\":\"AADSTS50049: Unknown instance.\"," + + "\"correlation_id\":\"disc-corr\"}"; + + AadInstanceDiscoveryResponse discoveryResponse = + TestHelper.parseJson(json, AadInstanceDiscoveryResponse::fromJson); + + MsalServiceException ex = new MsalServiceException(discoveryResponse); + + assertEquals("invalid_instance", ex.errorCode()); + assertEquals("AADSTS50049: Unknown instance.", ex.getMessage()); + assertEquals("disc-corr", ex.correlationId()); + assertNull(ex.statusCode()); + assertNull(ex.headers()); + } + + @Test + void msalServiceException_managedIdentityConstructor() { + MsalServiceException ex = new MsalServiceException( + "MI error", "managed_identity_error", + ManagedIdentitySourceType.APP_SERVICE); + + assertEquals("MI error", ex.getMessage()); + assertEquals("managed_identity_error", ex.errorCode()); + assertEquals("APP_SERVICE", ex.managedIdentitySource()); + } + + // ========== MsalServiceExceptionFactory ========== + + @Test + void msalServiceExceptionFactory_blankBody_returnsUnknownError() { + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(""); + when(response.statusCode()).thenReturn(500); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); + assertTrue(ex.getMessage().contains("500")); + assertTrue(ex.getMessage().contains("no response body")); + } + + @Test + void msalServiceExceptionFactory_nullBody_returnsUnknownError() { + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(null); + when(response.statusCode()).thenReturn(503); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); + assertTrue(ex.getMessage().contains("503")); + } + + @Test + void msalServiceExceptionFactory_invalidGrant_interactionRequired() { + String body = "{\"error\":\"invalid_grant\"," + + "\"error_description\":\"AADSTS50076: needs MFA\"," + + "\"suberror\":\"basic_action\"}"; + + Map> headers = new HashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(body); + when(response.statusCode()).thenReturn(400); + when(response.headers()).thenReturn(headers); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertTrue(ex instanceof MsalInteractionRequiredException, + "invalid_grant with UI-required suberror should return MsalInteractionRequiredException"); + } + + @Test + void msalServiceExceptionFactory_invalidGrant_clientMismatch_notInteractionRequired() { + String body = "{\"error\":\"invalid_grant\"," + + "\"error_description\":\"Client mismatch\"," + + "\"suberror\":\"client_mismatch\"}"; + + Map> headers = new HashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(body); + when(response.statusCode()).thenReturn(400); + when(response.headers()).thenReturn(headers); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertFalse(ex instanceof MsalInteractionRequiredException, + "client_mismatch suberror should NOT return MsalInteractionRequiredException"); + assertEquals(400, ex.statusCode().intValue()); + } + + @Test + void msalServiceExceptionFactory_invalidGrant_protectionPolicyRequired_notInteractionRequired() { + String body = "{\"error\":\"invalid_grant\"," + + "\"error_description\":\"Protection policy required\"," + + "\"suberror\":\"protection_policy_required\"}"; + + Map> headers = new HashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(body); + when(response.statusCode()).thenReturn(400); + when(response.headers()).thenReturn(headers); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertFalse(ex instanceof MsalInteractionRequiredException, + "protection_policy_required suberror should NOT return MsalInteractionRequiredException"); + } + + @Test + void msalServiceExceptionFactory_noErrorInBody_returnsUnknownError() { + String body = "{\"some_field\":\"some_value\"}"; + + IHttpResponse response = mock(IHttpResponse.class); + when(response.body()).thenReturn(body); + when(response.statusCode()).thenReturn(500); + + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + + assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); + assertTrue(ex.getMessage().contains("500")); + } + } From bebe2d7a9f6b73043b6401503e4531aae9145843 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 9 Jun 2026 07:19:36 -0700 Subject: [PATCH 7/7] Minor improvements --- .../aad/msal4j/AcquireTokenSilentlyTest.java | 18 ++-- .../aad/msal4j/ResponseParsingTest.java | 83 +++++++++---------- 2 files changed, 45 insertions(+), 56 deletions(-) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java index bf6163bc..332a79fc 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java @@ -5,11 +5,7 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Collections; @@ -18,6 +14,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; + class AcquireTokenSilentlyTest { @@ -212,7 +210,7 @@ void getCacheRefreshReason_claimsPresent_returnsClaims() { when(cachedResult.accessToken()).thenReturn("valid-token"); when(cachedResult.expiresOn()).thenReturn(System.currentTimeMillis() / 1000 + 3600); - org.slf4j.Logger log = mock(org.slf4j.Logger.class); + Logger log = mock(Logger.class); assertEquals(CacheRefreshReason.CLAIMS, SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); @@ -229,7 +227,7 @@ void getCacheRefreshReason_expiredToken_returnsExpired() { when(cachedResult.accessToken()).thenReturn("expired-token"); when(cachedResult.expiresOn()).thenReturn(System.currentTimeMillis() / 1000 - 600); - org.slf4j.Logger log = mock(org.slf4j.Logger.class); + Logger log = mock(Logger.class); assertEquals(CacheRefreshReason.EXPIRED, SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); @@ -248,7 +246,7 @@ void getCacheRefreshReason_proactiveRefresh_returnsProactiveRefresh() { when(cachedResult.expiresOn()).thenReturn(now + 3600); when(cachedResult.refreshOn()).thenReturn(now - 600); - org.slf4j.Logger log = mock(org.slf4j.Logger.class); + Logger log = mock(Logger.class); assertEquals(CacheRefreshReason.PROACTIVE_REFRESH, SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); @@ -265,7 +263,7 @@ void getCacheRefreshReason_noAccessTokenWithRefreshToken_returnsNoCachedAccessTo when(cachedResult.accessToken()).thenReturn(null); when(cachedResult.refreshToken()).thenReturn("refresh-token-value"); - org.slf4j.Logger log = mock(org.slf4j.Logger.class); + Logger log = mock(Logger.class); assertEquals(CacheRefreshReason.NO_CACHED_ACCESS_TOKEN, SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); @@ -284,7 +282,7 @@ void getCacheRefreshReason_validToken_returnsNotApplicable() { when(cachedResult.expiresOn()).thenReturn(now + 3600); when(cachedResult.refreshOn()).thenReturn(null); - org.slf4j.Logger log = mock(org.slf4j.Logger.class); + Logger log = mock(Logger.class); assertEquals(CacheRefreshReason.NOT_APPLICABLE, SilentRequestHelper.getCacheRefreshReasonIfApplicable(params, cachedResult, log)); diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java index 6768dffd..52ce2730 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ResponseParsingTest.java @@ -6,7 +6,9 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -460,8 +462,8 @@ void requestedClaim_toJson_nullNameAndInfo_writesEmptyObject() throws IOExceptio @Test void clientInfo_createFromJson_validBase64() { String json = "{\"uid\":\"user-id\",\"utid\":\"tenant-id\"}"; - String base64 = java.util.Base64.getUrlEncoder().withoutPadding() - .encodeToString(json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + String base64 = Base64.getUrlEncoder().withoutPadding() + .encodeToString(json.getBytes(StandardCharsets.UTF_8)); ClientInfo info = ClientInfo.createFromJson(base64); @@ -563,13 +565,31 @@ void msalServiceException_managedIdentityConstructor() { // ========== MsalServiceExceptionFactory ========== - @Test - void msalServiceExceptionFactory_blankBody_returnsUnknownError() { + private IHttpResponse mockHttpResponse(String body, int statusCode) { + return mockHttpResponse(body, statusCode, null); + } + + private IHttpResponse mockHttpResponse(String body, int statusCode, + Map> headers) { IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(""); - when(response.statusCode()).thenReturn(500); + when(response.body()).thenReturn(body); + when(response.statusCode()).thenReturn(statusCode); + if (headers != null) { + when(response.headers()).thenReturn(headers); + } + return response; + } + + private Map> jsonContentTypeHeaders() { + Map> headers = new HashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + return headers; + } - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + @Test + void msalServiceExceptionFactory_blankBody_returnsUnknownError() { + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse("", 500)); assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); assertTrue(ex.getMessage().contains("500")); @@ -578,11 +598,8 @@ void msalServiceExceptionFactory_blankBody_returnsUnknownError() { @Test void msalServiceExceptionFactory_nullBody_returnsUnknownError() { - IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(null); - when(response.statusCode()).thenReturn(503); - - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse(null, 503)); assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); assertTrue(ex.getMessage().contains("503")); @@ -594,15 +611,8 @@ void msalServiceExceptionFactory_invalidGrant_interactionRequired() { + "\"error_description\":\"AADSTS50076: needs MFA\"," + "\"suberror\":\"basic_action\"}"; - Map> headers = new HashMap<>(); - headers.put("Content-Type", Collections.singletonList("application/json")); - - IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(body); - when(response.statusCode()).thenReturn(400); - when(response.headers()).thenReturn(headers); - - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse(body, 400, jsonContentTypeHeaders())); assertTrue(ex instanceof MsalInteractionRequiredException, "invalid_grant with UI-required suberror should return MsalInteractionRequiredException"); @@ -614,15 +624,8 @@ void msalServiceExceptionFactory_invalidGrant_clientMismatch_notInteractionRequi + "\"error_description\":\"Client mismatch\"," + "\"suberror\":\"client_mismatch\"}"; - Map> headers = new HashMap<>(); - headers.put("Content-Type", Collections.singletonList("application/json")); - - IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(body); - when(response.statusCode()).thenReturn(400); - when(response.headers()).thenReturn(headers); - - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse(body, 400, jsonContentTypeHeaders())); assertFalse(ex instanceof MsalInteractionRequiredException, "client_mismatch suberror should NOT return MsalInteractionRequiredException"); @@ -635,15 +638,8 @@ void msalServiceExceptionFactory_invalidGrant_protectionPolicyRequired_notIntera + "\"error_description\":\"Protection policy required\"," + "\"suberror\":\"protection_policy_required\"}"; - Map> headers = new HashMap<>(); - headers.put("Content-Type", Collections.singletonList("application/json")); - - IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(body); - when(response.statusCode()).thenReturn(400); - when(response.headers()).thenReturn(headers); - - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse(body, 400, jsonContentTypeHeaders())); assertFalse(ex instanceof MsalInteractionRequiredException, "protection_policy_required suberror should NOT return MsalInteractionRequiredException"); @@ -651,13 +647,8 @@ void msalServiceExceptionFactory_invalidGrant_protectionPolicyRequired_notIntera @Test void msalServiceExceptionFactory_noErrorInBody_returnsUnknownError() { - String body = "{\"some_field\":\"some_value\"}"; - - IHttpResponse response = mock(IHttpResponse.class); - when(response.body()).thenReturn(body); - when(response.statusCode()).thenReturn(500); - - MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse(response); + MsalServiceException ex = MsalServiceExceptionFactory.fromHttpResponse( + mockHttpResponse("{\"some_field\":\"some_value\"}", 500)); assertEquals(AuthenticationErrorCode.UNKNOWN, ex.errorCode()); assertTrue(ex.getMessage().contains("500"));