diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index e67542a..602736a 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -18,6 +18,7 @@ Test Library for the Bridge Service true + true diff --git a/bridgeService-test-injection/pom.xml b/bridgeService-test-injection/pom.xml new file mode 100644 index 0000000..f28cd47 --- /dev/null +++ b/bridgeService-test-injection/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + com.adobe.campaign.tests.bridge + bridgeService-test-injection + ${project.groupId}:${project.artifactId} + Injection model E2E test harness for the Bridge Service + + true + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.5 + + + src/test/resources/testng.xml + + + + + + + + + com.github.therapi + therapi-runtime-javadoc-scribe + 0.15.0 + provided + + + + com.adobe.campaign.tests.bridge.testdata + bridgeService-data + ${project.parent.version} + + + + com.adobe.campaign.tests.bridge.service + integroBridgeService + ${project.parent.version} + test + + + org.testng + testng + 7.12.0 + test + + + io.rest-assured + rest-assured + 5.5.7 + test + + + io.rest-assured + json-path + 5.5.7 + test + + + + com.adobe.campaign.tests.bridge + parent + 3.11.4-SNAPSHOT + + diff --git a/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/caller/DepCaller.java b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/caller/DepCaller.java new file mode 100644 index 0000000..d509241 --- /dev/null +++ b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/caller/DepCaller.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Adobe + * All Rights Reserved. + * + * NOTICE: Adobe permits you to use, modify, and distribute this file in + * accordance with the terms of the Adobe license agreement accompanying + * it. + */ +package com.adobe.campaign.tests.bridge.dependency.caller; + +import com.adobe.campaign.tests.bridge.dependency.factory.DepFactory; +import com.adobe.campaign.tests.bridge.dependency.model.DepResult; + +/** + * Simulates project code that calls a dependency library in a static initializer. + * When this class's package and DepResult's package are in STATIC_INTEGRITY_PACKAGES + * but DepFactory's package is not, both the IBS classloader and the parent classloader + * end up loading DepResult, which causes a LinkageError. + */ +public class DepCaller { + + private final static DepResult instantiatedStaticConstant = DepFactory.makeDepResult("initial"); + + /** + * Returns a fixed string, serving as the method to invoke via the /call endpoint in tests. + * + * @return a confirmation string + */ + public static String doSomething() { + return instantiatedStaticConstant.getValue(); + } +} diff --git a/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/factory/DepFactory.java b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/factory/DepFactory.java new file mode 100644 index 0000000..315da61 --- /dev/null +++ b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/factory/DepFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Adobe + * All Rights Reserved. + * + * NOTICE: Adobe permits you to use, modify, and distribute this file in + * accordance with the terms of the Adobe license agreement accompanying + * it. + */ +package com.adobe.campaign.tests.bridge.dependency.factory; + +import com.adobe.campaign.tests.bridge.dependency.model.DepResult; +import com.adobe.campaign.tests.bridge.testdata.issue34.pckg1.MiddleMan; + +/** + * Simulated dependency library factory. + * Lives in a separate package from DepResult to enable the split-package classloader conflict scenario: + * when DepResult's package is in STATIC_INTEGRITY_PACKAGES but this factory's package is not, + * parent classloader loads DepResult a second time (as this method's return type), causing a LinkageError. + */ +public class DepFactory { + + /** + * Creates a DepResult instance. + * + * @param in_value the string value to embed in the result + * @return a new DepResult + */ + public static DepResult makeDepResult(String in_value) { + return new DepResult(in_value); + } + + /** + * Creates a MiddleMan instance from the project (bridgeService-data) test data. + * Used to demonstrate that a dependency library can return project types when + * both packages are included in STATIC_INTEGRITY_PACKAGES. + * + * @return a new MiddleMan instance + */ + public static MiddleMan makeMiddleMan() { + return new MiddleMan(); + } +} diff --git a/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/model/DepResult.java b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/model/DepResult.java new file mode 100644 index 0000000..82303a3 --- /dev/null +++ b/bridgeService-test-injection/src/main/java/com/adobe/campaign/tests/bridge/dependency/model/DepResult.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Adobe + * All Rights Reserved. + * + * NOTICE: Adobe permits you to use, modify, and distribute this file in + * accordance with the terms of the Adobe license agreement accompanying + * it. + */ +package com.adobe.campaign.tests.bridge.dependency.model; + +/** + * A result type produced by the simulated dependency library. + * Used by the injection model tests to trigger and verify classloader isolation behaviour. + */ +public class DepResult { + + private String value; + + public DepResult(String in_value) { + this.value = in_value; + } + + public String getValue() { + return value; + } +} diff --git a/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelE2ETests.java b/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelE2ETests.java new file mode 100644 index 0000000..992045c --- /dev/null +++ b/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelE2ETests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2022 Adobe + * All Rights Reserved. + * + * NOTICE: Adobe permits you to use, modify, and distribute this file in + * accordance with the terms of the Adobe license agreement accompanying + * it. + */ +package com.adobe.campaign.tests.bridge.dependency; + +import com.adobe.campaign.tests.bridge.service.CallContent; +import com.adobe.campaign.tests.bridge.service.ConfigValueHandlerIBS; +import com.adobe.campaign.tests.bridge.service.IntegroAPI; +import com.adobe.campaign.tests.bridge.service.JavaCalls; +import com.adobe.campaign.tests.bridge.service.exceptions.IBSConfigurationException; +import com.adobe.campaign.tests.bridge.dependency.caller.DepCaller; +import com.adobe.campaign.tests.bridge.dependency.factory.DepFactory; +import io.javalin.Javalin; +import org.hamcrest.Matchers; +import org.testng.annotations.AfterGroups; +import org.testng.annotations.BeforeGroups; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static io.restassured.RestAssured.given; + +/** + * E2E tests for the injection model (IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES). + * Demonstrates that dependency library packages can and must be listed alongside + * project packages to avoid LinkageError when the library's factory returns a type + * that is also loaded by the IBS classloader. + */ +public class InjectionModelE2ETests { + + private static final String END_POINT_URL = "http://localhost:8080/"; + + private static final String CALLER_PACKAGE = + "com.adobe.campaign.tests.bridge.dependency.caller."; + private static final String MODEL_PACKAGE = + "com.adobe.campaign.tests.bridge.dependency.model."; + private static final String FACTORY_PACKAGE = + "com.adobe.campaign.tests.bridge.dependency.factory."; + + private Javalin app; + + @BeforeGroups(groups = "E2E") + public void startUpService() { + app = IntegroAPI.startServices(8080); + } + + @BeforeMethod + public void cleanCache() { + ConfigValueHandlerIBS.resetAllValues(); + } + + @AfterGroups(groups = "E2E", alwaysRun = true) + public void tearDown() { + ConfigValueHandlerIBS.resetAllValues(); + app.stop(); + } + + /** + * Negative test: when DepResult's package IS in STATIC_INTEGRITY_PACKAGES but + * DepFactory's package is NOT, the IBS classloader and the parent classloader both + * load DepResult independently, producing a LinkageError. + */ + @Test(groups = "E2E") + public void testInjectionConflict_missingFactoryPackage() { + ConfigValueHandlerIBS.INTEGRITY_PACKAGE_INJECTION_MODE.activate("manual"); + ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate( + MODEL_PACKAGE + "," + CALLER_PACKAGE); + + JavaCalls l_myJavaCalls = new JavaCalls(); + CallContent l_cc = new CallContent(); + l_cc.setClassName(DepCaller.class.getTypeName()); + l_cc.setMethodName("doSomething"); + l_myJavaCalls.getCallContent().put("call1", l_cc); + + given().body(l_myJavaCalls).post(END_POINT_URL + "call").then() + .assertThat().statusCode(500) + .body("title", Matchers.equalTo( + "The provided class and method for setting environment variables is not valid.")) + .body("code", Matchers.equalTo(500)) + .body("detail", Matchers.startsWith("Linkage Error detected")) + .body("bridgeServiceException", + Matchers.equalTo(IBSConfigurationException.class.getTypeName())) + .body("originalException", + Matchers.equalTo(LinkageError.class.getTypeName())); + } + + /** + * Positive test: adding DepFactory's package to STATIC_INTEGRITY_PACKAGES ensures all + * three packages are loaded by the same IBS classloader, eliminating the type mismatch. + */ + @Test(groups = "E2E") + public void testInjectionConflict_allPackagesPresent() { + ConfigValueHandlerIBS.INTEGRITY_PACKAGE_INJECTION_MODE.activate("manual"); + ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate( + MODEL_PACKAGE + "," + CALLER_PACKAGE + "," + FACTORY_PACKAGE); + + JavaCalls l_myJavaCalls = new JavaCalls(); + CallContent l_cc = new CallContent(); + l_cc.setClassName(DepCaller.class.getTypeName()); + l_cc.setMethodName("doSomething"); + l_myJavaCalls.getCallContent().put("call1", l_cc); + + given().body(l_myJavaCalls).post(END_POINT_URL + "call").then() + .assertThat().statusCode(200) + .body("returnValues.call1", Matchers.equalTo("initial")); + } + + /** + * Cross-module type test: DepFactory.makeMiddleMan() returns a MiddleMan from bridgeService-data. + * When the factory package is in STATIC_INTEGRITY_PACKAGES, the call succeeds and the + * MiddleMan object is serialised correctly by the BridgeService response layer. + */ + @Test(groups = "E2E") + public void testDepFactoryReturnsBridgeDataType() { + ConfigValueHandlerIBS.INTEGRITY_PACKAGE_INJECTION_MODE.activate("manual"); + ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate(FACTORY_PACKAGE); + + JavaCalls l_myJavaCalls = new JavaCalls(); + CallContent l_cc = new CallContent(); + l_cc.setClassName(DepFactory.class.getTypeName()); + l_cc.setMethodName("makeMiddleMan"); + l_myJavaCalls.getCallContent().put("call1", l_cc); + + given().body(l_myJavaCalls).post(END_POINT_URL + "call").then() + .assertThat().statusCode(200); + } +} diff --git a/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelMCPTests.java b/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelMCPTests.java new file mode 100644 index 0000000..91de69e --- /dev/null +++ b/bridgeService-test-injection/src/test/java/com/adobe/campaign/tests/bridge/dependency/InjectionModelMCPTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2022 Adobe + * All Rights Reserved. + * + * NOTICE: Adobe permits you to use, modify, and distribute this file in + * accordance with the terms of the Adobe license agreement accompanying + * it. + */ +package com.adobe.campaign.tests.bridge.dependency; + +import com.adobe.campaign.tests.bridge.dependency.factory.DepFactory; +import com.adobe.campaign.tests.bridge.service.CallContent; +import com.adobe.campaign.tests.bridge.service.ConfigValueHandlerIBS; +import com.adobe.campaign.tests.bridge.service.IntegroAPI; +import com.adobe.campaign.tests.bridge.service.JavaCalls; +import com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods; +import io.javalin.Javalin; +import org.hamcrest.Matchers; +import org.testng.annotations.AfterGroups; +import org.testng.annotations.BeforeGroups; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static io.restassured.RestAssured.given; + +/** + * Verifies that when BridgeService is started from bridgeService-test-injection's classpath, + * methods from both this module and bridgeService-data are reachable via REST (/call) + * and via MCP (tools/list). + * + * The server is started once with MCP enabled and both modules' packages in + * STATIC_INTEGRITY_PACKAGES so that tool discovery and class loading cover both. + */ +public class InjectionModelMCPTests { + + private static final String REST_ENDPOINT = "http://localhost:8080/"; + private static final String MCP_ENDPOINT = "http://localhost:8080/mcp"; + private static final String CONTENT_TYPE_JSON = "application/json"; + + /** Package from bridgeService-data whose Javadoc is embedded at compile time. */ + private static final String BRIDGE_DATA_PACKAGE = + "com.adobe.campaign.tests.bridge.testdata.one"; + + /** Package from this module whose Javadoc is embedded via therapi at compile time. */ + private static final String FACTORY_PACKAGE = + "com.adobe.campaign.tests.bridge.dependency.factory."; + + private static final String BOTH_PACKAGES = BRIDGE_DATA_PACKAGE + "," + FACTORY_PACKAGE; + + private Javalin app; + + @BeforeGroups(groups = "INJECTION_MCP") + public void startMCPService() { + ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate(BOTH_PACKAGES); + ConfigValueHandlerIBS.MCP_ENABLED.activate("true"); + app = IntegroAPI.startServices(8080); + } + + @BeforeMethod + public void resetConfig() { + ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate(BOTH_PACKAGES); + } + + @AfterGroups(groups = "INJECTION_MCP", alwaysRun = true) + public void tearDown() { + ConfigValueHandlerIBS.resetAllValues(); + app.stop(); + } + + // ---- REST access ---- + + /** + * Calls SimpleStaticMethods.methodReturningString() from bridgeService-data via REST. + * Proves that dependency JAR methods are callable through the /call endpoint when + * their package is listed in STATIC_INTEGRITY_PACKAGES. + */ + @Test(groups = "INJECTION_MCP") + public void testRESTCall_bridgeDataMethod() { + JavaCalls l_calls = new JavaCalls(); + CallContent l_cc = new CallContent(); + l_cc.setClassName(SimpleStaticMethods.class.getTypeName()); + l_cc.setMethodName("methodReturningString"); + l_calls.getCallContent().put("call1", l_cc); + + given().body(l_calls).post(REST_ENDPOINT + "call").then() + .assertThat().statusCode(200) + .body("returnValues.call1", Matchers.notNullValue()); + } + + /** + * Calls DepFactory.makeMiddleMan() from this module (bridgeService-test-injection) via REST. + * Proves that methods defined in the module that starts the service are also callable + * through the /call endpoint. + */ + @Test(groups = "INJECTION_MCP") + public void testRESTCall_testInjectionMethod() { + JavaCalls l_calls = new JavaCalls(); + CallContent l_cc = new CallContent(); + l_cc.setClassName(DepFactory.class.getTypeName()); + l_cc.setMethodName("makeMiddleMan"); + l_calls.getCallContent().put("call1", l_cc); + + given().body(l_calls).post(REST_ENDPOINT + "call").then() + .assertThat().statusCode(200); + } + + // ---- MCP tools/list access ---- + + /** + * Verifies tools/list returns SimpleStaticMethods_methodReturningString from bridgeService-data. + * Proves that MCP tool discovery scans dependency JARs on the classpath when their package + * is listed in STATIC_INTEGRITY_PACKAGES at server startup. + */ + @Test(groups = "INJECTION_MCP") + public void testMCPToolDiscovery_findsBridgeDataMethod() { + given().contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}") + .post(MCP_ENDPOINT) + .then() + .assertThat().statusCode(200) + .body("result.tools.name", + Matchers.hasItem("SimpleStaticMethods_methodReturningString")); + } + + /** + * Verifies tools/list returns DepFactory_makeMiddleMan from this module. + * Proves that MCP tool discovery also covers classes compiled into the module that + * starts the service — not just classes from external dependency JARs. + */ + @Test(groups = "INJECTION_MCP") + public void testMCPToolDiscovery_findsTestInjectionMethod() { + given().contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}") + .post(MCP_ENDPOINT) + .then() + .assertThat().statusCode(200) + .body("result.tools.name", Matchers.hasItem("DepFactory_makeMiddleMan")); + } +} diff --git a/bridgeService-test-injection/src/test/resources/log4j2.xml b/bridgeService-test-injection/src/test/resources/log4j2.xml new file mode 100644 index 0000000..fbc4540 --- /dev/null +++ b/bridgeService-test-injection/src/test/resources/log4j2.xml @@ -0,0 +1,36 @@ + + + + + + + + %-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n + + + + + + + + + + + + + + + + + + diff --git a/bridgeService-test-injection/src/test/resources/testng.xml b/bridgeService-test-injection/src/test/resources/testng.xml new file mode 100644 index 0000000..b31e66d --- /dev/null +++ b/bridgeService-test-injection/src/test/resources/testng.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index bd58a41..051987d 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ integroBridgeService bridgeService-data + bridgeService-test-injection 11