diff --git a/boms/extras/pom.xml b/boms/extras/pom.xml index e5918e876..cb71d8f80 100644 --- a/boms/extras/pom.xml +++ b/boms/extras/pom.xml @@ -94,6 +94,11 @@ a2a-java-extras-multitenancy ${project.version} + + ${project.groupId} + a2a-java-extras-multitenancy-tests-test-common + ${project.version} + diff --git a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java index 8828bc873..dea222f66 100644 --- a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java +++ b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java @@ -22,8 +22,8 @@ public class ExtrasBomVerifier extends DynamicBomVerifier { "compat-0.3/", // Compat 0.3 modules (part of SDK BOM, not extras BOM) "extras/queue-manager-replicated/tests-multi-instance/", // Test harness applications "extras/queue-manager-replicated/tests-single-instance/", // Test harness applications - "extras/opentelemetry/integration-tests/" // Test harness applications - // Note: extras/ production modules are NOT in this list - we want to verify those classes load + "extras/opentelemetry/integration-tests/", // Test harness applications + "extras/multitenancy/tests/" // Test support jar (uses provided junit/rest-assured deps) ); private static final Set EXTRAS_FORBIDDEN = Set.of( diff --git a/extras/multitenancy/pom.xml b/extras/multitenancy/pom.xml index 5d5ea9983..f3f62ab32 100644 --- a/extras/multitenancy/pom.xml +++ b/extras/multitenancy/pom.xml @@ -30,40 +30,5 @@ jakarta.inject-api provided - - org.junit.jupiter - junit-jupiter-api - test - - - io.quarkus - quarkus-junit5 - test - - - io.rest-assured - rest-assured - test - - - ${project.groupId} - a2a-java-sdk-reference-jsonrpc - test - - - ${project.groupId} - a2a-java-sdk-reference-rest - test - - - ${project.groupId} - a2a-java-sdk-client-transport-jsonrpc - test - - - ${project.groupId} - a2a-java-sdk-client - test - diff --git a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentCardProducer.java b/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentCardProducer.java deleted file mode 100644 index a521082e7..000000000 --- a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentCardProducer.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.a2aproject.sdk.extras.multitenancy.it; - -import java.util.List; - -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Singleton; - -import org.a2aproject.sdk.extras.multitenancy.Tenant; -import org.a2aproject.sdk.server.ExtendedAgentCard; -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.spec.AgentCapabilities; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.TransportProtocol; - -@Singleton -public class MultiTenantAgentCardProducer { - - private static final String BASE_URL = "http://localhost:8081"; - - @Produces - @PublicAgentCard - public AgentCard publicCard() { - return AgentCard.builder() - .name("Multi-Tenant Test Agent") - .description("Test agent for multitenancy integration tests") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder() - .streaming(true) - .extendedAgentCard(true) - .build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL))) - .build(); - } - - @Produces - @Singleton - @ExtendedAgentCard - public AgentCard defaultExtendedCard() { - return AgentCard.builder() - .name("default-extended") - .description("Default extended card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL))) - .build(); - } - - @Produces - @Singleton - @Tenant("acme") - @ExtendedAgentCard - public AgentCard acmeExtendedCard() { - return AgentCard.builder() - .name("acme-extended") - .description("Acme extended card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL + "/acme"))) - .build(); - } - - @Produces - @Singleton - @Tenant("acme") - public AgentCard acmePublicCard() { - return AgentCard.builder() - .name("Acme Agent") - .description("Acme-specific public agent card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder() - .streaming(true) - .extendedAgentCard(true) - .build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL + "/acme"))) - .build(); - } - - @Produces - @Singleton - @Tenant("beta") - @ExtendedAgentCard - public AgentCard betaExtendedCard() { - return AgentCard.builder() - .name("beta-extended") - .description("Beta extended card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL + "/beta"))) - .build(); - } - - @Produces - @Singleton - @Tenant("beta") - public AgentCard betaPublicCard() { - return AgentCard.builder() - .name("Beta Agent") - .description("Beta-specific public agent card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder() - .streaming(true) - .extendedAgentCard(true) - .build()) - .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), BASE_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), BASE_URL + "/beta"))) - .build(); - } -} diff --git a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentExecutorProducer.java b/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentExecutorProducer.java deleted file mode 100644 index 80c68d1cb..000000000 --- a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantAgentExecutorProducer.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.a2aproject.sdk.extras.multitenancy.it; - -import java.util.List; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; - -import org.a2aproject.sdk.extras.multitenancy.Tenant; -import org.a2aproject.sdk.server.agentexecution.AgentExecutor; -import org.a2aproject.sdk.server.agentexecution.RequestContext; -import org.a2aproject.sdk.server.tasks.AgentEmitter; -import org.a2aproject.sdk.spec.A2AError; -import org.a2aproject.sdk.spec.TextPart; - -@ApplicationScoped -public class MultiTenantAgentExecutorProducer { - - @Produces - public AgentExecutor defaultExecutor() { - return new TenantEchoExecutor("default"); - } - - @Produces - @Tenant("acme") - public AgentExecutor acmeExecutor() { - return new TenantEchoExecutor("acme"); - } - - @Produces - @Tenant("beta") - public AgentExecutor betaExecutor() { - return new TenantEchoExecutor("beta"); - } - - static class TenantEchoExecutor implements AgentExecutor { - private final String label; - - TenantEchoExecutor(String label) { - this.label = label; - } - - @Override - public void execute(RequestContext context, AgentEmitter emitter) throws A2AError { - emitter.startWork(); - emitter.addArtifact(List.of(new TextPart(label))); - emitter.complete(); - } - - @Override - public void cancel(RequestContext context, AgentEmitter emitter) throws A2AError { - emitter.cancel(); - } - } -} diff --git a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantJSONRPCTest.java b/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantJSONRPCTest.java deleted file mode 100644 index 884ccad5c..000000000 --- a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantJSONRPCTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package org.a2aproject.sdk.extras.multitenancy.it; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; -import io.restassured.response.Response; -import org.a2aproject.sdk.common.A2AHeaders; -import org.a2aproject.sdk.spec.AgentInterface; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class MultiTenantJSONRPCTest { - - @Test - public void knownTenantRoutesToAcmeExecutor() { - String response = rawPost(buildSendMessageRequest("acme")); - assertArtifactText(response, "acme"); - } - - @Test - public void secondTenantRoutesToBetaExecutor() { - String response = rawPost(buildSendMessageRequest("beta")); - assertArtifactText(response, "beta"); - } - - @Test - public void unknownTenantFallsBackToDefault() { - String response = rawPost(buildSendMessageRequest("unknown-corp")); - assertArtifactText(response, "default"); - } - - @Test - public void nullTenantUsesDefault() { - String response = rawPost(buildSendMessageRequest(null)); - assertArtifactText(response, "default"); - } - - @Test - public void getExtendedAgentCardWithAcmeTenant() { - String response = rawPost(buildGetExtendedAgentCardRequest("acme")); - JsonPath json = JsonPath.from(response); - assertEquals("acme-extended", json.getString("result.name")); - } - - @Test - public void getExtendedAgentCardWithBetaTenant() { - String response = rawPost(buildGetExtendedAgentCardRequest("beta")); - JsonPath json = JsonPath.from(response); - assertEquals("beta-extended", json.getString("result.name")); - } - - @Test - public void getExtendedAgentCardWithUnknownTenant() { - String response = rawPost(buildGetExtendedAgentCardRequest("unknown")); - JsonPath json = JsonPath.from(response); - assertEquals("default-extended", json.getString("result.name")); - } - - @Test - public void getExtendedAgentCardWithoutTenant() { - String response = rawPost(buildGetExtendedAgentCardRequest(null)); - JsonPath json = JsonPath.from(response); - assertEquals("default-extended", json.getString("result.name")); - } - - @Test - public void streamingWithKnownTenant() { - String msgId = UUID.randomUUID().toString(); - Response response = RestAssured.given() - .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(buildStreamingMessageRequest("acme", msgId)) - .when().post("/"); - - assertEquals(200, response.getStatusCode()); - String body = response.getBody().asString(); - assertTrue(body.contains("\"text\":\"acme\""), "Stream should contain acme artifact: " + body); - } - - @Test - public void streamingWithUnknownTenantUsesDefault() { - String msgId = UUID.randomUUID().toString(); - Response response = RestAssured.given() - .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(buildStreamingMessageRequest("unknown", msgId)) - .when().post("/"); - - assertEquals(200, response.getStatusCode()); - String body = response.getBody().asString(); - assertTrue(body.contains("\"text\":\"default\""), "Stream should contain default artifact: " + body); - } - - @Test - public void getPublicAgentCardWithAcmeTenant() { - String response = RestAssured.given() - .when().get("/.well-known/acme/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Acme Agent", json.getString("name")); - } - - @Test - public void getPublicAgentCardWithBetaTenant() { - String response = RestAssured.given() - .when().get("/.well-known/beta/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Beta Agent", json.getString("name")); - } - - @Test - public void unknownTenantReturns404() { - RestAssured.given() - .when().get("/.well-known/unknown/agent-card.json") - .then().statusCode(404); - } - - @Test - public void getPublicAgentCardWithoutTenantReturnsDefault() { - String response = RestAssured.given() - .when().get("/.well-known/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Multi-Tenant Test Agent", json.getString("name")); - } - - @Test - public void concurrentRequestsForDifferentTenants() throws Exception { - ExecutorService pool = Executors.newFixedThreadPool(3); - try { - CompletableFuture acmeFuture = CompletableFuture.supplyAsync( - () -> rawPost(buildSendMessageRequest("acme", UUID.randomUUID().toString())), pool); - CompletableFuture betaFuture = CompletableFuture.supplyAsync( - () -> rawPost(buildSendMessageRequest("beta", UUID.randomUUID().toString())), pool); - CompletableFuture defaultFuture = CompletableFuture.supplyAsync( - () -> rawPost(buildSendMessageRequest(null, UUID.randomUUID().toString())), pool); - - assertArtifactText(acmeFuture.get(), "acme"); - assertArtifactText(betaFuture.get(), "beta"); - assertArtifactText(defaultFuture.get(), "default"); - } finally { - pool.shutdown(); - } - } - - private String rawPost(String body) { - return RestAssured.given() - .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(body) - .when().post("/") - .then().statusCode(200) - .extract().asString(); - } - - private void assertArtifactText(String response, String expected) { - JsonPath json = JsonPath.from(response); - assertNotNull(json.getString("result"), "Expected result in response: " + response); - assertEquals(expected, json.getString("result.task.artifacts[0].parts[0].text")); - } - - private static String buildSendMessageRequest(@Nullable String tenant) { - return buildSendMessageRequest(tenant, "msg-1"); - } - - private static String buildSendMessageRequest(@Nullable String tenant, String messageId) { - return """ - { - "jsonrpc": "2.0", - "id": "1", - "method": "SendMessage", - "params": { - "message": { - "messageId": "%s", - "role": "ROLE_USER", - "parts": [{"text": "hello"}] - }%s - } - }""".formatted(messageId, buildTenantField(tenant)); - } - - private static String buildStreamingMessageRequest(@Nullable String tenant, String messageId) { - return """ - { - "jsonrpc": "2.0", - "id": "1", - "method": "SendStreamingMessage", - "params": { - "message": { - "messageId": "%s", - "role": "ROLE_USER", - "parts": [{"text": "hello"}] - }%s - } - }""".formatted(messageId, buildTenantField(tenant)); - } - - private static String buildTenantField(@Nullable String tenant) { - return tenant != null && !tenant.isBlank() - ? """ - , "tenant": "%s\"""".formatted(tenant) - : ""; - } - - private static String buildGetExtendedAgentCardRequest(@Nullable String tenant) { - String params = tenant != null - ? """ - , "params": { "tenant": "%s" }""".formatted(tenant) - : ""; - return """ - { "jsonrpc": "2.0", "method": "GetExtendedAgentCard", "id": "1"%s }""".formatted(params); - } -} diff --git a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantRESTTest.java b/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantRESTTest.java deleted file mode 100644 index 191667484..000000000 --- a/extras/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/it/MultiTenantRESTTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.a2aproject.sdk.extras.multitenancy.it; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; -import io.restassured.response.Response; -import org.a2aproject.sdk.spec.AgentInterface; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class MultiTenantRESTTest { - - @Test - public void knownTenantRoutesToAcmeExecutor() { - String response = postMessageSend("acme"); - assertArtifactText(response, "acme"); - } - - @Test - public void secondTenantRoutesToBetaExecutor() { - String response = postMessageSend("beta"); - assertArtifactText(response, "beta"); - } - - @Test - public void unknownTenantFallsBackToDefault() { - String response = postMessageSend("unknown-corp"); - assertArtifactText(response, "default"); - } - - @Test - public void noTenantUsesDefault() { - String response = postMessageSend(null); - assertArtifactText(response, "default"); - } - - @Test - public void getExtendedAgentCardWithAcmeTenant() { - String response = RestAssured.given() - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .when().get("/acme/extendedAgentCard") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("acme-extended", json.getString("name")); - } - - @Test - public void getExtendedAgentCardWithBetaTenant() { - String response = RestAssured.given() - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .when().get("/beta/extendedAgentCard") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("beta-extended", json.getString("name")); - } - - @Test - public void getExtendedAgentCardWithUnknownTenant() { - String response = RestAssured.given() - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .when().get("/unknown/extendedAgentCard") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("default-extended", json.getString("name")); - } - - @Test - public void getExtendedAgentCardWithoutTenant() { - String response = RestAssured.given() - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .when().get("/extendedAgentCard") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("default-extended", json.getString("name")); - } - - @Test - public void streamingWithKnownTenant() { - Response response = RestAssured.given() - .urlEncodingEnabled(false) - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(buildMessageBody()) - .when().post("/acme/message:stream"); - - assertEquals(200, response.getStatusCode()); - String body = response.getBody().asString(); - assertTrue(body.contains("\"text\":\"acme\""), "Stream should contain acme artifact: " + body); - } - - @Test - public void streamingWithUnknownTenantUsesDefault() { - Response response = RestAssured.given() - .urlEncodingEnabled(false) - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(buildMessageBody()) - .when().post("/unknown/message:stream"); - - assertEquals(200, response.getStatusCode()); - String body = response.getBody().asString(); - assertTrue(body.contains("\"text\":\"default\""), "Stream should contain default artifact: " + body); - } - - @Test - public void getPublicAgentCardWithAcmeTenant() { - String response = RestAssured.given() - .when().get("/.well-known/acme/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Acme Agent", json.getString("name")); - } - - @Test - public void getPublicAgentCardWithBetaTenant() { - String response = RestAssured.given() - .when().get("/.well-known/beta/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Beta Agent", json.getString("name")); - } - - @Test - public void unknownTenantReturns404() { - RestAssured.given() - .when().get("/.well-known/unknown/agent-card.json") - .then().statusCode(404); - } - - @Test - public void getPublicAgentCardWithoutTenantReturnsDefault() { - String response = RestAssured.given() - .when().get("/.well-known/agent-card.json") - .then().statusCode(200) - .extract().asString(); - JsonPath json = JsonPath.from(response); - assertEquals("Multi-Tenant Test Agent", json.getString("name")); - } - - @Test - public void concurrentRequestsForDifferentTenants() throws Exception { - ExecutorService pool = Executors.newFixedThreadPool(3); - try { - CompletableFuture acmeFuture = CompletableFuture.supplyAsync( - () -> postMessageSend("acme"), pool); - CompletableFuture betaFuture = CompletableFuture.supplyAsync( - () -> postMessageSend("beta"), pool); - CompletableFuture defaultFuture = CompletableFuture.supplyAsync( - () -> postMessageSend(null), pool); - - assertArtifactText(acmeFuture.get(), "acme"); - assertArtifactText(betaFuture.get(), "beta"); - assertArtifactText(defaultFuture.get(), "default"); - } finally { - pool.shutdown(); - } - } - - private String postMessageSend(@Nullable String tenant) { - String path = tenant != null ? "/" + tenant + "/message:send" : "/message:send"; - return RestAssured.given() - .urlEncodingEnabled(false) - .header("A2A-Version", AgentInterface.CURRENT_PROTOCOL_VERSION) - .contentType("application/json") - .body(buildMessageBody()) - .when().post(path) - .then().statusCode(200) - .extract().asString(); - } - - private void assertArtifactText(String response, String expected) { - JsonPath json = JsonPath.from(response); - assertNotNull(json.getString("task"), "Expected task in response: " + response); - assertEquals(expected, json.getString("task.artifacts[0].parts[0].text")); - } - - private static String buildMessageBody() { - return """ - { - "message": { - "messageId": "%s", - "role": "ROLE_USER", - "parts": [{"text": "hello"}] - } - }""".formatted(UUID.randomUUID().toString()); - } -} diff --git a/extras/multitenancy/src/test/resources/application.properties b/extras/multitenancy/src/test/resources/application.properties deleted file mode 100644 index 30af146a3..000000000 --- a/extras/multitenancy/src/test/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -# Authorization is not configured in test profiles -a2a.authorization.required=false diff --git a/tests/multitenancy/grpc/pom.xml b/extras/multitenancy/tests/grpc/pom.xml similarity index 79% rename from tests/multitenancy/grpc/pom.xml rename to extras/multitenancy/tests/grpc/pom.xml index 9d0c98745..6af47f947 100644 --- a/tests/multitenancy/grpc/pom.xml +++ b/extras/multitenancy/tests/grpc/pom.xml @@ -8,15 +8,15 @@ org.a2aproject.sdk a2a-java-sdk-parent 1.3.2.Final-SNAPSHOT - ../../../pom.xml + ../../../../pom.xml - a2a-java-sdk-tests-multitenancy-grpc + a2a-java-extras-multitenancy-tests-grpc jar - Java A2A SDK Tests Multitenancy gRPC - Integration tests for multi-tenant executor routing on a gRPC server + A2A Java SDK :: Extras :: Multitenancy :: Tests :: gRPC + Multitenancy integration tests for the gRPC transport true @@ -25,37 +25,44 @@ ${project.groupId} - a2a-java-sdk-reference-grpc + a2a-java-extras-multitenancy-tests-test-common + ${project.version} test ${project.groupId} - a2a-java-extras-multitenancy + a2a-java-sdk-reference-grpc + ${project.version} test ${project.groupId} - a2a-java-sdk-tests-multitenancy-test-common + a2a-java-extras-multitenancy + ${project.version} test ${project.groupId} a2a-java-sdk-spec + ${project.version} test ${project.groupId} a2a-java-sdk-client + ${project.version} test ${project.groupId} a2a-java-sdk-client-transport-grpc + ${project.version} test io.quarkus quarkus-grpc + test io.quarkus @@ -67,7 +74,6 @@ junit-jupiter-api test - io.quarkus quarkus-security diff --git a/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java b/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java new file mode 100644 index 000000000..e92e071df --- /dev/null +++ b/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java @@ -0,0 +1,80 @@ +package org.a2aproject.sdk.extras.multitenancy.tests.grpc; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.client.ClientBuilder; +import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; +import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; +import org.a2aproject.sdk.extras.multitenancy.tests.AbstractMultiTenantServerTest; +import org.a2aproject.sdk.spec.TransportProtocol; +import org.junit.jupiter.api.AfterAll; + +@QuarkusTest +public class MultiTenantGrpcTest extends AbstractMultiTenantServerTest { + + // Two clients (streaming + non-streaming) are built per test instance, each creating its own + // channel; accumulate them all here so @AfterAll can close every one, not just the last. + private static final List channels = new CopyOnWriteArrayList<>(); + + public MultiTenantGrpcTest() { + super(TEST_PORT); + } + + @Override + protected String getTransportProtocol() { + return TransportProtocol.GRPC.asString(); + } + + @Override + protected String getTransportUrl() { + return "localhost:" + TEST_PORT; + } + + @Override + protected void configureTransport(ClientBuilder builder) { + builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> { + ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); + channels.add(channel); + return channel; + })); + } + + @AfterAll + public static void closeChannels() { + for (ManagedChannel channel : channels) { + channel.shutdownNow(); + try { + channel.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + channels.clear(); + } + + // gRPC-only deployments do not serve the well-known public-card endpoints. + @Override + public void publicCardWithoutTenantReturnsDefault() { + // no-op: not served by gRPC + } + + @Override + public void publicCardWithAcmeTenant() { + // no-op: not served by gRPC + } + + @Override + public void publicCardWithBetaTenant() { + // no-op: not served by gRPC + } + + @Override + public void publicCardUnknownTenantReturns404() { + // no-op: not served by gRPC + } +} diff --git a/extras/multitenancy/tests/grpc/src/test/resources/a2a-requesthandler-test.properties b/extras/multitenancy/tests/grpc/src/test/resources/a2a-requesthandler-test.properties new file mode 100644 index 000000000..a8f637a17 --- /dev/null +++ b/extras/multitenancy/tests/grpc/src/test/resources/a2a-requesthandler-test.properties @@ -0,0 +1 @@ +preferred-transport=GRPC diff --git a/tests/multitenancy/grpc/src/test/resources/application.properties b/extras/multitenancy/tests/grpc/src/test/resources/application.properties similarity index 100% rename from tests/multitenancy/grpc/src/test/resources/application.properties rename to extras/multitenancy/tests/grpc/src/test/resources/application.properties diff --git a/tests/multitenancy/jsonrpc/pom.xml b/extras/multitenancy/tests/jsonrpc/pom.xml similarity index 67% rename from tests/multitenancy/jsonrpc/pom.xml rename to extras/multitenancy/tests/jsonrpc/pom.xml index 7792d7e24..d231818af 100644 --- a/tests/multitenancy/jsonrpc/pom.xml +++ b/extras/multitenancy/tests/jsonrpc/pom.xml @@ -8,34 +8,49 @@ org.a2aproject.sdk a2a-java-sdk-parent 1.3.2.Final-SNAPSHOT - ../../../pom.xml + ../../../../pom.xml - a2a-java-sdk-tests-multitenancy-jsonrpc + a2a-java-extras-multitenancy-tests-jsonrpc jar - Java A2A SDK Tests Multitenancy JSON-RPC - Integration tests for multi-tenant public agent card on a JSON-RPC-only server (no REST transport on classpath) + A2A Java SDK :: Extras :: Multitenancy :: Tests :: JSON-RPC + Multitenancy integration tests for the JSON-RPC transport (no REST on classpath) true + + ${project.groupId} + a2a-java-extras-multitenancy-tests-test-common + ${project.version} + test + ${project.groupId} a2a-java-sdk-reference-jsonrpc + ${project.version} test ${project.groupId} a2a-java-extras-multitenancy + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-client + ${project.version} test ${project.groupId} - a2a-java-sdk-tests-multitenancy-test-common + a2a-java-sdk-client-transport-jsonrpc + ${project.version} test @@ -53,7 +68,6 @@ rest-assured test - io.quarkus quarkus-security diff --git a/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java b/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java new file mode 100644 index 000000000..a030f07b3 --- /dev/null +++ b/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java @@ -0,0 +1,33 @@ +package org.a2aproject.sdk.extras.multitenancy.tests.jsonrpc; + +import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.client.ClientBuilder; +import org.a2aproject.sdk.client.http.JdkA2AHttpClient; +import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport; +import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfigBuilder; +import org.a2aproject.sdk.extras.multitenancy.tests.AbstractMultiTenantServerTest; +import org.a2aproject.sdk.spec.TransportProtocol; + +@QuarkusTest +public class MultiTenantJSONRPCTest extends AbstractMultiTenantServerTest { + + public MultiTenantJSONRPCTest() { + super(TEST_PORT); + } + + @Override + protected String getTransportProtocol() { + return TransportProtocol.JSONRPC.asString(); + } + + @Override + protected String getTransportUrl() { + return "http://localhost:" + TEST_PORT; + } + + @Override + protected void configureTransport(ClientBuilder builder) { + builder.withTransport(JSONRPCTransport.class, + new JSONRPCTransportConfigBuilder().httpClient(new JdkA2AHttpClient())); + } +} diff --git a/tests/multitenancy/test-common/src/main/resources/META-INF/beans.xml b/extras/multitenancy/tests/jsonrpc/src/test/resources/META-INF/beans.xml similarity index 100% rename from tests/multitenancy/test-common/src/main/resources/META-INF/beans.xml rename to extras/multitenancy/tests/jsonrpc/src/test/resources/META-INF/beans.xml diff --git a/extras/multitenancy/tests/jsonrpc/src/test/resources/a2a-requesthandler-test.properties b/extras/multitenancy/tests/jsonrpc/src/test/resources/a2a-requesthandler-test.properties new file mode 100644 index 000000000..2d2582df3 --- /dev/null +++ b/extras/multitenancy/tests/jsonrpc/src/test/resources/a2a-requesthandler-test.properties @@ -0,0 +1 @@ +preferred-transport=JSONRPC diff --git a/tests/multitenancy/jsonrpc/src/test/resources/application.properties b/extras/multitenancy/tests/jsonrpc/src/test/resources/application.properties similarity index 100% rename from tests/multitenancy/jsonrpc/src/test/resources/application.properties rename to extras/multitenancy/tests/jsonrpc/src/test/resources/application.properties diff --git a/extras/multitenancy/tests/rest/pom.xml b/extras/multitenancy/tests/rest/pom.xml new file mode 100644 index 000000000..a9a2329b0 --- /dev/null +++ b/extras/multitenancy/tests/rest/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + org.a2aproject.sdk + a2a-java-sdk-parent + 1.3.2.Final-SNAPSHOT + ../../../../pom.xml + + + a2a-java-extras-multitenancy-tests-rest + + jar + + A2A Java SDK :: Extras :: Multitenancy :: Tests :: REST + Multitenancy integration tests for the REST transport + + + true + + + + + ${project.groupId} + a2a-java-extras-multitenancy-tests-test-common + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-reference-rest + ${project.version} + test + + + ${project.groupId} + a2a-java-extras-multitenancy + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-client + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-client-transport-rest + ${project.version} + test + + + io.quarkus + quarkus-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-security + test + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + diff --git a/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java b/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java new file mode 100644 index 000000000..a54740705 --- /dev/null +++ b/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java @@ -0,0 +1,33 @@ +package org.a2aproject.sdk.extras.multitenancy.tests.rest; + +import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.client.ClientBuilder; +import org.a2aproject.sdk.client.http.JdkA2AHttpClient; +import org.a2aproject.sdk.client.transport.rest.RestTransport; +import org.a2aproject.sdk.client.transport.rest.RestTransportConfigBuilder; +import org.a2aproject.sdk.extras.multitenancy.tests.AbstractMultiTenantServerTest; +import org.a2aproject.sdk.spec.TransportProtocol; + +@QuarkusTest +public class MultiTenantRestTest extends AbstractMultiTenantServerTest { + + public MultiTenantRestTest() { + super(TEST_PORT); + } + + @Override + protected String getTransportProtocol() { + return TransportProtocol.HTTP_JSON.asString(); + } + + @Override + protected String getTransportUrl() { + return "http://localhost:" + TEST_PORT; + } + + @Override + protected void configureTransport(ClientBuilder builder) { + builder.withTransport(RestTransport.class, + new RestTransportConfigBuilder().httpClient(new JdkA2AHttpClient())); + } +} diff --git a/tests/multitenancy/jsonrpc/src/test/resources/META-INF/beans.xml b/extras/multitenancy/tests/rest/src/test/resources/META-INF/beans.xml similarity index 87% rename from tests/multitenancy/jsonrpc/src/test/resources/META-INF/beans.xml rename to extras/multitenancy/tests/rest/src/test/resources/META-INF/beans.xml index b0e6ff3e2..548d44b72 100644 --- a/tests/multitenancy/jsonrpc/src/test/resources/META-INF/beans.xml +++ b/extras/multitenancy/tests/rest/src/test/resources/META-INF/beans.xml @@ -1,7 +1,6 @@ - + bean-discovery-mode="annotated"> diff --git a/extras/multitenancy/tests/rest/src/test/resources/a2a-requesthandler-test.properties b/extras/multitenancy/tests/rest/src/test/resources/a2a-requesthandler-test.properties new file mode 100644 index 000000000..61696e179 --- /dev/null +++ b/extras/multitenancy/tests/rest/src/test/resources/a2a-requesthandler-test.properties @@ -0,0 +1 @@ +preferred-transport=HTTP+JSON diff --git a/extras/multitenancy/tests/rest/src/test/resources/application.properties b/extras/multitenancy/tests/rest/src/test/resources/application.properties new file mode 100644 index 000000000..44cbfd7a4 --- /dev/null +++ b/extras/multitenancy/tests/rest/src/test/resources/application.properties @@ -0,0 +1 @@ +a2a.authorization.required=false diff --git a/tests/multitenancy/pom.xml b/extras/multitenancy/tests/router-unit/pom.xml similarity index 81% rename from tests/multitenancy/pom.xml rename to extras/multitenancy/tests/router-unit/pom.xml index 74a4ed45c..318f11145 100644 --- a/tests/multitenancy/pom.xml +++ b/extras/multitenancy/tests/router-unit/pom.xml @@ -8,14 +8,14 @@ org.a2aproject.sdk a2a-java-sdk-parent 1.3.2.Final-SNAPSHOT - ../../pom.xml + ../../../../pom.xml - a2a-java-sdk-tests-multitenancy + a2a-java-extras-multitenancy-tests-router-unit jar - Java A2A SDK Tests Multitenancy + A2A Java SDK :: Extras :: Multitenancy :: Tests :: Router Unit Weld SE unit tests for CDI-based multitenancy routers @@ -26,10 +26,12 @@ ${project.groupId} a2a-java-extras-multitenancy + ${project.version} ${project.groupId} a2a-java-sdk-server-common + ${project.version} org.junit.jupiter diff --git a/tests/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java similarity index 100% rename from tests/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java rename to extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java diff --git a/tests/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java similarity index 100% rename from tests/multitenancy/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java rename to extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java diff --git a/extras/multitenancy/tests/test-common/pom.xml b/extras/multitenancy/tests/test-common/pom.xml new file mode 100644 index 000000000..27517e037 --- /dev/null +++ b/extras/multitenancy/tests/test-common/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + org.a2aproject.sdk + a2a-java-sdk-parent + 1.3.2.Final-SNAPSHOT + ../../../../pom.xml + + + a2a-java-extras-multitenancy-tests-test-common + + jar + + A2A Java SDK :: Extras :: Multitenancy :: Tests :: Common + Publishable shared test infrastructure (CDI producers + abstract test) for multitenancy integration tests + + + + ${project.groupId} + a2a-java-extras-multitenancy + ${project.version} + + + ${project.groupId} + a2a-java-sdk-server-common + ${project.version} + + + ${project.groupId} + a2a-java-sdk-spec + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client + ${project.version} + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + org.eclipse.microprofile.config + microprofile-config-api + provided + + + org.junit.jupiter + junit-jupiter-api + provided + + + io.quarkus + quarkus-junit5 + provided + + + io.rest-assured + rest-assured + provided + + + io.quarkus + quarkus-security-runtime-spi + provided + + + diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java new file mode 100644 index 000000000..1717a992e --- /dev/null +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java @@ -0,0 +1,279 @@ +package org.a2aproject.sdk.extras.multitenancy.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import io.restassured.RestAssured; +import io.restassured.path.json.JsonPath; +import org.a2aproject.sdk.client.Client; +import org.a2aproject.sdk.client.ClientBuilder; +import org.a2aproject.sdk.client.ClientEvent; +import org.a2aproject.sdk.client.TaskEvent; +import org.a2aproject.sdk.client.TaskUpdateEvent; +import org.a2aproject.sdk.client.config.ClientConfig; +import org.a2aproject.sdk.spec.AgentCapabilities; +import org.a2aproject.sdk.spec.AgentCard; +import org.a2aproject.sdk.spec.AgentInterface; +import org.a2aproject.sdk.spec.Artifact; +import org.a2aproject.sdk.spec.Message; +import org.a2aproject.sdk.spec.MessageSendParams; +import org.a2aproject.sdk.spec.Task; +import org.a2aproject.sdk.spec.TaskState; +import org.a2aproject.sdk.spec.TextPart; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Transport-agnostic multitenancy server test driven through the real A2A {@link Client}. + * Concrete {@code @QuarkusTest} subclasses implement the three transport hooks and each carry + * exactly one transport's reference module on the classpath. + */ +public abstract class AbstractMultiTenantServerTest { + + /** Port every subclass's Quarkus test server and client URL/target must agree on. */ + protected static final int TEST_PORT = 8081; + + protected final int serverPort; + private Client streamingClient; + private Client nonStreamingClient; + + protected AbstractMultiTenantServerTest(int serverPort) { + this.serverPort = serverPort; + } + + /** e.g. "JSONRPC", "GRPC", "HTTP+JSON". */ + protected abstract String getTransportProtocol(); + + /** + * Base URL/target the client uses to reach the server. Must encode the same port passed to + * this class's constructor (e.g. {@code "http://localhost:" + TEST_PORT}, or {@code "localhost:" + TEST_PORT} + * for gRPC's schemeless target) — the two are not derived from each other. + */ + protected abstract String getTransportUrl(); + + /** Wire the transport-specific {@code Client} (channel/http-client factory). */ + protected abstract void configureTransport(ClientBuilder builder); + + // ---------- executor routing (all transports) ---------- + + @Test + public void knownTenantRoutesToAcmeExecutor() throws Exception { + assertEquals(Tenants.ACME, sendAndGetArtifactText(Tenants.ACME, false)); + } + + @Test + public void secondTenantRoutesToBetaExecutor() throws Exception { + assertEquals(Tenants.BETA, sendAndGetArtifactText(Tenants.BETA, false)); + } + + @Test + public void unknownTenantFallsBackToDefault() throws Exception { + assertEquals(Tenants.DEFAULT_LABEL, sendAndGetArtifactText(Tenants.UNKNOWN, false)); + } + + @Test + public void nullTenantUsesDefault() throws Exception { + assertEquals(Tenants.DEFAULT_LABEL, sendAndGetArtifactText(null, false)); + } + + // ---------- streaming routing (all transports) ---------- + + @Test + public void streamingWithKnownTenant() throws Exception { + assertEquals(Tenants.ACME, sendAndGetArtifactText(Tenants.ACME, true)); + } + + @Test + public void streamingWithUnknownTenantUsesDefault() throws Exception { + assertEquals(Tenants.DEFAULT_LABEL, sendAndGetArtifactText(Tenants.UNKNOWN, true)); + } + + // ---------- concurrency (all transports) ---------- + + @Test + public void concurrentRequestsForDifferentTenants() throws Exception { + ExecutorService pool = Executors.newFixedThreadPool(3); + try { + CompletableFuture acme = CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.ACME), pool); + CompletableFuture beta = CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.BETA), pool); + CompletableFuture defaultFuture = CompletableFuture.supplyAsync(() -> sendQuietly(null), pool); + assertEquals(Tenants.ACME, acme.get(30, TimeUnit.SECONDS)); + assertEquals(Tenants.BETA, beta.get(30, TimeUnit.SECONDS)); + assertEquals(Tenants.DEFAULT_LABEL, defaultFuture.get(30, TimeUnit.SECONDS)); + } finally { + pool.shutdown(); + } + } + + // ---------- extended card via Client (all transports, incl. gRPC RPC) ---------- + + @Test + public void extendedCardKnownTenantResolvesToAcme() throws Exception { + assertEquals("acme-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.ACME, null).name()); + } + + @Test + public void extendedCardSecondTenantResolvesToBeta() throws Exception { + assertEquals("beta-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.BETA, null).name()); + } + + @Test + public void extendedCardUnknownTenantFallsBackToDefault() throws Exception { + assertEquals("default-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.UNKNOWN, null).name()); + } + + @Test + public void extendedCardNullTenantUsesDefault() throws Exception { + assertEquals("default-extended", getNonStreamingClient().getExtendedAgentCard((String) null, null).name()); + } + + // ---------- public well-known card (HTTP only; gRPC overrides to no-ops) ---------- + + @Test + public void publicCardWithoutTenantReturnsDefault() { + assertEquals("Default Agent", getWellKnownCardName("/.well-known/agent-card.json")); + } + + @Test + public void publicCardWithAcmeTenant() { + assertEquals("Acme Agent", getWellKnownCardName("/.well-known/acme/agent-card.json")); + } + + @Test + public void publicCardWithBetaTenant() { + assertEquals("Beta Agent", getWellKnownCardName("/.well-known/beta/agent-card.json")); + } + + @Test + public void publicCardUnknownTenantReturns404() { + RestAssured.given().when().get(wellKnownUrl("/.well-known/unknown-corp/agent-card.json")) + .then().statusCode(404); + } + + // ---------- lifecycle ---------- + + @AfterEach + public void closeClients() { + if (streamingClient != null) { + streamingClient.close(); + } + if (nonStreamingClient != null) { + nonStreamingClient.close(); + } + } + + // ---------- helpers ---------- + + private String sendQuietly(@Nullable String tenant) { + try { + return sendAndGetArtifactText(tenant, false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String sendAndGetArtifactText(@Nullable String tenant, boolean streaming) throws Exception { + Message message = Message.builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("hello"))) + .build(); + MessageSendParams.Builder params = MessageSendParams.builder().message(message); + if (tenant != null) { + params.tenant(tenant); + } + + List artifactTexts = new ArrayList<>(); + CountDownLatch done = new CountDownLatch(1); + BiConsumer consumer = (event, card) -> { + Task task = null; + if (event instanceof TaskEvent te) { + task = te.getTask(); + } else if (event instanceof TaskUpdateEvent tue) { + task = tue.getTask(); + } + if (task != null && task.artifacts() != null && !task.artifacts().isEmpty()) { + Artifact artifact = task.artifacts().get(0); + if (!artifact.parts().isEmpty() && artifact.parts().get(0) instanceof TextPart tp) { + artifactTexts.add(tp.text()); + } + } + if (task != null && task.status() != null && task.status().state().isFinal()) { + done.countDown(); + } + }; + + Client client = streaming ? getStreamingClient() : getNonStreamingClient(); + client.sendMessage(params.build(), List.of(consumer), null, null); + if (streaming) { + // The streaming client returns as soon as the SSE connection is initiated; wait for + // completion. The non-streaming client blocks in sendMessage() until the task is done, + // so no latch is needed there. + boolean completed = done.await(30, TimeUnit.SECONDS); + assertTrue(completed, "Timed out waiting for streaming task to complete"); + } + + assertFalse(artifactTexts.isEmpty(), "Expected at least one artifact text event"); + return artifactTexts.get(artifactTexts.size() - 1); + } + + private String getWellKnownCardName(String path) { + String body = RestAssured.given().when().get(wellKnownUrl(path)) + .then().statusCode(200).extract().asString(); + String name = JsonPath.from(body).getString("name"); + assertNotNull(name, "Expected a card name in: " + body); + return name; + } + + private String wellKnownUrl(String path) { + return "http://localhost:" + serverPort + path; + } + + protected Client getStreamingClient() { + if (streamingClient == null) { + streamingClient = buildClient(true); + } + return streamingClient; + } + + protected Client getNonStreamingClient() { + if (nonStreamingClient == null) { + nonStreamingClient = buildClient(false); + } + return nonStreamingClient; + } + + private Client buildClient(boolean streaming) { + AgentCard card = AgentCard.builder() + .name("multitenant-client-card") + .description("client-side card") + .version("1.0.0") + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .capabilities(AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) + .skills(List.of()) + .supportedInterfaces(List.of(new AgentInterface(getTransportProtocol(), getTransportUrl()))) + .build(); + ClientBuilder builder = Client.builder(card) + .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build()); + configureTransport(builder); + try { + return builder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java new file mode 100644 index 000000000..f4d57be44 --- /dev/null +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java @@ -0,0 +1,115 @@ +package org.a2aproject.sdk.extras.multitenancy.tests; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.Properties; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +import org.a2aproject.sdk.extras.multitenancy.Tenant; +import org.a2aproject.sdk.server.ExtendedAgentCard; +import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.spec.AgentCapabilities; +import org.a2aproject.sdk.spec.AgentCard; +import org.a2aproject.sdk.spec.AgentInterface; +import org.a2aproject.sdk.spec.TransportProtocol; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Single CDI producer for all six multi-tenant agent cards (default/acme/beta × public/extended). + * The transport interface stamped onto every card is chosen from the {@code preferred-transport} + * key in {@code /a2a-requesthandler-test.properties}, so one producer serves every transport module. + * + *

Router contract: per-tenant public cards carry {@code @Tenant} only (never {@code @PublicAgentCard}), + * per-tenant extended cards carry {@code @Tenant @ExtendedAgentCard}. + */ +@Singleton +public class MultiTenantAgentCardProducer { + + @ConfigProperty(name = "quarkus.http.port", defaultValue = "" + AbstractMultiTenantServerTest.TEST_PORT) + int serverPort; + + private final String preferredTransport = loadPreferredTransport(); + + @Produces + @Singleton + @PublicAgentCard + public AgentCard publicCard() { + return card("Default Agent"); + } + + @Produces + @Singleton + @ExtendedAgentCard + public AgentCard defaultExtendedCard() { + return card("default-extended"); + } + + @Produces + @Singleton + @Tenant("acme") + public AgentCard acmePublicCard() { + return card("Acme Agent"); + } + + @Produces + @Singleton + @Tenant("acme") + @ExtendedAgentCard + public AgentCard acmeExtendedCard() { + return card("acme-extended"); + } + + @Produces + @Singleton + @Tenant("beta") + public AgentCard betaPublicCard() { + return card("Beta Agent"); + } + + @Produces + @Singleton + @Tenant("beta") + @ExtendedAgentCard + public AgentCard betaExtendedCard() { + return card("beta-extended"); + } + + private AgentCard card(String name) { + String url = TransportProtocol.GRPC.asString().equals(preferredTransport) + ? "localhost:" + serverPort + : "http://localhost:" + serverPort; + List interfaces = List.of(new AgentInterface(preferredTransport, url)); + return AgentCard.builder() + .name(name) + .description(name) + .version("1.0.0") + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .capabilities(AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) + .skills(List.of()) + .supportedInterfaces(interfaces) + .build(); + } + + private static String loadPreferredTransport() { + URL url = MultiTenantAgentCardProducer.class.getResource("/a2a-requesthandler-test.properties"); + if (url == null) { + throw new IllegalStateException("Missing /a2a-requesthandler-test.properties on the test classpath"); + } + Properties properties = new Properties(); + try (InputStream in = url.openStream()) { + properties.load(in); + } catch (IOException e) { + throw new IllegalStateException("Failed to read /a2a-requesthandler-test.properties", e); + } + String preferredTransport = properties.getProperty("preferred-transport"); + if (preferredTransport == null || preferredTransport.isBlank()) { + throw new IllegalStateException("preferred-transport not set in /a2a-requesthandler-test.properties"); + } + return preferredTransport; + } +} diff --git a/tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentExecutorProducer.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java similarity index 73% rename from tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentExecutorProducer.java rename to extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java index 3872426b6..4e17bb76d 100644 --- a/tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentExecutorProducer.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java @@ -1,4 +1,4 @@ -package org.a2aproject.sdk.tests.multitenancy; +package org.a2aproject.sdk.extras.multitenancy.tests; import java.util.List; @@ -10,8 +10,8 @@ import org.a2aproject.sdk.spec.TextPart; /** - * Shared CDI producer for multi-tenant agent executors used across integration tests. - * Each executor echoes the tenant name as artifact text, enabling tenant routing verification. + * Shared CDI producer for multi-tenant agent executors used across multitenancy integration tests. + * Each executor echoes its tenant label as artifact text, enabling tenant-routing verification. */ @ApplicationScoped public class MultiTenantAgentExecutorProducer { @@ -19,21 +19,21 @@ public class MultiTenantAgentExecutorProducer { @Produces @ApplicationScoped public AgentExecutor defaultExecutor() { - return tenantEchoExecutor("default"); + return tenantEchoExecutor(Tenants.DEFAULT_LABEL); } @Produces @ApplicationScoped @Tenant("acme") public AgentExecutor acmeExecutor() { - return tenantEchoExecutor("acme"); + return tenantEchoExecutor(Tenants.ACME); } @Produces @ApplicationScoped @Tenant("beta") public AgentExecutor betaExecutor() { - return tenantEchoExecutor("beta"); + return tenantEchoExecutor(Tenants.BETA); } private static AgentExecutor tenantEchoExecutor(String label) { diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java new file mode 100644 index 000000000..b6af7d3f2 --- /dev/null +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java @@ -0,0 +1,13 @@ +package org.a2aproject.sdk.extras.multitenancy.tests; + +/** Shared tenant identifiers and expected executor labels for multitenancy tests. */ +public final class Tenants { + + public static final String ACME = "acme"; + public static final String BETA = "beta"; + public static final String UNKNOWN = "unknown-corp"; + public static final String DEFAULT_LABEL = "default"; + + private Tenants() { + } +} diff --git a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/TestAuthorizationController.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java similarity index 60% rename from tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/TestAuthorizationController.java rename to extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java index c7125d6ce..35bae7f4e 100644 --- a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/TestAuthorizationController.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java @@ -1,4 +1,4 @@ -package org.a2aproject.sdk.tests.multitenancy.grpc; +package org.a2aproject.sdk.extras.multitenancy.tests; import jakarta.annotation.Priority; import jakarta.enterprise.inject.Alternative; @@ -10,13 +10,14 @@ import io.quarkus.security.spi.runtime.AuthorizationController; /** - * Disables authorization for gRPC multitenancy tests. + * Disables authorization for multitenancy tests, across every transport. *

* The {@code @Authenticated} CDI interceptor checks {@link AuthorizationController#isAuthorizationEnabled()} - * before enforcing. When disabled, {@code @Authenticated} becomes a no-op, allowing - * tests to call gRPC endpoints without credentials. + * before enforcing. CDI resolves at most one {@code AuthorizationController} bean; declaring this + * {@code @Alternative} with a priority above the built-in controller makes CDI pick it instead, so + * when disabled, {@code @Authenticated} becomes a no-op and tests can call endpoints without credentials. *

- * The default is {@code false}. To enforce real authentication in a specific test profile, + * The default is {@code false}. To exercise real authentication in a specific test profile, * set {@code test.authorization.enabled=true}. */ @Alternative diff --git a/extras/multitenancy/tests/test-common/src/main/resources/META-INF/beans.xml b/extras/multitenancy/tests/test-common/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..548d44b72 --- /dev/null +++ b/extras/multitenancy/tests/test-common/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/pom.xml b/pom.xml index b480303f4..d2408b0b6 100644 --- a/pom.xml +++ b/pom.xml @@ -635,17 +635,20 @@ reference/multiversion-jsonrpc reference/multiversion-rest - - tests/multitenancy + + extras/multitenancy/tests/test-common - - tests/multitenancy/test-common + + extras/multitenancy/tests/rest - tests/multitenancy/jsonrpc + extras/multitenancy/tests/jsonrpc - - tests/multitenancy/grpc + + extras/multitenancy/tests/grpc + + + extras/multitenancy/tests/router-unit tests/multiversion/jsonrpc diff --git a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantAgentCardProducer.java b/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantAgentCardProducer.java deleted file mode 100644 index cf25c9789..000000000 --- a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantAgentCardProducer.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.a2aproject.sdk.tests.multitenancy.grpc; - -import java.util.List; - -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Singleton; - -import org.a2aproject.sdk.extras.multitenancy.Tenant; -import org.a2aproject.sdk.server.ExtendedAgentCard; -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.TransportProtocol; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -@Singleton -public class MultiTenantAgentCardProducer extends org.a2aproject.sdk.tests.multitenancy.MultiTenantAgentCardProducer { - - @ConfigProperty(name = "quarkus.http.port", defaultValue = "8081") - int serverPort; - - @Produces - @Singleton - @PublicAgentCard - public AgentCard publicCard() { - return card("Default Agent", grpcInterfaces()); - } - - @Produces - @Singleton - @ExtendedAgentCard - public AgentCard defaultExtendedCard() { - return card("Default Agent (extended)", grpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("acme") - public AgentCard acmePublicCard() { - return card("Acme Agent", grpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("acme") - @ExtendedAgentCard - public AgentCard acmeExtendedCard() { - return card("Acme Agent (extended)", grpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("beta") - public AgentCard betaPublicCard() { - return card("Beta Agent", grpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("beta") - @ExtendedAgentCard - public AgentCard betaExtendedCard() { - return card("Beta Agent (extended)", grpcInterfaces()); - } - - private List grpcInterfaces() { - return List.of(new AgentInterface(TransportProtocol.GRPC.asString(), "localhost:" + serverPort)); - } -} diff --git a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantGrpcTest.java b/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantGrpcTest.java deleted file mode 100644 index fb9a41fa7..000000000 --- a/tests/multitenancy/grpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/grpc/MultiTenantGrpcTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.a2aproject.sdk.tests.multitenancy.grpc; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; - -import jakarta.inject.Inject; - -import org.a2aproject.sdk.client.Client; -import org.a2aproject.sdk.client.ClientEvent; -import org.a2aproject.sdk.client.TaskEvent; -import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; -import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; -import org.a2aproject.sdk.spec.AgentCapabilities; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.Message; -import org.a2aproject.sdk.spec.MessageSendParams; -import org.a2aproject.sdk.spec.Task; -import org.a2aproject.sdk.spec.TextPart; -import org.a2aproject.sdk.spec.TransportProtocol; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.quarkus.security.spi.runtime.AuthorizationController; -import io.quarkus.test.junit.QuarkusTest; -import org.eclipse.microprofile.config.ConfigProvider; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -/** - * Integration tests for multi-tenant executor routing on a gRPC server. - * Verifies that gRPC SendMessage requests with a {@code tenant} field are routed - * to the correct {@code AgentExecutor} bean qualified with {@code @Tenant}. - */ -@QuarkusTest -public class MultiTenantGrpcTest { - - @Inject - AuthorizationController authorizationController; - - private static ManagedChannel channel; - private static String grpcTarget; - - @BeforeAll - public static void setupChannel() { - int port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); - grpcTarget = "localhost:" + port; - channel = ManagedChannelBuilder.forTarget(grpcTarget).usePlaintext().build(); - } - - @AfterAll - public static void closeChannel() { - channel.shutdownNow(); - try { - channel.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Test - public void authorizationControllerIsTestAlternative() { - assertInstanceOf(TestAuthorizationController.class, authorizationController, - "TestAuthorizationController CDI alternative must be active"); - assertFalse(authorizationController.isAuthorizationEnabled(), - "Authorization must be disabled for multitenancy tests"); - } - - @Test - public void knownTenantRoutesToAcmeExecutor() throws Exception { - assertEquals("acme", sendMessageAndGetArtifactText("acme")); - } - - @Test - public void secondTenantRoutesToBetaExecutor() throws Exception { - assertEquals("beta", sendMessageAndGetArtifactText("beta")); - } - - @Test - public void unknownTenantFallsBackToDefault() throws Exception { - assertEquals("default", sendMessageAndGetArtifactText("unknown-corp")); - } - - @Test - public void nullTenantUsesDefault() throws Exception { - assertEquals("default", sendMessageAndGetArtifactText(null)); - } - - private String sendMessageAndGetArtifactText(@Nullable String tenant) throws Exception { - AgentCard card = buildGrpcAgentCard(); - MessageSendParams params = buildParams(tenant); - - List tasks = new ArrayList<>(); - List> consumers = List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - tasks.add(te.getTask()); - } - }); - try (Client client = Client.builder(card) - .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder() - .channelFactory(target -> channel)) - .build()) { - client.sendMessage(params, consumers, null, null); - } - - assertEquals(1, tasks.size(), "Expected exactly one Task in the response"); - Task task = tasks.get(0); - assertNotNull(task.artifacts(), "Expected artifacts in task"); - assertFalse(task.artifacts().isEmpty(), "Expected at least one artifact"); - return ((TextPart) task.artifacts().get(0).parts().get(0)).text(); - } - - private static MessageSendParams buildParams(@Nullable String tenant) { - Message message = Message.builder() - .messageId(UUID.randomUUID().toString()) - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("hello"))) - .build(); - MessageSendParams.Builder builder = MessageSendParams.builder().message(message); - if (tenant != null) { - builder.tenant(tenant); - } - return builder.build(); - } - - private static AgentCard buildGrpcAgentCard() { - return AgentCard.builder() - .name("test-agent") - .description("test") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().streaming(false).build()) - .skills(List.of()) - .supportedInterfaces(List.of(new AgentInterface(TransportProtocol.GRPC.asString(), grpcTarget))) - .build(); - } -} diff --git a/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantAgentCardProducer.java b/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantAgentCardProducer.java deleted file mode 100644 index c475106af..000000000 --- a/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantAgentCardProducer.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.a2aproject.sdk.tests.multitenancy.jsonrpc; - -import java.util.List; - -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Singleton; - -import org.a2aproject.sdk.extras.multitenancy.Tenant; -import org.a2aproject.sdk.server.ExtendedAgentCard; -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.TransportProtocol; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -@Singleton -public class MultiTenantAgentCardProducer extends org.a2aproject.sdk.tests.multitenancy.MultiTenantAgentCardProducer { - - @ConfigProperty(name = "quarkus.http.port", defaultValue = "8081") - int serverPort; - - @Produces - @Singleton - @PublicAgentCard - public AgentCard publicCard() { - return card("Default Agent", jsonrpcInterfaces()); - } - - @Produces - @Singleton - @ExtendedAgentCard - public AgentCard defaultExtendedCard() { - return card("Default Agent (extended)", jsonrpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("acme") - public AgentCard acmePublicCard() { - return card("Acme Agent", jsonrpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("acme") - @ExtendedAgentCard - public AgentCard acmeExtendedCard() { - return card("Acme Agent (extended)", jsonrpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("beta") - public AgentCard betaPublicCard() { - return card("Beta Agent", jsonrpcInterfaces()); - } - - @Produces - @Singleton - @Tenant("beta") - @ExtendedAgentCard - public AgentCard betaExtendedCard() { - return card("Beta Agent (extended)", jsonrpcInterfaces()); - } - - private List jsonrpcInterfaces() { - return List.of(new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:" + serverPort)); - } -} diff --git a/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantJSONRPCPublicCardTest.java b/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantJSONRPCPublicCardTest.java deleted file mode 100644 index e795d4cf9..000000000 --- a/tests/multitenancy/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multitenancy/jsonrpc/MultiTenantJSONRPCPublicCardTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.a2aproject.sdk.tests.multitenancy.jsonrpc; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; -import org.junit.jupiter.api.Test; - -/** - * Verifies that the JSON-RPC transport serves the public agent card endpoints - * (/.well-known/agent-card.json and /.well-known/{tenant}/agent-card.json) - * when the REST transport is NOT on the classpath. - */ -@QuarkusTest -public class MultiTenantJSONRPCPublicCardTest { - - @Test - public void defaultPublicCardIsServed() { - String response = RestAssured.given() - .when().get("/.well-known/agent-card.json") - .then().statusCode(200) - .extract().asString(); - assertEquals("Default Agent", JsonPath.from(response).getString("name")); - } - - @Test - public void acmeTenantPublicCardIsServed() { - String response = RestAssured.given() - .when().get("/.well-known/acme/agent-card.json") - .then().statusCode(200) - .extract().asString(); - assertEquals("Acme Agent", JsonPath.from(response).getString("name")); - } - - @Test - public void betaTenantPublicCardIsServed() { - String response = RestAssured.given() - .when().get("/.well-known/beta/agent-card.json") - .then().statusCode(200) - .extract().asString(); - assertEquals("Beta Agent", JsonPath.from(response).getString("name")); - } - - @Test - public void unknownTenantReturns404() { - RestAssured.given() - .when().get("/.well-known/unknown-corp/agent-card.json") - .then().statusCode(404); - } -} diff --git a/tests/multitenancy/test-common/pom.xml b/tests/multitenancy/test-common/pom.xml deleted file mode 100644 index cbded74bc..000000000 --- a/tests/multitenancy/test-common/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - 4.0.0 - - - org.a2aproject.sdk - a2a-java-sdk-parent - 1.3.2.Final-SNAPSHOT - ../../../pom.xml - - - a2a-java-sdk-tests-multitenancy-test-common - - jar - - Java A2A SDK Tests Multitenancy Test Common - Shared test infrastructure (CDI producers) for multitenancy integration tests - - - true - - - - - ${project.groupId} - a2a-java-extras-multitenancy - - - ${project.groupId} - a2a-java-sdk-server-common - - - ${project.groupId} - a2a-java-sdk-spec - - - jakarta.enterprise - jakarta.enterprise.cdi-api - - - jakarta.inject - jakarta.inject-api - - - diff --git a/tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentCardProducer.java b/tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentCardProducer.java deleted file mode 100644 index c921b161a..000000000 --- a/tests/multitenancy/test-common/src/main/java/org/a2aproject/sdk/tests/multitenancy/MultiTenantAgentCardProducer.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.a2aproject.sdk.tests.multitenancy; - -import java.util.List; - -import jakarta.enterprise.inject.Vetoed; - -import org.a2aproject.sdk.spec.AgentCapabilities; -import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; - -/** - * Base helper for building multi-tenant agent cards in integration tests. - * Not a CDI bean — consuming modules define their own transport-specific card producer. - * Each module's card producer must declare only the interfaces whose transports are - * deployed in that module (JSONRPC/HTTP for jsonrpc tests, GRPC for grpc tests). - */ -@Vetoed -public class MultiTenantAgentCardProducer { - - protected static AgentCard card(String name, List supportedInterfaces) { - return AgentCard.builder() - .name(name) - .description(name) - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) - .skills(List.of()) - .supportedInterfaces(supportedInterfaces) - .build(); - } -}