From 62fcb335986eb2aa5d89da2af73bc91839d4fd28 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 6 Jun 2025 14:49:49 -0700 Subject: [PATCH 1/2] First draft of new test structure --- .../microsoft/aad/msal4j/RunnerHelper.java | 163 +++++++++++++++++ .../aad/msal4j/RunnerJsonHelper.java | 104 +++++++++++ .../com/microsoft/aad/msal4j/RunnerTest.java | 29 ++++ .../com/microsoft/aad/msal4j/Shortcuts.java | 164 ++++++++++++++++++ 4 files changed, 460 insertions(+) create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java create mode 100644 msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java new file mode 100644 index 00000000..5054b0e3 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java @@ -0,0 +1,163 @@ +package com.microsoft.aad.msal4j; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.microsoft.aad.msal4j.ManagedIdentitySourceType.SERVICE_FABRIC; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.microsoft.aad.msal4j.Shortcuts.TestConfig; +import com.microsoft.aad.msal4j.Shortcuts.TestObject; +import com.microsoft.aad.msal4j.Shortcuts.TestAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RunnerHelper { + private static final Logger LOG = LoggerFactory.getLogger(RunnerHelper.class); + + /** + * Create Managed Identity applications from the test configuration. + * This method processes the "arrange" section of the test configuration. + */ + static Map createAppsFromConfig(TestConfig config) { + Map apps = new HashMap<>(); + + for (String appName : config.getAllArrangeObjects()) { + TestObject appObject = config.getArrangeObject(appName); + if ("ManagedIdentityClient".equals(appObject.getType())) { + ManagedIdentityId identityId = createManagedIdentityId(appObject); + List capabilities = extractClientCapabilities(appObject); + IEnvironmentVariables envVars = createEnvironmentVariables(config); + // TODO: other application properties + + ManagedIdentityApplication app = ManagedIdentityApplication.builder(identityId) + .clientCapabilities(capabilities) + .build(); + + ManagedIdentityApplication.setEnvironmentVariables(envVars); + + apps.put(appName, app); + } //TODO: Confidential and public clients + } + + return apps; + } + + /** + * Execute an action and return the result + * This method uses the "act" section of the test configuration. + */ + static IAuthenticationResult executeAction(ManagedIdentityApplication app, TestAction action) throws Exception { + if (action.getMethodName().equals("AcquireTokenForManagedIdentity")) { + LOG.info(String.format("Executing action: %s", action.getMethodName())); + + ManagedIdentityParameters params = buildManagedIdentityParameters(action); + + IAuthenticationResult result = app.acquireTokenForManagedIdentity(params).get(); + + LOG.info("Action result:"); + LOG.info(String.format("Access Token: %s", result.accessToken())); + LOG.info(String.format("ID Token : %s", result.idToken())); + LOG.info(String.format("Account : %s", result.account())); + LOG.info(String.format("Token Source: %s", result.metadata().tokenSource())); + + return result; + } else { + //TODO: other token calls and apps + throw new UnsupportedOperationException("Unsupported action: " + action.getMethodName()); + } + } + + /** + * Validate assertions against a result. + * This method uses the "assert" section of the test configuration. + */ + static void validateAssertions(IAuthenticationResult result, Map assertions) { + assertions.forEach((key, value) -> { + switch (key) { + case "token_source": + LOG.info("Validating token source"); + validateTokenSource(value.asText(), result); + break; + //TODO: other assertions + default: + // Optional: Handle unknown assertion types + break; + } + }); + } + + /** + * Create managed identity ID from test object + */ + static ManagedIdentityId createManagedIdentityId(TestObject appObject) { + String idType = appObject.getProperty("managed_identity").get("ManagedIdentityIdType").asText(); + + if ("SystemAssigned".equals(idType)) { + return ManagedIdentityId.systemAssigned(); + } else { + // TODO: handle user assertions + return null; + } + } + + /** + * Extract client capabilities from test object + */ + static List extractClientCapabilities(TestObject testObject) { + List capabilities = new ArrayList<>(); + JsonNode capabilitiesNode = testObject.getProperty("client_capabilities"); + + if (capabilitiesNode != null && capabilitiesNode.isArray()) { + capabilitiesNode.forEach(node -> capabilities.add(node.asText())); + } + + LOG.info(String.format("Extracted client capabilities: %s", capabilities)); + + return capabilities; + } + + //TODO: Re-used from other Managed Identity tests, specific to this proof-of-concept but should be more generic + static IEnvironmentVariables createEnvironmentVariables(TestConfig config) { + return new EnvironmentVariablesHelper( + SERVICE_FABRIC, + config.getEnvironmentVariable("IDENTITY_ENDPOINT")); + } + + /** + * Build parameters for token acquisition + */ + static ManagedIdentityParameters buildManagedIdentityParameters(TestAction action) { + String resource = action.getParameter("resource").asText(); + + LOG.info(String.format("Building ManagedIdentityParameters with resource: %s", resource)); + + ManagedIdentityParameters.ManagedIdentityParametersBuilder builder = + ManagedIdentityParameters.builder(resource); + + // Add optional claims challenge + if (action.hasParameter("claims_challenge")) { + builder.claims(action.getParameter("claims_challenge").asText()); + } + + //TODO: other parameters + + return builder.build(); + } + + /** + * Validate token source assertion, either cache or identity provider + */ + static void validateTokenSource(String expectedSource, IAuthenticationResult result) { + TokenSource expected = "identity_provider".equals(expectedSource) ? + TokenSource.IDENTITY_PROVIDER : TokenSource.CACHE; + LOG.info(String.format("Expected token source: %s", expected)); + LOG.info(String.format("Actual token source : %s", result.metadata().tokenSource())); + + assertEquals(expected, result.metadata().tokenSource()); + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java new file mode 100644 index 00000000..67df806a --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java @@ -0,0 +1,104 @@ +package com.microsoft.aad.msal4j; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4j.Shortcuts.TestConfig; +import com.microsoft.aad.msal4j.Shortcuts.TestObject; +import com.microsoft.aad.msal4j.Shortcuts.TestStep; +import com.microsoft.aad.msal4j.Shortcuts.TestAction; + +//TODO: Too specific for the test case used in this proof-of-concept, should be able to reuse the regular JsonHelper class +class RunnerJsonHelper { + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Parse test configuration from JSON string + */ + static TestConfig parseTestConfig(String jsonContent) { + try { + return JsonParser.parseConfig(mapper.readTree(jsonContent)); + } catch (Exception e) { + throw new RuntimeException("Failed to parse test configuration: " + e.getMessage(), e); + } + } + + /** + * Helper class for parsing JSON into test configuration + */ + private static class JsonParser { + static TestConfig parseConfig(JsonNode rootNode) { + TestConfig.Builder builder = new TestConfig.Builder() + .type(rootNode.path("type").asText()) + .version(rootNode.path("ver").asInt()); + + parseEnvironment(rootNode.path("env"), builder); + parseArrangement(rootNode.path("arrange"), builder); + parseSteps(rootNode.path("steps"), builder); + + return builder.build(); + } + + private static void parseEnvironment(JsonNode envNode, TestConfig.Builder builder) { + envNode.fields().forEachRemaining(entry -> + builder.addEnvironmentVariable(entry.getKey(), entry.getValue().asText())); + } + + private static void parseArrangement(JsonNode arrangeNode, TestConfig.Builder builder) { + arrangeNode.fields().forEachRemaining(appEntry -> { + String appName = appEntry.getKey(); + JsonNode appNode = appEntry.getValue(); + + appNode.fields().forEachRemaining(classEntry -> { + String classType = classEntry.getKey(); + JsonNode classNode = classEntry.getValue(); + + Map properties = new HashMap<>(); + classNode.fields().forEachRemaining(prop -> + properties.put(prop.getKey(), prop.getValue())); + + builder.addArrangedObject(appName, + new TestObject(appName, classType, properties)); + }); + }); + } + + private static void parseSteps(JsonNode stepsNode, TestConfig.Builder builder) { + for (JsonNode stepNode : stepsNode) { + TestAction action = parseAction(stepNode.path("act")); + Map assertions = parseAssertions(stepNode.path("assert")); + + if (action != null) { + builder.addStep(new TestStep(action, assertions)); + } + } + } + + private static TestAction parseAction(JsonNode actNode) { + if (actNode.isMissingNode()) return null; + + String actorKey = actNode.fieldNames().next(); + String[] actorParts = actorKey.split("\\.", 2); + + Map parameters = new HashMap<>(); + actNode.get(actorKey).fields().forEachRemaining(entry -> + parameters.put(entry.getKey(), entry.getValue())); + + return new TestAction(actorParts[0], actorParts[1], parameters); + } + + private static Map parseAssertions(JsonNode assertNode) { + Map assertions = new HashMap<>(); + + if (!assertNode.isMissingNode()) { + assertNode.fields().forEachRemaining(entry -> + assertions.put(entry.getKey(), entry.getValue())); + } + + return assertions; + } + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java new file mode 100644 index 00000000..f4d541b6 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java @@ -0,0 +1,29 @@ +package com.microsoft.aad.msal4j; + +import org.junit.jupiter.api.Test; + +import com.microsoft.aad.msal4j.Shortcuts.TestConfig; +import com.microsoft.aad.msal4j.Shortcuts.TestStep; + +import java.util.Map; + +class RunnerTest { + + @Test + void testManagedIdentityWithJsonConfig() throws Exception { + //TODO: get test cases list from the server + TestConfig config = RunnerJsonHelper.parseTestConfig(Shortcuts.getTestConfigJson()); + Map apps = RunnerHelper.createAppsFromConfig(config); + + for (ManagedIdentityApplication app : apps.values()) { + //Execute the "steps" section of the test config + for (TestStep step : config.getSteps()) { + //Execute the "act" section of the test config + IAuthenticationResult result = RunnerHelper.executeAction(app, step.getAction()); + + //Execute the "assert" section of the test config + RunnerHelper.validateAssertions(result, step.getAssertions()); + } + } + } +} diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java new file mode 100644 index 00000000..ee798ec6 --- /dev/null +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java @@ -0,0 +1,164 @@ +package com.microsoft.aad.msal4j; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.*; + +class Shortcuts { + //Various helpers and utilities for simplifying the first proof-of-concept implementation. + //They don't follow any design doc, and would not be part of the final implementation. + + //Represents test cases that are stored in the server. JSON is used here because the library already knows how to parse JSON, + // but the 'real' implementation could use another format. + private static final String MI_CAPABILITY_SML = "{\n" + + " \"type\": \"MSAL Test\",\n" + + " \"ver\": 1,\n" + + " \"env\": {\n" + + " \"IDENTITY_ENDPOINT\": \"fill in\",\n" + + " \"IDENTITY_HEADER\": \"foo\",\n" + + " \"IDENTITY_SERVER_THUMBPRINT\": \"bar\"\n" + + " },\n" + + " \"arrange\": {\n" + + " \"app1\": {\n" + + " \"ManagedIdentityClient\": {\n" + + " \"managed_identity\": {\n" + + " \"ManagedIdentityIdType\": \"SystemAssigned\",\n" + + " \"Id\": null\n" + + " },\n" + + " \"client_capabilities\": [\"cp1\", \"cp2\"]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"steps\": [\n" + + " {\n" + + " \"act\": {\n" + + " \"app1.AcquireTokenForManagedIdentity\": {\n" + + " \"resource\": \"R\"\n" + + " }\n" + + " },\n" + + " \"assert\": {\n" + + " \"token_type\": \"Bearer\",\n" + + " \"token_source\": \"identity_provider\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"act\": {\n" + + " \"app1.AcquireTokenForManagedIdentity\": {\n" + + " \"resource\": \"R\"\n" + + " }\n" + + " },\n" + + " \"assert\": {\n" + + " \"token_type\": \"Bearer\",\n" + + " \"token_source\": \"cache\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"act\": {\n" + + " \"app1.AcquireTokenForManagedIdentity\": {\n" + + " \"resource\": \"R\",\n" + + " \"claims_challenge\": \"{\\\"capability test case likely needs\\\": \\\"a valid json object\\\"}\"\n" + + " }\n" + + " },\n" + + " \"assert\": {\n" + + " \"token_type\": \"Bearer\",\n" + + " \"token_source\": \"identity_provider\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + //Represents a response from the tests cases server, would be + static String getTestConfigJson() { + return MI_CAPABILITY_SML; + } + + //The following static classes would ideally extend Azure JSON's JsonSerializable or some equivalent YAML parsing interface, + // but for now they are simple classes representing the structure of a test configuration. + static class TestConfig { + private final String type; + private final int version; + private final Map environment; + private final Map arrangeObjects; + private final List steps; + + private TestConfig(Shortcuts.TestConfig.Builder builder) { + this.type = builder.type; + this.version = builder.version; + this.environment = Collections.unmodifiableMap(builder.environment); + this.arrangeObjects = Collections.unmodifiableMap(builder.arrangeObjects); + this.steps = Collections.unmodifiableList(builder.steps); + } + + String getType() { return type; } + int getVersion() { return version; } + String getEnvironmentVariable(String name) { return environment.get(name); } + Map getAllEnvironmentVariables() { return environment; } + Shortcuts.TestObject getArrangeObject(String name) { return arrangeObjects.get(name); } + List getAllArrangeObjects() { return new ArrayList<>(arrangeObjects.keySet()); } + List getSteps() { return steps; } + + static class Builder { + private String type; + private int version; + private final Map environment = new HashMap<>(); + private final Map arrangeObjects = new HashMap<>(); + private final List steps = new ArrayList<>(); + + Shortcuts.TestConfig.Builder type(String type) { this.type = type; return this; } + Shortcuts.TestConfig.Builder version(int version) { this.version = version; return this; } + Shortcuts.TestConfig.Builder addEnvironmentVariable(String name, String value) { environment.put(name, value); return this; } + Shortcuts.TestConfig.Builder addArrangedObject(String name, Shortcuts.TestObject object) { arrangeObjects.put(name, object); return this; } + Shortcuts.TestConfig.Builder addStep(Shortcuts.TestStep step) { steps.add(step); return this; } + + Shortcuts.TestConfig build() { return new Shortcuts.TestConfig(this); } + } + } + + static class TestObject { + private final String name; + private final String type; + private final Map properties; + + TestObject(String name, String type, Map properties) { + this.name = name; + this.type = type; + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + String getName() { return name; } + String getType() { return type; } + JsonNode getProperty(String name) { return properties.get(name); } + } + + static class TestStep { + private final Shortcuts.TestAction action; + private final Map assertions; + + TestStep(Shortcuts.TestAction action, Map assertions) { + this.action = action; + this.assertions = Collections.unmodifiableMap(new HashMap<>(assertions)); + } + + Shortcuts.TestAction getAction() { return action; } + JsonNode getAssertion(String key) { return assertions.get(key); } + Map getAssertions() { return assertions; } + } + + static class TestAction { + private final String targetObject; + private final String methodName; + private final Map parameters; + + TestAction(String targetObject, String methodName, Map parameters) { + this.targetObject = targetObject; + this.methodName = methodName; + this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); + } + + String getTargetObject() { return targetObject; } + String getMethodName() { return methodName; } + JsonNode getParameter(String name) { return parameters.get(name); } + boolean hasParameter(String name) { return parameters.containsKey(name); } + Map getParameters() { return parameters; } + } +} From 11abc43952e479cb0a4ca8a603646900bc22732a Mon Sep 17 00:00:00 2001 From: avdunn Date: Sun, 8 Jun 2025 12:44:09 -0700 Subject: [PATCH 2/2] Use configs from server --- .../microsoft/aad/msal4j/RunnerHelper.java | 114 +++++-- .../aad/msal4j/RunnerJsonHelper.java | 81 +++++ .../com/microsoft/aad/msal4j/RunnerTest.java | 50 ++- .../com/microsoft/aad/msal4j/Shortcuts.java | 313 ++++++++++-------- 4 files changed, 383 insertions(+), 175 deletions(-) diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java index 5054b0e3..cd792552 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerHelper.java @@ -2,12 +2,11 @@ import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; - -import static com.microsoft.aad.msal4j.ManagedIdentitySourceType.SERVICE_FABRIC; import static org.junit.jupiter.api.Assertions.assertEquals; import com.microsoft.aad.msal4j.Shortcuts.TestConfig; @@ -31,7 +30,7 @@ static Map createAppsFromConfig(TestConfig c if ("ManagedIdentityClient".equals(appObject.getType())) { ManagedIdentityId identityId = createManagedIdentityId(appObject); List capabilities = extractClientCapabilities(appObject); - IEnvironmentVariables envVars = createEnvironmentVariables(config); + IEnvironmentVariables envVars = setEnvironmentVariables(config); // TODO: other application properties ManagedIdentityApplication app = ManagedIdentityApplication.builder(identityId) @@ -53,21 +52,19 @@ static Map createAppsFromConfig(TestConfig c */ static IAuthenticationResult executeAction(ManagedIdentityApplication app, TestAction action) throws Exception { if (action.getMethodName().equals("AcquireTokenForManagedIdentity")) { - LOG.info(String.format("Executing action: %s", action.getMethodName())); - + LOG.info(String.format("===Executing action: %s", action.getMethodName())); ManagedIdentityParameters params = buildManagedIdentityParameters(action); - IAuthenticationResult result = app.acquireTokenForManagedIdentity(params).get(); - LOG.info("Action result:"); - LOG.info(String.format("Access Token: %s", result.accessToken())); - LOG.info(String.format("ID Token : %s", result.idToken())); - LOG.info(String.format("Account : %s", result.account())); - LOG.info(String.format("Token Source: %s", result.metadata().tokenSource())); + LOG.info("---Action result"); + LOG.info(String.format("-Access Token: %s", result.accessToken())); + LOG.info(String.format("-ID Token : %s", result.idToken())); + LOG.info(String.format("-Account : %s", result.account())); + LOG.info(String.format("-Token Source: %s", result.metadata().tokenSource())); return result; } else { - //TODO: other token calls and apps + //TODO: other token calls and confidential/public client apps throw new UnsupportedOperationException("Unsupported action: " + action.getMethodName()); } } @@ -80,10 +77,10 @@ static void validateAssertions(IAuthenticationResult result, Map { switch (key) { case "token_source": - LOG.info("Validating token source"); + LOG.info("===Validating token source"); validateTokenSource(value.asText(), result); break; - //TODO: other assertions + //TODO: other assertions, such as exceptions checks, token content, etc. default: // Optional: Handle unknown assertion types break; @@ -95,13 +92,23 @@ static void validateAssertions(IAuthenticationResult result, Map extractClientCapabilities(TestObject testObject) { capabilitiesNode.forEach(node -> capabilities.add(node.asText())); } - LOG.info(String.format("Extracted client capabilities: %s", capabilities)); + LOG.info(String.format("---Extracted client capabilities: %s", capabilities)); return capabilities; } - //TODO: Re-used from other Managed Identity tests, specific to this proof-of-concept but should be more generic - static IEnvironmentVariables createEnvironmentVariables(TestConfig config) { - return new EnvironmentVariablesHelper( - SERVICE_FABRIC, - config.getEnvironmentVariable("IDENTITY_ENDPOINT")); + /** + * Creates provider for mocked environment variables using the test configuration. + * + * @param config The test configuration containing the environment variables + * @return An IEnvironmentVariables implementation with the configured variables + */ + static IEnvironmentVariables setEnvironmentVariables(TestConfig config) { + // Get all environment variables from the config + final Map envVars = config.getAllEnvironmentVariables(); + + LOG.info(String.format("---Configured environment variables: %s", envVars.keySet())); + + return new IEnvironmentVariables() { + @Override + public String getEnvironmentVariable(String envVariable) { + return envVars.get(envVariable); + } + }; } /** @@ -141,7 +161,8 @@ static ManagedIdentityParameters buildManagedIdentityParameters(TestAction actio // Add optional claims challenge if (action.hasParameter("claims_challenge")) { - builder.claims(action.getParameter("claims_challenge").asText()); + String validatedClaimsChallenge = Shortcuts.validateAndGetClaimsChallenge(action); + builder.claims(validatedClaimsChallenge); } //TODO: other parameters @@ -155,9 +176,42 @@ static ManagedIdentityParameters buildManagedIdentityParameters(TestAction actio static void validateTokenSource(String expectedSource, IAuthenticationResult result) { TokenSource expected = "identity_provider".equals(expectedSource) ? TokenSource.IDENTITY_PROVIDER : TokenSource.CACHE; - LOG.info(String.format("Expected token source: %s", expected)); - LOG.info(String.format("Actual token source : %s", result.metadata().tokenSource())); + LOG.info(String.format("---Expected token source: %s", expected)); + LOG.info(String.format("---Actual token source : %s", result.metadata().tokenSource())); assertEquals(expected, result.metadata().tokenSource()); } + + /** + * Complete workflow to get all test case configs + * + * @param indexEndpoint The URL of the index containing test case URLs + * @return Map of test case names to their JSON configurations + */ + static Map getAllTestCaseConfigs(String indexEndpoint) throws IOException { + // Get list of SML test case URLs + List smlUrls = RunnerJsonHelper.getTestCaseUrlsFromEndpoint(indexEndpoint); + + // Convert SML URLs to JSON URLs + List jsonUrls = Shortcuts.convertSmlUrlsToJsonUrls(smlUrls); + + // Fetch content for each JSON URL + Map testCaseConfigs = new HashMap<>(); + for (String jsonUrl : jsonUrls) { + String testCaseName = extractTestCaseName(jsonUrl); + JsonNode config = RunnerJsonHelper.fetchJsonContent(jsonUrl); + testCaseConfigs.put(testCaseName, config); + } + + return testCaseConfigs; + } + + /** + * Extract test case name from URL + */ + private static String extractTestCaseName(String url) { + String[] parts = url.split("/"); + String fileName = parts[parts.length - 1]; + return fileName.substring(0, fileName.lastIndexOf('.')); + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java index 67df806a..ebaf181c 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerJsonHelper.java @@ -2,7 +2,13 @@ import com.fasterxml.jackson.databind.JsonNode; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; @@ -11,6 +17,8 @@ import com.microsoft.aad.msal4j.Shortcuts.TestStep; import com.microsoft.aad.msal4j.Shortcuts.TestAction; +import javax.net.ssl.HttpsURLConnection; + //TODO: Too specific for the test case used in this proof-of-concept, should be able to reuse the regular JsonHelper class class RunnerJsonHelper { private static final ObjectMapper mapper = new ObjectMapper(); @@ -101,4 +109,77 @@ private static Map parseAssertions(JsonNode assertNode) { return assertions; } } + + /** + * Fetches test case URLs from an endpoint containing JSON with a "testcases" array. + * + * @param endpointUrl The URL to fetch test cases from + * @return List of test case URLs + */ + static List getTestCaseUrlsFromEndpoint(String endpointUrl) throws IOException { + List testCaseUrls = new ArrayList<>(); + + URL url = new URL(endpointUrl); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", "application/json"); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpsURLConnection.HTTP_OK) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(connection.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(response.toString()); + + if (rootNode.has("testcases") && rootNode.get("testcases").isArray()) { + JsonNode testcasesNode = rootNode.get("testcases"); + for (JsonNode testcase : testcasesNode) { + testCaseUrls.add(testcase.asText()); + } + } else { + throw new IllegalStateException("JSON response does not contain a 'testcases' array"); + } + } + } else { + throw new IOException("HTTP request failed with status code: " + responseCode); + } + + return testCaseUrls; + } + + /** + * Fetches JSON content from a URL + * + * @param jsonUrl The URL to fetch JSON content from + * @return The JSON content parsed as a JsonNode + */ + static JsonNode fetchJsonContent(String jsonUrl) throws IOException { + URL url = new URL(jsonUrl); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", "application/json"); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpsURLConnection.HTTP_OK) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(connection.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(response.toString()); + } + } else { + throw new IOException("Failed to fetch JSON content. HTTP status: " + responseCode); + } + } } diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java index f4d541b6..9765f954 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/RunnerTest.java @@ -1,29 +1,59 @@ package com.microsoft.aad.msal4j; -import org.junit.jupiter.api.Test; - +import com.fasterxml.jackson.databind.JsonNode; import com.microsoft.aad.msal4j.Shortcuts.TestConfig; import com.microsoft.aad.msal4j.Shortcuts.TestStep; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Map; +import java.util.stream.Stream; class RunnerTest { + private static final Logger LOG = LoggerFactory.getLogger(RunnerTest.class); + + /** + * Defines a set of test cases for a single unit test to run. + */ + static Stream managedIdentityTestsProvider() { + return Stream.of( + "mi_capability", + "token_sha256_to_refresh", + "mi_vm_pod" + ); + } + + @ParameterizedTest + @MethodSource("managedIdentityTestsProvider") + void runManagedIdentityTest(String testCaseName) throws Exception { + LOG.info("==========Executing Test Case=========="); + + // Get all test configurations + Map configs = RunnerHelper.getAllTestCaseConfigs("https://smile-test.azurewebsites.net/testcases.json"); - @Test - void testManagedIdentityWithJsonConfig() throws Exception { - //TODO: get test cases list from the server - TestConfig config = RunnerJsonHelper.parseTestConfig(Shortcuts.getTestConfigJson()); + LOG.info(String.format("---Found test case: %s", configs.get(testCaseName).toString())); + + TestConfig config = RunnerJsonHelper.parseTestConfig(configs.get(testCaseName).toString()); + + // Create applications from the configuration Map apps = RunnerHelper.createAppsFromConfig(config); + // For each application, execute all steps for (ManagedIdentityApplication app : apps.values()) { - //Execute the "steps" section of the test config + app.tokenCache.accessTokens.clear(); // Clear the static token cache for each test run + + // Execute each step in the test configuration for (TestStep step : config.getSteps()) { - //Execute the "act" section of the test config + LOG.info("----------Executing step----------"); + + // Execute the action IAuthenticationResult result = RunnerHelper.executeAction(app, step.getAction()); - //Execute the "assert" section of the test config + // Validate assertions RunnerHelper.validateAssertions(result, step.getAssertions()); } } } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java index ee798ec6..de607980 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/Shortcuts.java @@ -1,153 +1,149 @@ package com.microsoft.aad.msal4j; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; +import java.util.stream.Collectors; class Shortcuts { //Various helpers and utilities for simplifying the first proof-of-concept implementation. - //They don't follow any design doc, and would not be part of the final implementation. - - //Represents test cases that are stored in the server. JSON is used here because the library already knows how to parse JSON, - // but the 'real' implementation could use another format. - private static final String MI_CAPABILITY_SML = "{\n" + - " \"type\": \"MSAL Test\",\n" + - " \"ver\": 1,\n" + - " \"env\": {\n" + - " \"IDENTITY_ENDPOINT\": \"fill in\",\n" + - " \"IDENTITY_HEADER\": \"foo\",\n" + - " \"IDENTITY_SERVER_THUMBPRINT\": \"bar\"\n" + - " },\n" + - " \"arrange\": {\n" + - " \"app1\": {\n" + - " \"ManagedIdentityClient\": {\n" + - " \"managed_identity\": {\n" + - " \"ManagedIdentityIdType\": \"SystemAssigned\",\n" + - " \"Id\": null\n" + - " },\n" + - " \"client_capabilities\": [\"cp1\", \"cp2\"]\n" + - " }\n" + - " }\n" + - " },\n" + - " \"steps\": [\n" + - " {\n" + - " \"act\": {\n" + - " \"app1.AcquireTokenForManagedIdentity\": {\n" + - " \"resource\": \"R\"\n" + - " }\n" + - " },\n" + - " \"assert\": {\n" + - " \"token_type\": \"Bearer\",\n" + - " \"token_source\": \"identity_provider\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"act\": {\n" + - " \"app1.AcquireTokenForManagedIdentity\": {\n" + - " \"resource\": \"R\"\n" + - " }\n" + - " },\n" + - " \"assert\": {\n" + - " \"token_type\": \"Bearer\",\n" + - " \"token_source\": \"cache\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"act\": {\n" + - " \"app1.AcquireTokenForManagedIdentity\": {\n" + - " \"resource\": \"R\",\n" + - " \"claims_challenge\": \"{\\\"capability test case likely needs\\\": \\\"a valid json object\\\"}\"\n" + - " }\n" + - " },\n" + - " \"assert\": {\n" + - " \"token_type\": \"Bearer\",\n" + - " \"token_source\": \"identity_provider\"\n" + - " }\n" + - " }\n" + - " ]\n" + - "}"; - - //Represents a response from the tests cases server, would be - static String getTestConfigJson() { - return MI_CAPABILITY_SML; - } - - //The following static classes would ideally extend Azure JSON's JsonSerializable or some equivalent YAML parsing interface, - // but for now they are simple classes representing the structure of a test configuration. - static class TestConfig { - private final String type; - private final int version; - private final Map environment; - private final Map arrangeObjects; - private final List steps; - - private TestConfig(Shortcuts.TestConfig.Builder builder) { - this.type = builder.type; - this.version = builder.version; - this.environment = Collections.unmodifiableMap(builder.environment); - this.arrangeObjects = Collections.unmodifiableMap(builder.arrangeObjects); - this.steps = Collections.unmodifiableList(builder.steps); - } + //They don't follow any design doc or language conventions, and would not be part of the final implementation. + + //=====The following static classes would ideally extend Azure JSON's JsonSerializable or some equivalent YAML parsing interface, + // but for now they are simple classes representing the structure of a test configuration. + static class TestConfig { + private final String type; + private final int version; + private final Map environment; + private final Map arrangeObjects; + private final List steps; + private static final ObjectMapper mapper = new ObjectMapper(); + + private TestConfig(Shortcuts.TestConfig.Builder builder) { + this.type = builder.type; + this.version = builder.version; + this.environment = Collections.unmodifiableMap(builder.environment); + this.arrangeObjects = Collections.unmodifiableMap(builder.arrangeObjects); + this.steps = Collections.unmodifiableList(builder.steps); + } - String getType() { return type; } - int getVersion() { return version; } - String getEnvironmentVariable(String name) { return environment.get(name); } - Map getAllEnvironmentVariables() { return environment; } - Shortcuts.TestObject getArrangeObject(String name) { return arrangeObjects.get(name); } - List getAllArrangeObjects() { return new ArrayList<>(arrangeObjects.keySet()); } - List getSteps() { return steps; } - - static class Builder { - private String type; - private int version; - private final Map environment = new HashMap<>(); - private final Map arrangeObjects = new HashMap<>(); - private final List steps = new ArrayList<>(); - - Shortcuts.TestConfig.Builder type(String type) { this.type = type; return this; } - Shortcuts.TestConfig.Builder version(int version) { this.version = version; return this; } - Shortcuts.TestConfig.Builder addEnvironmentVariable(String name, String value) { environment.put(name, value); return this; } - Shortcuts.TestConfig.Builder addArrangedObject(String name, Shortcuts.TestObject object) { arrangeObjects.put(name, object); return this; } - Shortcuts.TestConfig.Builder addStep(Shortcuts.TestStep step) { steps.add(step); return this; } - - Shortcuts.TestConfig build() { return new Shortcuts.TestConfig(this); } - } + String getType() { + return type; + } + + int getVersion() { + return version; + } + + String getEnvironmentVariable(String name) { + return environment.get(name); + } + + Map getAllEnvironmentVariables() { + return environment; + } + + Shortcuts.TestObject getArrangeObject(String name) { + return arrangeObjects.get(name); + } + + List getAllArrangeObjects() { + return new ArrayList<>(arrangeObjects.keySet()); + } + + List getSteps() { + return steps; } - static class TestObject { - private final String name; - private final String type; - private final Map properties; + static class Builder { + private String type; + private int version; + private final Map environment = new HashMap<>(); + private final Map arrangeObjects = new HashMap<>(); + private final List steps = new ArrayList<>(); - TestObject(String name, String type, Map properties) { - this.name = name; + Shortcuts.TestConfig.Builder type(String type) { this.type = type; - this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + return this; } - String getName() { return name; } - String getType() { return type; } - JsonNode getProperty(String name) { return properties.get(name); } - } + Shortcuts.TestConfig.Builder version(int version) { + this.version = version; + return this; + } + + Shortcuts.TestConfig.Builder addEnvironmentVariable(String name, String value) { + environment.put(name, value); + return this; + } - static class TestStep { - private final Shortcuts.TestAction action; - private final Map assertions; + Shortcuts.TestConfig.Builder addArrangedObject(String name, Shortcuts.TestObject object) { + arrangeObjects.put(name, object); + return this; + } - TestStep(Shortcuts.TestAction action, Map assertions) { - this.action = action; - this.assertions = Collections.unmodifiableMap(new HashMap<>(assertions)); + Shortcuts.TestConfig.Builder addStep(Shortcuts.TestStep step) { + steps.add(step); + return this; } - Shortcuts.TestAction getAction() { return action; } - JsonNode getAssertion(String key) { return assertions.get(key); } - Map getAssertions() { return assertions; } + Shortcuts.TestConfig build() { + return new Shortcuts.TestConfig(this); + } } + } - static class TestAction { - private final String targetObject; - private final String methodName; - private final Map parameters; + static class TestObject { + private final String name; + private final String type; + private final Map properties; + + TestObject(String name, String type, Map properties) { + this.name = name; + this.type = type; + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + String getName() { + return name; + } + + String getType() { + return type; + } + + JsonNode getProperty(String name) { + return properties.get(name); + } + } + + static class TestStep { + private final Shortcuts.TestAction action; + private final Map assertions; + + TestStep(Shortcuts.TestAction action, Map assertions) { + this.action = action; + this.assertions = Collections.unmodifiableMap(new HashMap<>(assertions)); + } + + Shortcuts.TestAction getAction() { + return action; + } + + JsonNode getAssertion(String key) { + return assertions.get(key); + } + + Map getAssertions() { + return assertions; + } + } + + static class TestAction { + private final String targetObject; + private final String methodName; + private final Map parameters; TestAction(String targetObject, String methodName, Map parameters) { this.targetObject = targetObject; @@ -155,10 +151,57 @@ static class TestAction { this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters)); } - String getTargetObject() { return targetObject; } - String getMethodName() { return methodName; } - JsonNode getParameter(String name) { return parameters.get(name); } - boolean hasParameter(String name) { return parameters.containsKey(name); } - Map getParameters() { return parameters; } + String getTargetObject() { + return targetObject; + } + + String getMethodName() { + return methodName; + } + + JsonNode getParameter(String name) { + return parameters.get(name); + } + + boolean hasParameter(String name) { + return parameters.containsKey(name); + } + + Map getParameters() { + return parameters; + } + } + + //=====The following methods are small fixes for issues in the test configuration JSONs + + //Some URLs in the test configurations are malformed and are missing a slash before "test". + static String fixAzureWebsiteUrls(String jsonString) { + return jsonString.replace( + "https://smile-test.azurewebsites.nettest/token", + "https://smile-test.azurewebsites.net/test/token"); + } + + //Some test configurations use a claims challenge that is not valid JSON. + static String validateAndGetClaimsChallenge(TestAction action) { + if (!action.hasParameter("claims_challenge")) { + return null; + } + + String claimsChallenge = action.getParameter("claims_challenge").asText(); + try { + // Try to parse the claims challenge as JSON + new ObjectMapper().readTree(claimsChallenge); + return claimsChallenge; + } catch (Exception e) { + return TestConfiguration.CLAIMS_CHALLENGE; + } + } + + //The server has a public list of endpoints leading to .sml files, but there are JSON equivalents for those files at endpoints ending with .json + static List convertSmlUrlsToJsonUrls(List smlUrls) { + return smlUrls.stream() + .filter(url -> url.endsWith(".sml")) + .map(url -> url.substring(0, url.length() - 4) + ".json") + .collect(Collectors.toList()); } -} +} \ No newline at end of file