diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 1458b89d2f7..99655d4d9ee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -32,6 +32,7 @@ body: - K3S - K6 - Kafka + - Liberty - LocalStack - MariaDB - Milvus diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 2baf2be99eb..8d95de837b7 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -32,6 +32,7 @@ body: - K3S - K6 - Kafka + - Liberty - LocalStack - MariaDB - Milvus diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index fb5013a41d3..b1d918fa1f9 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -32,6 +32,7 @@ body: - K3S - K6 - Kafka + - Liberty - LocalStack - MariaDB - Milvus diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0bb82dce9f4..a963fd94f21 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,6 +34,12 @@ updates: # Explicit entry for each module - package-ecosystem: "gradle" + directory: "/modules/application-server-commons" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/azure" directory: "/modules/activemq" schedule: interval: "monthly" @@ -161,6 +167,11 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/liberty" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/localstack" schedule: diff --git a/docs/modules/liberty.md b/docs/modules/liberty.md new file mode 100644 index 00000000000..a25b54726f8 --- /dev/null +++ b/docs/modules/liberty.md @@ -0,0 +1,56 @@ +# Liberty Containers + +Testcontainers can be used to automatically instantiate and manage [Open Liberty](https://openliberty.io/) and [WebSphere Liberty](https://www.ibm.com/products/websphere-liberty/) containers. +More precisely Testcontainers uses the official Docker images for [Open Liberty](https://hub.docker.com/_/open-liberty) or [WebSphere Liberty](https://hub.docker.com/_/websphere-liberty) + +## Benefits + +* Easier integration testing for application developers. +* Easier functional testing for platform development. + +## Example + +Create a `LibertyContainer` to use it in your tests: + +[Creating a LibertyContainer](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:constructorWithVersion + + +Now you can perform integration testing, in this example we are using [RestAssured](https://rest-assured.io/) to query a RESTful web service running in Liberty. + + +[RESTful Test](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:testRestEndpoint + + +## Multi-container usage + +If your Liberty server needs to connect to a data provider, message provider, +or other service that can also be run as a container you can connect them using a network: + +* Run your other container on the same network as Liberty container, e.g.: + +[Network](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:constructorMockDatabase + +* Use network aliases and unmapped ports to configure an environment variable that can be access from your Application server. + +[Configure Liberty](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:configureLiberty + + +You will need to explicitly create a network and set it on the Liberty container as well as on your other containers that Liberty communicates with. + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" +```groovy +testImplementation "org.testcontainers:liberty:{{latest_version}}" +``` +=== "Maven" +```xml + +org.testcontainers +liberty +{{latest_version}} +test + +``` diff --git a/modules/application-server-commons/build.gradle b/modules/application-server-commons/build.gradle new file mode 100644 index 00000000000..58404e95279 --- /dev/null +++ b/modules/application-server-commons/build.gradle @@ -0,0 +1,9 @@ +description = "Testcontainers :: Application Server Common" + +dependencies { + api project(':testcontainers') + + compileOnly 'org.jetbrains:annotations:24.0.1' + implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6' + testImplementation 'org.assertj:assertj-core:3.24.2' +} diff --git a/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java new file mode 100644 index 00000000000..0190a4beb94 --- /dev/null +++ b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java @@ -0,0 +1,371 @@ +package org.testcontainers.applicationserver; + +import lombok.NonNull; +import org.apache.commons.lang3.SystemUtils; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.Base58; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a JavaEE, JakartaEE, or Microprofile application platform running inside a Docker container + */ +public abstract class ApplicationServerContainer extends GenericContainer { + + // List of archive(s) to install + List archives = new ArrayList<>(); + + // Where to save runtime archives + private static final String TESTCONTAINERS_TMP_DIR_PREFIX = ".testcontainers-archive-"; + + private static final String OS_MAC_TMP_DIR = "/tmp"; + + private static Path tempDirectory; + + // How to query an application + private Integer httpPort; + + private String appContextRoot; + + // How to query application for readiness + private Integer readinessPort; + + private String readinessPath; + + // How long to wait for readiness + @NonNull + private Duration httpWaitTimeout = Duration.ofSeconds(60); + + // Expected path for Microprofile platforms to query for readiness + static final String MP_HEALTH_READINESS_PATH = "/health/ready"; + + //Constructors + + public ApplicationServerContainer(@NonNull final Future image) { + super(image); + } + + public ApplicationServerContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + } + + //Overrides + + @Override + protected void configure() { + super.configure(); + + // Setup default wait strategy + waitingFor( + Wait + .forHttp(readinessPath != null ? readinessPath : appContextRoot) + .forPort(readinessPort != null ? readinessPort : httpPort) + .withStartupTimeout(httpWaitTimeout) + ); + + // Copy applications + for (MountableFile archive : archives) { + withCopyFileToContainer(archive, getApplicationInstallDirectory() + extractApplicationName(archive)); + } + } + + @Override + protected void containerIsCreated(String containerId) { + if (Objects.isNull(tempDirectory)) { + return; + } + + try { + //Delete files in temp directory + for (String file : tempDirectory.toFile().list()) { + Files.deleteIfExists(Paths.get(tempDirectory.toString(), file)); + } + //Delete temp directory + Files.deleteIfExists(tempDirectory); + } catch (IOException e) { + logger().info("Unable to delete temporary directory " + tempDirectory.toString(), e); + } + } + + @Override + public void setExposedPorts(List exposedPorts) { + if (Objects.isNull(this.httpPort)) { + super.setExposedPorts(exposedPorts); + return; + } + + super.setExposedPorts(appendPort(exposedPorts, this.httpPort)); + } + + //Configuration + + /** + * One or more archives to be deployed to the application platform. + * + * Calling this method more than once will append new archives to a list to be deployed. + * + * @param archives - An archive created using shrinkwrap and test runtime + * @return self + */ + public ApplicationServerContainer withArchives(@NonNull Archive... archives) { + Stream + .of(archives) + .forEach(archive -> { + String name = archive.getName(); + Path target = Paths.get(createTempDirectory().toString(), name); + archive.as(ZipExporter.class).exportTo(target.toFile(), true); + this.archives.add(MountableFile.forHostPath(target)); + }); + + return this; + } + + /** + * One or more archives to be deployed to the application platform. + * + * Calling this method more than once will append new archives to a list to be deployed. + * + * @param archives - A MountableFile which represents an archive that was created prior to test runtime. + * @return self + */ + public ApplicationServerContainer withArchives(@NonNull MountableFile... archives) { + this.archives.addAll(Arrays.asList(archives)); + return this; + } + + /** + * This will set the port used to construct the base application URL, as well as + * the port used to determine container readiness in the case of Microprofile platforms. + * + * Calling this method more than once will replace the current httpPort if it was already set. + * + * @param httpPort - The HTTP port used by the Application platform + * @return self + */ + public ApplicationServerContainer withHttpPort(int httpPort) { + if (Objects.nonNull(this.httpPort) && this.httpPort == this.readinessPort) { + int oldPort = this.httpPort; + this.readinessPort = this.httpPort = httpPort; + super.setExposedPorts(replacePort(getExposedPorts(), oldPort, this.httpPort)); + } else { + this.httpPort = httpPort; + super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort)); + } + + return this; + } + + /** + * This will set the path used to construct the base application URL. + * + * Calling this method more than once will replace the current appContextRoot if it was already set. + * + * @param appContextRoot - The application path + * @return self + */ + public ApplicationServerContainer withAppContextRoot(@NonNull String appContextRoot) { + if (Objects.nonNull(this.appContextRoot) && this.appContextRoot == this.readinessPath) { + this.readinessPath = this.appContextRoot = normalizePath(appContextRoot); + } else { + this.appContextRoot = normalizePath(appContextRoot); + } + + return this; + } + + /** + * By default, the ApplicationServerContainer will be configured with a HttpWaitStrategy and wait + * for a successful response from the {@link ApplicationServerContainer#getApplicationURL()}. + * + * This method will overwrite the path used by the HttpWaitStrategy but will keep the httpPort. + * + * Calling this method more than once will replace the current readinessPath if it was already set. + * + * @param readinessPath - The path to be polled for readiness. + * @return self + */ + public ApplicationServerContainer withReadiness(String readinessPath) { + return withReadiness(readinessPath, this.httpPort); + } + + /** + * By default, the ApplicationServerContainer will be configured with a HttpWaitStrategy and wait + * for a successful response from the {@link ApplicationServerContainer#getApplicationURL()}. + * + * This method will overwrite the path used by the HttpWaitStrategy. + * This method will overwrite the port used by the HttpWaitStrategy. + * + * Calling this method more than once will replace the current readinessPath and readinessPort if any were already set. + * + * @param readinessPath - The path to be polled for readiness. + * @param readinessPort - The port that should be used for the readiness check. + * @return self + */ + public ApplicationServerContainer withReadiness(@NonNull String readinessPath, @NonNull Integer readinessPort) { + this.readinessPath = normalizePath(readinessPath); + this.readinessPort = readinessPort; + return this; + } + + /** + * Set the timeout configured on the HttpWaitStrategy. + * + * Calling this method more than once will replace the current httpWaitTimeout if it was already set. + * Default value is 60 seconds + * + * @param httpWaitTimeout - The duration of time to wait for a successful http response + * @return self + */ + public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) { + this.httpWaitTimeout = httpWaitTimeout; + return this; + } + + //Getters + + /** + * The URL used to determine container readiness. + * If a readiness port and path were configured, those will be used to construct this URL. + * Otherwise, the readiness path will default to the httpPort and application context root. + * + * @return - The readiness URL + */ + public String getReadinessURL() { + if (Objects.isNull(this.readinessPath) || Objects.isNull(this.readinessPort)) { + return getApplicationURL(); + } + + return "http://" + getHost() + ':' + getMappedPort(this.readinessPort) + this.readinessPath; + } + + /** + * The URL where the application is running. + * The application URL is a concatenation of the baseURL {@link ApplicationServerContainer#getBaseURL()} with the appContextRoot. + * This is the URL that the test client should use to connect to an application running on the application platform. + * + * @return - The application URL + */ + public String getApplicationURL() { + Objects.requireNonNull(this.appContextRoot); + return getBaseURL() + appContextRoot; + } + + /** + * The base URL for the application platform. + * This is the URL that the test client should use to connect to the application platform. + * + * @return - The base URL + */ + public String getBaseURL() { + Objects.requireNonNull(this.httpPort); + return "http://" + getHost() + ':' + getMappedPort(this.httpPort); + } + + // Abstract + + /** + * Each implementation will need to provide an install directory + * where their platform expects applications to be copied into. + * + * @return - the application install directory + */ + protected abstract String getApplicationInstallDirectory(); + + // Helpers + + /** + * Create a temporary directory where runtime archives can be exported and copied to the application container. + * + * @return - The temporary directory path + */ + protected static Path createTempDirectory() { + if (Objects.nonNull(tempDirectory)) { + return tempDirectory; + } + + try { + if (SystemUtils.IS_OS_MAC) { + tempDirectory = Files.createTempDirectory(Paths.get(OS_MAC_TMP_DIR), TESTCONTAINERS_TMP_DIR_PREFIX); + } else { + tempDirectory = Files.createTempDirectory(TESTCONTAINERS_TMP_DIR_PREFIX); + } + } catch (IOException e) { + tempDirectory = new File(TESTCONTAINERS_TMP_DIR_PREFIX + Base58.randomString(5)).toPath(); + } + + return tempDirectory; + } + + private static String extractApplicationName(MountableFile file) throws IllegalArgumentException { + String path = file.getFilesystemPath(); + //TODO would any application servers support .zip? + if (path.matches(".*\\.jar|.*\\.war|.*\\.ear|.*\\.rar")) { + return path.substring(path.lastIndexOf("/")); + } + throw new IllegalArgumentException("File did not contain an application archive"); + } + + /** + * Appends a port to a list of ports at position 0 and ensures port list does + * not contain duplicates to ensure this order is maintained. + * + * @param portList - List of existing ports + * @param appendingPort - The port to append + * @return - resulting list of ports + */ + protected static List appendPort(List portList, int appendingPort) { + portList = new ArrayList<>(portList); //Ensure not immutable + portList.add(0, appendingPort); + return portList.stream().distinct().collect(Collectors.toList()); + } + + /** + * Replaces a port in a list of ports putting the new port at position 0 and ensures port + * list does not contain any duplicates values to ensure this order is maintained. + * + * @param portList - List of existing ports + * @param oldPort - The port to remove + * @param newPort - The port to add + * @return - resulting list of ports + */ + protected static List replacePort(List portList, Integer oldPort, Integer newPort) { + portList = new ArrayList<>(portList); //Ensure not immutable + portList.remove(oldPort); + portList.add(0, newPort); + return portList.stream().distinct().collect(Collectors.toList()); + } + + /** + * Normalizes the path provided by developer input. + * - Ensures paths start with '/' + * - Ensures paths are separated with exactly one '/' + * - Ensures paths do not end with a '/' + * + * @param paths the list of paths to be normalized + * @return The normalized path + */ + protected static String normalizePath(String... paths) { + return Stream + .of(paths) + .flatMap(path -> Stream.of(path.split("/"))) + .filter(part -> !part.isEmpty()) + .collect(Collectors.joining("/", "/", "")); + } +} diff --git a/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java new file mode 100644 index 00000000000..ec3894a244b --- /dev/null +++ b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java @@ -0,0 +1,87 @@ +package org.testcontainers.applicationserver; + +import lombok.NonNull; +import org.junit.Test; +import org.testcontainers.utility.DockerImageName; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Future; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ApplicationServerContainerTest { + + private static ApplicationServerContainer testContainer; + + @Test + public void testNormalizePath() { + String expected, actual; + + expected = "/path/to/application"; + actual = ApplicationServerContainer.normalizePath("path", "to", "application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationServerContainer.normalizePath("path/to", "application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationServerContainer.normalizePath("path/to/application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationServerContainer.normalizePath("path/", "to/", "application/"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationServerContainer.normalizePath("path/", "/to/", "/application/"); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void httpPortMapping() { + List expected, actual; + + expected = Arrays.asList(8080, 9080, 9443); + + // Test expose ports, then add httpPort + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer.withExposedPorts(9080, 9443); + testContainer.withHttpPort(8080); + + actual = testContainer.getExposedPorts(); + assertThat(actual).containsExactlyElementsOf(expected); + + // Test httpPort then expose ports + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer.withHttpPort(8080); + testContainer.withExposedPorts(9080, 9443); + + actual = testContainer.getExposedPorts(); + assertThat(actual).containsExactlyElementsOf(expected); + + //Test httpPort then set exposed ports + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer.withHttpPort(8080); + testContainer.setExposedPorts(Arrays.asList(9080, 9443)); + + actual = testContainer.getExposedPorts(); + assertThat(actual).containsExactlyElementsOf(expected); + } + + static class ApplicationServerContainerStub extends ApplicationServerContainer { + + public ApplicationServerContainerStub(@NonNull Future image) { + super(image); + } + + public ApplicationServerContainerStub(DockerImageName dockerImageName) { + super(dockerImageName); + } + + @Override + protected String getApplicationInstallDirectory() { + return "null"; + } + } +} diff --git a/modules/application-server-commons/src/test/resources/logback-test.xml b/modules/application-server-commons/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/application-server-commons/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/liberty/build.gradle b/modules/liberty/build.gradle new file mode 100644 index 00000000000..a797a1af727 --- /dev/null +++ b/modules/liberty/build.gradle @@ -0,0 +1,14 @@ +description = "Testcontainers :: Application Server :: Liberty" + +dependencies { + api project(':application-server-commons') + + compileOnly 'org.jetbrains:annotations:24.0.1' + + implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6' + + testImplementation project(':mockserver') + testImplementation 'io.rest-assured:rest-assured:5.3.0' + testImplementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0' + testImplementation 'org.assertj:assertj-core:3.24.2' +} diff --git a/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java new file mode 100644 index 00000000000..6d84b0a230b --- /dev/null +++ b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java @@ -0,0 +1,128 @@ +package org.testcontainers.liberty; + +import lombok.NonNull; +import org.testcontainers.applicationserver.ApplicationServerContainer; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Represents an Open Liberty or WebSphere Liberty container + */ +public class LibertyServerContainer extends ApplicationServerContainer { + + // About the image + static final String IMAGE = "open-liberty"; + + static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); + + // Container defaults + static final int DEFAULT_HTTP_PORT = 9080; + + static final int DEFAULT_HTTPS_PORT = 9443; + + static final Duration DEFAULT_WAIT_TIMEOUT = Duration.ofSeconds(30); + + private static final String SERVER_CONFIG_DIR = "/config/"; + + private static final String APPLICATION_DROPIN_DIR = "/config/dropins/"; + + private static final List DEFAULT_FEATURES = Arrays.asList("webProfile-10.0"); + + // Container fields + private Transferable serverConfiguration; + + private List features = new ArrayList<>(); + + // Constructors + public LibertyServerContainer(String imageName) { + this(DockerImageName.parse(imageName)); + } + + public LibertyServerContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + preconfigure(); + } + + /** + * Configure defaults that can be overridden by developer prior to start() + */ + private void preconfigure() { + withHttpPort(DEFAULT_HTTP_PORT); + withHttpWaitTimeout(DEFAULT_WAIT_TIMEOUT); + } + + // Overrides + + @Override + public void configure() { + super.configure(); + + // Copy server configuration + if (Objects.nonNull(serverConfiguration)) { + withCopyToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + return; + } + + if (!features.isEmpty()) { + withCopyToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml"); + return; + } + + withCopyToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml"); + } + + @Override + protected String getApplicationInstallDirectory() { + return APPLICATION_DROPIN_DIR; + } + + // Configuration + + /** + * The server configuration file that will be copied to the Liberty container. + * + * Calling this method more than once will replace the existing serverConfig if set. + * + * @param serverConfig - server.xml + * @return self + */ + public LibertyServerContainer withServerConfiguration(@NonNull MountableFile serverConfig) { + this.serverConfiguration = serverConfig; + return this; + } + + /** + * A list of Liberty features to configure on the Liberty container. + * + * These features will be ignored if a serverConfig file is set. + * + * @param features - The list of features + * @return self + */ + public LibertyServerContainer withFeatures(String... features) { + this.features.addAll(Arrays.asList(features)); + return this; + } + + // Helpers + + private static final Transferable generateServerConfiguration(List features) { + String configContents = ""; + configContents += ""; + for (String feature : features) { + configContents += "" + feature + ""; + } + configContents += ""; + configContents += System.lineSeparator(); + + return Transferable.of(configContents); + } +} diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java new file mode 100644 index 00000000000..c475a5635e1 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java @@ -0,0 +1,132 @@ +package org.testcontainers.liberty; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.testcontainers.applicationserver.ApplicationServerContainer; +import org.testcontainers.containers.MockServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.util.function.Consumer; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +public class LibertyContainerTest { + + // constructorWithVersion { + static Network network = Network.newNetwork(); + + private static DockerImageName libertyImage = DockerImageName.parse("open-liberty:full-java17-openj9"); + + private static ApplicationServerContainer liberty = new LibertyServerContainer(libertyImage) + .withArchives(ShrinkWrap.create(WebArchive.class, "test.war").addPackage("org.testcontainers.liberty.app")) + .withAppContextRoot("test/app/service/") + .withNetwork(network);; + // } + + // constructorMockDatabase { + private static DockerImageName mockDatabaseImage = DockerImageName.parse("mockserver/mockserver:mockserver-5.15.0"); + + private static MockServerContainer mockDatabase = new MockServerContainer(mockDatabaseImage) + .withNetwork(network) + .withNetworkAliases("mockDatabase"); + + // } + + @BeforeClass + public static void setup() { + mockDatabase.withCopyFileToContainer(MountableFile.forClasspathResource("expectation.json"), "/expectation.json"); + mockDatabase.withEnv("MOCKSERVER_WATCH_INITIALIZATION_JSON", "true"); + mockDatabase.withEnv("MOCKSERVER_INITIALIZATION_JSON_PATH", "/expectation.json"); + mockDatabase.start(); + + //Note cannot use mockDatabase.getEndpoint() since it will return http://localhost:56254 when instead we need http://mockDatabase:1080 + //This is unintuitive and should have a better solution. + + // configureLiberty { + liberty.withEnv("DB_URL", mockDatabase.getNetworkAliases().get(0) + ":1080"); + // } + + liberty.start(); + } + + @AfterClass + public static void teardown() { + liberty.stop(); + mockDatabase.stop(); + } + + @Test + public void testURLs() { + String expectedURL, actualURL; + + expectedURL = + "http://" + liberty.getHost() + ":" + liberty.getMappedPort(LibertyServerContainer.DEFAULT_HTTP_PORT); + actualURL = liberty.getBaseURL(); + assertThat(actualURL).isEqualTo(expectedURL); + + expectedURL += "/test/app/service"; + actualURL = liberty.getApplicationURL(); + assertThat(actualURL).isEqualTo(expectedURL); + + actualURL = liberty.getReadinessURL(); + assertThat(actualURL).isEqualTo(expectedURL); + } + + // testRestEndpoint { + @Test + public void testRestEndpoint() { + RequestSpecification request = new RequestSpecBuilder().setBaseUri(liberty.getApplicationURL()).build(); + + String expected, actual; + + //Add value to cache + given(request) + .header("Content-Type", "text/plain") + .queryParam("value", "post-it") + .when() + .post() + .then() + .statusCode(200); + + //Verify value in cache + expected = "[post-it]"; + actual = given(request).accept("text/plain").when().get("/").then().statusCode(200).extract().body().asString(); + + assertThat(actual).isEqualTo(expected); + } + + // } + + @Test + public void testResourceConnection() { + RequestSpecification mockDbRequest = new RequestSpecBuilder().setBaseUri(mockDatabase.getEndpoint()).build(); + RequestSpecification libertyRequest = new RequestSpecBuilder().setBaseUri(liberty.getBaseURL()).build(); + + String expected, actual; + + expected = "Hello World!"; + + //Verify liberty could connect to database + actual = + given(libertyRequest) + .accept("text/plain") + .when() + .get("/test/app/resource") + .then() + .statusCode(200) + .extract() + .body() + .asString(); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java new file mode 100644 index 00000000000..87e62f8d948 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java @@ -0,0 +1,7 @@ +package org.testcontainers.liberty.app; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/app") +public class TestApp extends Application {} diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java new file mode 100644 index 00000000000..a301fc7a1f2 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java @@ -0,0 +1,37 @@ +package org.testcontainers.liberty.app; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +@Path("/resource") +@Produces(MediaType.TEXT_PLAIN) +@Consumes(MediaType.TEXT_PLAIN) +public class TestResource { + + private static final String dbURL = System.getenv("DB_URL"); + + @GET + public String getConnection() { + try { + URL url = new URL(dbURL + "/hello"); + System.out.println("KJA1017 url is: " + url.toString()); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + String response = in.readLine(); + System.out.println("KJA1017 response: " + response); + return response; + } + } catch (Exception e) { + System.out.println("KJA1017 error: " + e.toString()); + } + return "FAILURE"; + } +} diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java new file mode 100644 index 00000000000..e0b6e852311 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java @@ -0,0 +1,58 @@ +package org.testcontainers.liberty.app; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import java.util.ArrayList; +import java.util.List; + +@Path("/service") +@Produces(MediaType.TEXT_PLAIN) +@Consumes(MediaType.TEXT_PLAIN) +public class TestService { + + private static final List cache = new ArrayList<>(); + + @GET + public String getAll() { + System.out.println("Calling getAll"); + return cache.toString(); + } + + @GET + @Path("/{value}") + public boolean isCached(@PathParam("value") String value) { + System.out.println("Calling isCached with value " + value); + return cache.contains(value); + } + + @POST + public String cacheIt(@QueryParam("value") String value) { + System.out.println("Calling cacheIt with value " + value); + cache.add(value); + return value; + } + + @POST + @Path("/{value}") + public boolean updateIt(@PathParam("value") String oldValue, @QueryParam("value") String newValue) { + System.out.println("Calling updateIt with oldValue " + oldValue + " and new value " + newValue); + boolean result = cache.remove(oldValue); + cache.add(newValue); + return result; + } + + @DELETE + @Path("/{value}") + public boolean removeIt(@PathParam("value") String value) { + System.out.println("Calling removeIt with value " + value); + return cache.remove(value); + } +} diff --git a/modules/liberty/src/test/resources/expectation.json b/modules/liberty/src/test/resources/expectation.json new file mode 100644 index 00000000000..aced7a9f6c4 --- /dev/null +++ b/modules/liberty/src/test/resources/expectation.json @@ -0,0 +1,10 @@ +[ + { + "httpRequest": { + "path": "/hello" + }, + "httpResponse": { + "body": "Hello World!" + } + } +] diff --git a/modules/liberty/src/test/resources/logback-test.xml b/modules/liberty/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/liberty/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + +