From 715ff0b28c2802f33361686b9ce1612bd9947ae9 Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Wed, 7 Jun 2023 10:00:45 -0500 Subject: [PATCH 1/8] WIP blueprint for application containers --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 + .github/ISSUE_TEMPLATE/enhancement.yaml | 2 + .github/ISSUE_TEMPLATE/feature.yaml | 2 + .github/dependabot.yml | 10 + modules/application-platform/build.gradle | 9 + .../containers/ApplicationContainer.java | 309 ++++++++++++++++++ .../containers/ApplicationContainerTest.java | 89 +++++ .../src/test/resources/logback-test.xml | 16 + modules/liberty/build.gradle | 13 + .../containers/LibertyContainer.java | 81 +++++ .../containers/LibertyContainerTest.java | 96 ++++++ .../containers/app/TestApp.java | 8 + .../containers/app/TestService.java | 57 ++++ .../test/resources/liberty/config/server.xml | 5 + .../src/test/resources/logback-test.xml | 16 + 15 files changed, 715 insertions(+) create mode 100644 modules/application-platform/build.gradle create mode 100644 modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java create mode 100644 modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java create mode 100644 modules/application-platform/src/test/resources/logback-test.xml create mode 100644 modules/liberty/build.gradle create mode 100644 modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java create mode 100644 modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java create mode 100644 modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java create mode 100644 modules/liberty/src/test/java/org/testcontainers/containers/app/TestService.java create mode 100644 modules/liberty/src/test/resources/liberty/config/server.xml create mode 100644 modules/liberty/src/test/resources/logback-test.xml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 033fd19c49d..eeb834f4a03 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,6 +14,7 @@ body: description: Which Testcontainers module are you using? options: - Core + - Application-Platform - Azure - Cassandra - Clickhouse @@ -29,6 +30,7 @@ body: - InfluxDB - K3S - Kafka + - Liberty - LocalStack - MariaDB - MockServer diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index b9d67bc7d52..05adc347540 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -14,6 +14,7 @@ body: description: For which Testcontainers module does the enhancement proposal apply? options: - Core + - Application-Platform - Azure - Cassandra - Clickhouse @@ -29,6 +30,7 @@ body: - InfluxDB - K3S - Kafka + - Liberty - LocalStack - MariaDB - MockServer diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index ffc1b19a6cb..32ccea4bf91 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -14,6 +14,7 @@ body: description: Is this feature related to any of the existing modules? options: - Core + - Application-Platform - Azure - Cassandra - Clickhouse @@ -29,6 +30,7 @@ body: - InfluxDB - K3S - Kafka + - Liberty - LocalStack - MariaDB - MockServer diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 52d11753ca8..31d388a2e77 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -31,6 +31,11 @@ updates: open-pull-requests-limit: 10 # Explicit entry for each module + - package-ecosystem: "gradle" + directory: "/modules/application-platform" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/azure" schedule: @@ -141,6 +146,11 @@ updates: schedule: interval: "monthly" 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/modules/application-platform/build.gradle b/modules/application-platform/build.gradle new file mode 100644 index 00000000000..9aa941a9434 --- /dev/null +++ b/modules/application-platform/build.gradle @@ -0,0 +1,9 @@ +description = "Testcontainers :: Application Platform" + +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-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java b/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java new file mode 100644 index 00000000000..ab1205c477b --- /dev/null +++ b/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java @@ -0,0 +1,309 @@ +package org.testcontainers.containers; + +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.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 ApplicationContainer extends GenericContainer { + + // List of applications to install + List archives = new ArrayList<>(); + + // Where to save runtime application 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 application + private Integer httpPort; + + private String appContextRoot; + + // How to query application for readiness + private Integer readinessPort; + + private String readinessPath; + + private Duration readinessTimeout; + + // Expected path for Microprofile platforms to query for readiness + static final String MP_HEALTH_READINESS_PATH = "/health/ready"; + + //Constructors + + public ApplicationContainer(@NonNull final Future image) { + super(image); + } + + public ApplicationContainer(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(readinessTimeout != null ? readinessTimeout : getDefaultWaitTimeout()) + ); + + // Copy applications + for(MountableFile archive : archives) { + //FIXME folder-like containerPath in copyFileToContainer is deprecated + withCopyFileToContainer(archive, getApplicationInstallDirectory()); + } + } + + @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 + * + * @param archives - An archive created using shrinkwrap and test runtime + * @return self + */ + public ApplicationContainer withApplicationArchvies(@NonNull Archive... archives) { + Stream.of(archives).forEach(archive -> { + String name = archive.getName(); + Path target = Paths.get(getTempDirectory().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 + * + * @param archives - A MountableFile which represents an archive that was created prior to test runtime. + * @return self + */ + public ApplicationContainer withApplicationArchives(@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. + * + * @param httpPort - The HTTP port used by the Application platform + * @return self + */ + public ApplicationContainer withHttpPort(int httpPort) { + if( Objects.nonNull(this.httpPort) ) { + throw new IllegalStateException("Only one application HTTP port can be configured."); + } + + this.httpPort = httpPort; + super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort)); + + return this; + } + + /** + * This will set the path used to construct the base application URL + * + * @param appContextRoot - The application path + * @return self + */ + public ApplicationContainer withAppContextRoot(@NonNull String appContextRoot) { + this.appContextRoot = normalizePath(appContextRoot); + return this; + } + + /** + * Sets the path to be used to determine container readiness. + * This will be used to construct a default WaitStrategy using Wait.forHttp() + * + * @param readinessPath - The path to be polled for readiness. + * @return self + */ + public ApplicationContainer withReadinessPath(String readinessPath) { + return withReadinessPath(readinessPath, getDefaultWaitTimeout()); + } + + /** + * Sets the path to be used to determine container readiness. + * The readiness check will timeout a user defined timeout. + * This will be used to construct a default WaitStrategy using Wait.forHttp() + * + * @param readinessPath - The path to be polled for readiness. + * @param timeout - The timeout after which the readiness check will fail. + * @return self + */ + public ApplicationContainer withReadinessPath(String readinessPath, Duration timeout) { + return withReadinessPath(readinessPath, httpPort, timeout); + } + + /** + * Sets the path to be used to determine container readiness. + * The readiness check will timeout a user defined timeout. + * The readiness check will happen on an alternative port to the httpPort + * + * @param readinessPath - The path to be polled for readiness. + * @param readinessPort - The port that should be used for the readiness check. + * @param timeout - The timeout after which the readiness check will fail. + * @return self + */ + public ApplicationContainer withReadinessPath(@NonNull String readinessPath, @NonNull Integer readinessPort, @NonNull Duration timeout) { + this.readinessPath = normalizePath(readinessPath); + this.readinessPort = readinessPort; + 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 ApplicationContainer#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 determine an appropriate amount of time + * to wait for their application platform to start. + * + * @return - Duration to wait for startup + */ + abstract protected Duration getDefaultWaitTimeout(); + + /** + * Each implementation will need to provide an install directory + * where their platform expects applications to be copied into. + * + * @return - the application install directory + */ + abstract protected String getApplicationInstallDirectory(); + + // Helpers + + /** + * Create a temporary directory where runtime archives can be exported and copied to the application container. + * + * @return - The temporary directory path + */ + private static Path getTempDirectory() { + 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; + } + + /** + * 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()); + } + + /** + * 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-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java b/modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java new file mode 100644 index 00000000000..c3a0f8af2fc --- /dev/null +++ b/modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java @@ -0,0 +1,89 @@ +package org.testcontainers.containers; + +import lombok.NonNull; +import org.junit.Test; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Future; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ApplicationContainerTest { + + private static ApplicationContainer testContainer; + + @Test + public void testNormalizePath() { + String expected, actual; + + expected = "/path/to/application"; + actual = ApplicationContainer.normalizePath("path", "to", "application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationContainer.normalizePath("path/to", "application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationContainer.normalizePath("path/to/application"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationContainer.normalizePath("path/", "to/", "application/"); + assertThat(actual).isEqualTo(expected); + + actual = ApplicationContainer.normalizePath("path/", "/to/", "/application/"); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void httpPortMapping() { + List expected, actual; + + expected = List.of(8080, 9080, 9443); + + // Test expose ports, then add httpPort + testContainer = new ApplicationContainerStub(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 ApplicationContainerStub(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 ApplicationContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer.withHttpPort(8080); + testContainer.setExposedPorts(List.of(9080, 9443)); + + actual = testContainer.getExposedPorts(); + assertThat(actual).containsExactlyElementsOf(expected); + } + + static class ApplicationContainerStub extends ApplicationContainer { + + public ApplicationContainerStub(@NonNull Future image) { + super(image); + } + + public ApplicationContainerStub(DockerImageName dockerImageName) { + super(dockerImageName); + } + + @Override + protected Duration getDefaultWaitTimeout() { + return Duration.ofSeconds(1); + } + + @Override + protected String getApplicationInstallDirectory() { + return "null"; + } + } +} diff --git a/modules/application-platform/src/test/resources/logback-test.xml b/modules/application-platform/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/application-platform/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..d9caa708812 --- /dev/null +++ b/modules/liberty/build.gradle @@ -0,0 +1,13 @@ +description = "Testcontainers :: Application Platform :: Liberty" + +dependencies { + api project(':application-platform') + + compileOnly 'org.jetbrains:annotations:24.0.1' + + implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6' + + 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/containers/LibertyContainer.java b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java new file mode 100644 index 00000000000..5b63ac0bdb6 --- /dev/null +++ b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java @@ -0,0 +1,81 @@ +package org.testcontainers.containers; + +import lombok.NonNull; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.Future; + +/** + * Represents an Open Liberty or WebSphere Liberty container + */ +public class LibertyContainer extends ApplicationContainer { + + public static final String NAME = "Liberty"; + + // About the image + static final String IMAGE = "open-liberty"; + + static final String DEFAULT_TAG = "23.0.0.3-full-java17-openj9"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); + + // Container defaults + public static final int DEFAULT_HTTP_PORT = 9080; + + public static final int DEFAULT_HTTPS_PORT = 9443; + + private 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/"; + + // Container fields + private MountableFile serverConfiguration; + + // Constructors + + public LibertyContainer() { + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + } + + public LibertyContainer(@NonNull Future image) { + super(image); + } + + public LibertyContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + } + + // Overrides + + @Override + public void configure() { + super.configure(); + + //Copy server configuration + Objects.requireNonNull(serverConfiguration); + withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + } + + @Override + protected Duration getDefaultWaitTimeout() { + return DEFAULT_WAIT_TIMEOUT; + } + + @Override + protected String getApplicationInstallDirectory() { + return APPLICATION_DROPIN_DIR; + } + + // Configuration + + public LibertyContainer withServerConfiguration(@NonNull MountableFile serverConfig) { + this.serverConfiguration = serverConfig; + return this; + } +} diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java new file mode 100644 index 00000000000..4ce70aca2f2 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -0,0 +1,96 @@ +package org.testcontainers.containers; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.jboss.shrinkwrap.api.Archive; +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.containers.output.OutputFrame; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +import static io.restassured.RestAssured.given; + +public class LibertyContainerTest { + + private static ApplicationContainer testContainer = new LibertyContainer() + .withServerConfiguration(MountableFile.forClasspathResource("liberty/config/server.xml")) + .withApplicationArchvies(createDeployment()) + .withHttpPort(LibertyContainer.DEFAULT_HTTP_PORT) + .withAppContextRoot("test/app/service/") + .withLogConsumer(new Consumer() { + @Override + public void accept(OutputFrame outputFrame) { + System.out.println("[liberty]" + outputFrame.getUtf8String()); + } + }) + ; + + private static Archive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "test.war") + .addPackage("org.testcontainers.containers.app"); + } + + @BeforeClass + public static void setup() { + testContainer.start(); + } + + @AfterClass + public static void teardown() { + testContainer.stop(); + } + + @Test + public void testURLs() { + String expectedURL, actualURL; + + expectedURL = "http://" + testContainer.getHost() + ":" + testContainer.getMappedPort(LibertyContainer.DEFAULT_HTTP_PORT); + actualURL = testContainer.getBaseURL(); + assertThat(actualURL).isEqualTo(expectedURL); + + expectedURL += "/test/app/service"; + actualURL = testContainer.getApplicationURL(); + assertThat(actualURL).isEqualTo(expectedURL); + + actualURL = testContainer.getReadinessURL(); + assertThat(actualURL).isEqualTo(expectedURL); + } + + @Test + public void testRestEndpoint() { + RequestSpecification request = new RequestSpecBuilder() + .setBaseUri(testContainer.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); + } +} diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java b/modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java new file mode 100644 index 00000000000..05d712091fe --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java @@ -0,0 +1,8 @@ +package org.testcontainers.containers.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/containers/app/TestService.java b/modules/liberty/src/test/java/org/testcontainers/containers/app/TestService.java new file mode 100644 index 00000000000..6208f27fd65 --- /dev/null +++ b/modules/liberty/src/test/java/org/testcontainers/containers/app/TestService.java @@ -0,0 +1,57 @@ +package org.testcontainers.containers.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/liberty/config/server.xml b/modules/liberty/src/test/resources/liberty/config/server.xml new file mode 100644 index 00000000000..d0fa56070f5 --- /dev/null +++ b/modules/liberty/src/test/resources/liberty/config/server.xml @@ -0,0 +1,5 @@ + + + webProfile-10.0 + + 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 + + + + + + + + + From 7cdfe5e67e4adfa395495adbab6592646079320e Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Wed, 7 Jun 2023 10:51:20 -0500 Subject: [PATCH 2/8] Enhancements --- .../containers/ApplicationContainer.java | 34 +++++++++++++++---- .../containers/LibertyContainer.java | 21 ++++++++++-- .../default/config/defaultServer.xml} | 0 .../containers/LibertyContainerTest.java | 15 +------- 4 files changed, 48 insertions(+), 22 deletions(-) rename modules/liberty/src/{test/resources/liberty/config/server.xml => main/resources/default/config/defaultServer.xml} (100%) diff --git a/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java b/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java index ab1205c477b..ff6df6dfb59 100644 --- a/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java +++ b/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java @@ -79,11 +79,29 @@ protected void configure() { // Copy applications for(MountableFile archive : archives) { - //FIXME folder-like containerPath in copyFileToContainer is deprecated - withCopyFileToContainer(archive, getApplicationInstallDirectory()); + 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) ) { @@ -132,10 +150,6 @@ public ApplicationContainer withApplicationArchives(@NonNull MountableFile... ar * @return self */ public ApplicationContainer withHttpPort(int httpPort) { - if( Objects.nonNull(this.httpPort) ) { - throw new IllegalStateException("Only one application HTTP port can be configured."); - } - this.httpPort = httpPort; super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort)); @@ -276,6 +290,14 @@ private static Path getTempDirectory() { return tempDirectory; } + private static String extractApplicationName(MountableFile file) throws IllegalArgumentException { + String path = file.getFilesystemPath(); + if( path.matches(".*\\.jar|.*\\.war|.*\\.ear") ) { + 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. diff --git a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java index 5b63ac0bdb6..4c8dbbdd2fa 100644 --- a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java +++ b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java @@ -34,7 +34,8 @@ public class LibertyContainer extends ApplicationContainer { private static final String APPLICATION_DROPIN_DIR = "/config/dropins/"; // Container fields - private MountableFile serverConfiguration; + @NonNull + private MountableFile serverConfiguration = MountableFile.forClasspathResource("default/config/defaultServer.xml"); // Constructors @@ -44,11 +45,13 @@ public LibertyContainer() { public LibertyContainer(@NonNull Future image) { super(image); + preconfigure(); } public LibertyContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + preconfigure(); } // Overrides @@ -57,9 +60,10 @@ public LibertyContainer(DockerImageName dockerImageName) { public void configure() { super.configure(); - //Copy server configuration + // Copy server configuration Objects.requireNonNull(serverConfiguration); withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + } @Override @@ -74,6 +78,19 @@ protected String getApplicationInstallDirectory() { // Configuration + /** + * Setup default configurations that can be overridden by users + */ + private void preconfigure() { + withHttpPort(DEFAULT_HTTP_PORT); + } + + /** + * The server configuration file that will be copied to the Liberty container + * + * @param serverConfig - server.xml + * @return self + */ public LibertyContainer withServerConfiguration(@NonNull MountableFile serverConfig) { this.serverConfiguration = serverConfig; return this; diff --git a/modules/liberty/src/test/resources/liberty/config/server.xml b/modules/liberty/src/main/resources/default/config/defaultServer.xml similarity index 100% rename from modules/liberty/src/test/resources/liberty/config/server.xml rename to modules/liberty/src/main/resources/default/config/defaultServer.xml diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java index 4ce70aca2f2..95c191e0f86 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -8,12 +8,8 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.utility.MountableFile; -import java.time.Duration; -import java.util.function.Consumer; - import static org.assertj.core.api.Assertions.assertThat; import static io.restassured.RestAssured.given; @@ -21,17 +17,8 @@ public class LibertyContainerTest { private static ApplicationContainer testContainer = new LibertyContainer() - .withServerConfiguration(MountableFile.forClasspathResource("liberty/config/server.xml")) .withApplicationArchvies(createDeployment()) - .withHttpPort(LibertyContainer.DEFAULT_HTTP_PORT) - .withAppContextRoot("test/app/service/") - .withLogConsumer(new Consumer() { - @Override - public void accept(OutputFrame outputFrame) { - System.out.println("[liberty]" + outputFrame.getUtf8String()); - } - }) - ; + .withAppContextRoot("test/app/service/"); private static Archive createDeployment() { return ShrinkWrap.create(WebArchive.class, "test.war") From ec310b1b6a1aabf79bdacb57267afb63890193ef Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Thu, 8 Jun 2023 10:42:02 -0500 Subject: [PATCH 3/8] feedback and refinement --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 - .github/ISSUE_TEMPLATE/enhancement.yaml | 1 - .github/ISSUE_TEMPLATE/feature.yaml | 1 - .github/dependabot.yml | 2 +- .../build.gradle | 2 +- .../ApplicationServerContainer.java} | 146 ++++++++++++------ .../ApplicationServerContainerTest.java} | 33 ++-- .../src/test/resources/logback-test.xml | 0 modules/liberty/build.gradle | 4 +- ...ainer.java => LibertyServerContainer.java} | 46 +++--- .../containers/LibertyContainerTest.java | 12 +- 11 files changed, 142 insertions(+), 106 deletions(-) rename modules/{application-platform => application-server-commons}/build.gradle (79%) rename modules/{application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java => application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java} (63%) rename modules/{application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java => application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java} (58%) rename modules/{application-platform => application-server-commons}/src/test/resources/logback-test.xml (100%) rename modules/liberty/src/main/java/org/testcontainers/containers/{LibertyContainer.java => LibertyServerContainer.java} (69%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index eeb834f4a03..021becd3c90 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,6 @@ body: description: Which Testcontainers module are you using? options: - Core - - Application-Platform - Azure - Cassandra - Clickhouse diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 05adc347540..96cb46c38f2 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -14,7 +14,6 @@ body: description: For which Testcontainers module does the enhancement proposal apply? options: - Core - - Application-Platform - Azure - Cassandra - Clickhouse diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 32ccea4bf91..9a629e54a78 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -14,7 +14,6 @@ body: description: Is this feature related to any of the existing modules? options: - Core - - Application-Platform - Azure - Cassandra - Clickhouse diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 31d388a2e77..04dc90358c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,7 +32,7 @@ updates: # Explicit entry for each module - package-ecosystem: "gradle" - directory: "/modules/application-platform" + directory: "/modules/application-server-commons" schedule: interval: "monthly" open-pull-requests-limit: 10 diff --git a/modules/application-platform/build.gradle b/modules/application-server-commons/build.gradle similarity index 79% rename from modules/application-platform/build.gradle rename to modules/application-server-commons/build.gradle index 9aa941a9434..58404e95279 100644 --- a/modules/application-platform/build.gradle +++ b/modules/application-server-commons/build.gradle @@ -1,4 +1,4 @@ -description = "Testcontainers :: Application Platform" +description = "Testcontainers :: Application Server Common" dependencies { api project(':testcontainers') diff --git a/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java similarity index 63% rename from modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java rename to modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java index ff6df6dfb59..dbee0d937f8 100644 --- a/modules/application-platform/src/main/java/org/testcontainers/containers/ApplicationContainer.java +++ b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java @@ -1,9 +1,10 @@ -package org.testcontainers.containers; +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; @@ -24,22 +25,21 @@ import java.util.stream.Stream; /** - * Represents a JavaEE, JakartaEE, or Microprofile application platform running inside - * a Docker container + * Represents a JavaEE, JakartaEE, or Microprofile application platform running inside a Docker container */ -public abstract class ApplicationContainer extends GenericContainer { +public abstract class ApplicationServerContainer extends GenericContainer { - // List of applications to install + // List of archive(s) to install List archives = new ArrayList<>(); - // Where to save runtime application archives + // 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 application + // How to query an application private Integer httpPort; private String appContextRoot; @@ -49,18 +49,20 @@ public abstract class ApplicationContainer extends GenericContainer image) { + public ApplicationServerContainer(@NonNull final Future image) { super(image); } - public ApplicationContainer(final DockerImageName dockerImageName) { + public ApplicationServerContainer(final DockerImageName dockerImageName) { super(dockerImageName); } @@ -74,7 +76,7 @@ protected void configure() { waitingFor( Wait.forHttp(readinessPath != null ? readinessPath : appContextRoot) .forPort(readinessPort != null ? readinessPort : httpPort) - .withStartupTimeout(readinessTimeout != null ? readinessTimeout : getDefaultWaitTimeout()) + .withStartupTimeout(httpWaitTimeout) ); // Copy applications @@ -115,12 +117,14 @@ public void setExposedPorts(List exposedPorts) { //Configuration /** - * One or more archives to be deployed to the application platform + * 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 ApplicationContainer withApplicationArchvies(@NonNull Archive... archives) { + public ApplicationServerContainer withArchvies(@NonNull Archive... archives) { Stream.of(archives).forEach(archive -> { String name = archive.getName(); Path target = Paths.get(getTempDirectory().toString(), name); @@ -132,12 +136,14 @@ public ApplicationContainer withApplicationArchvies(@NonNull Archive... archi } /** - * One or more archives to be deployed to the application platform + * 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 ApplicationContainer withApplicationArchives(@NonNull MountableFile... archives) { + public ApplicationServerContainer withArchives(@NonNull MountableFile... archives) { this.archives.addAll(Arrays.asList(archives)); return this; } @@ -146,64 +152,87 @@ public ApplicationContainer withApplicationArchives(@NonNull MountableFile... ar * 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 ApplicationContainer withHttpPort(int httpPort) { - this.httpPort = httpPort; - super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort)); + 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 + * 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 ApplicationContainer withAppContextRoot(@NonNull String appContextRoot) { - this.appContextRoot = normalizePath(appContextRoot); + 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; } /** - * Sets the path to be used to determine container readiness. - * This will be used to construct a default WaitStrategy using Wait.forHttp() + * 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 ApplicationContainer withReadinessPath(String readinessPath) { - return withReadinessPath(readinessPath, getDefaultWaitTimeout()); + public ApplicationServerContainer withReadiness(String readinessPath) { + return withReadiness(readinessPath, this.httpPort); } /** - * Sets the path to be used to determine container readiness. - * The readiness check will timeout a user defined timeout. - * This will be used to construct a default WaitStrategy using Wait.forHttp() + * 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 timeout - The timeout after which the readiness check will fail. + * @param readinessPort - The port that should be used for the readiness check. * @return self */ - public ApplicationContainer withReadinessPath(String readinessPath, Duration timeout) { - return withReadinessPath(readinessPath, httpPort, timeout); + public ApplicationServerContainer withReadiness(@NonNull String readinessPath, @NonNull Integer readinessPort) { + this.readinessPath = normalizePath(readinessPath); + this.readinessPort = readinessPort; + return this; } /** - * Sets the path to be used to determine container readiness. - * The readiness check will timeout a user defined timeout. - * The readiness check will happen on an alternative port to the httpPort + * Set the timeout configured on the HttpWaitStrategy. * - * @param readinessPath - The path to be polled for readiness. - * @param readinessPort - The port that should be used for the readiness check. - * @param timeout - The timeout after which the readiness check will fail. + * 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 ApplicationContainer withReadinessPath(@NonNull String readinessPath, @NonNull Integer readinessPort, @NonNull Duration timeout) { - this.readinessPath = normalizePath(readinessPath); - this.readinessPort = readinessPort; + public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) { + this.httpWaitTimeout = httpWaitTimeout; return this; } @@ -226,7 +255,7 @@ public String getReadinessURL() { /** * The URL where the application is running. - * The application URL is a concatenation of the baseURL {@link ApplicationContainer#getBaseURL()} with the appContextRoot. + * 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 @@ -247,15 +276,18 @@ public String getBaseURL() { return "http://" + getHost() + ':' + getMappedPort(this.httpPort); } - // Abstract - /** - * Each implementation will need to determine an appropriate amount of time - * to wait for their application platform to start. + * Gets the current state of the HTTP port. + * Helpful during the configure step for implementations to set a default port + * if none was configured by the user. * - * @return - Duration to wait for startup + * @return true if an httpPort was set, false otherwise. */ - abstract protected Duration getDefaultWaitTimeout(); + protected boolean isHttpPortSet() { + return Objects.nonNull(httpPort); + } + + // Abstract /** * Each implementation will need to provide an install directory @@ -292,7 +324,8 @@ private static Path getTempDirectory() { private static String extractApplicationName(MountableFile file) throws IllegalArgumentException { String path = file.getFilesystemPath(); - if( path.matches(".*\\.jar|.*\\.war|.*\\.ear") ) { + //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"); @@ -312,6 +345,23 @@ protected static List appendPort(List portList, int appendingP 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 '/' diff --git a/modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java similarity index 58% rename from modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java rename to modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java index c3a0f8af2fc..a4514384ed6 100644 --- a/modules/application-platform/src/test/java/org/testcontainers/containers/ApplicationContainerTest.java +++ b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.applicationserver; import lombok.NonNull; import org.junit.Test; @@ -10,28 +10,28 @@ import static org.assertj.core.api.Assertions.assertThat; -public class ApplicationContainerTest { +public class ApplicationServerContainerTest { - private static ApplicationContainer testContainer; + private static ApplicationServerContainer testContainer; @Test public void testNormalizePath() { String expected, actual; expected = "/path/to/application"; - actual = ApplicationContainer.normalizePath("path", "to", "application"); + actual = ApplicationServerContainer.normalizePath("path", "to", "application"); assertThat(actual).isEqualTo(expected); - actual = ApplicationContainer.normalizePath("path/to", "application"); + actual = ApplicationServerContainer.normalizePath("path/to", "application"); assertThat(actual).isEqualTo(expected); - actual = ApplicationContainer.normalizePath("path/to/application"); + actual = ApplicationServerContainer.normalizePath("path/to/application"); assertThat(actual).isEqualTo(expected); - actual = ApplicationContainer.normalizePath("path/", "to/", "application/"); + actual = ApplicationServerContainer.normalizePath("path/", "to/", "application/"); assertThat(actual).isEqualTo(expected); - actual = ApplicationContainer.normalizePath("path/", "/to/", "/application/"); + actual = ApplicationServerContainer.normalizePath("path/", "/to/", "/application/"); assertThat(actual).isEqualTo(expected); } @@ -42,7 +42,7 @@ public void httpPortMapping() { expected = List.of(8080, 9080, 9443); // Test expose ports, then add httpPort - testContainer = new ApplicationContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withExposedPorts(9080, 9443); testContainer.withHttpPort(8080); @@ -50,7 +50,7 @@ public void httpPortMapping() { assertThat(actual).containsExactlyElementsOf(expected); // Test httpPort then expose ports - testContainer = new ApplicationContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withHttpPort(8080); testContainer.withExposedPorts(9080, 9443); @@ -58,7 +58,7 @@ public void httpPortMapping() { assertThat(actual).containsExactlyElementsOf(expected); //Test httpPort then set exposed ports - testContainer = new ApplicationContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withHttpPort(8080); testContainer.setExposedPorts(List.of(9080, 9443)); @@ -66,21 +66,16 @@ public void httpPortMapping() { assertThat(actual).containsExactlyElementsOf(expected); } - static class ApplicationContainerStub extends ApplicationContainer { + static class ApplicationServerContainerStub extends ApplicationServerContainer { - public ApplicationContainerStub(@NonNull Future image) { + public ApplicationServerContainerStub(@NonNull Future image) { super(image); } - public ApplicationContainerStub(DockerImageName dockerImageName) { + public ApplicationServerContainerStub(DockerImageName dockerImageName) { super(dockerImageName); } - @Override - protected Duration getDefaultWaitTimeout() { - return Duration.ofSeconds(1); - } - @Override protected String getApplicationInstallDirectory() { return "null"; diff --git a/modules/application-platform/src/test/resources/logback-test.xml b/modules/application-server-commons/src/test/resources/logback-test.xml similarity index 100% rename from modules/application-platform/src/test/resources/logback-test.xml rename to modules/application-server-commons/src/test/resources/logback-test.xml diff --git a/modules/liberty/build.gradle b/modules/liberty/build.gradle index d9caa708812..4b50b748e8e 100644 --- a/modules/liberty/build.gradle +++ b/modules/liberty/build.gradle @@ -1,7 +1,7 @@ -description = "Testcontainers :: Application Platform :: Liberty" +description = "Testcontainers :: Application Server :: Liberty" dependencies { - api project(':application-platform') + api project(':application-server-commons') compileOnly 'org.jetbrains:annotations:24.0.1' diff --git a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java similarity index 69% rename from modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java rename to modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java index 4c8dbbdd2fa..29c1eb579e6 100644 --- a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyContainer.java +++ b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java @@ -3,22 +3,22 @@ import lombok.NonNull; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; +import org.testcontainers.applicationserver.ApplicationServerContainer; import java.time.Duration; import java.util.Objects; -import java.util.concurrent.Future; /** * Represents an Open Liberty or WebSphere Liberty container */ -public class LibertyContainer extends ApplicationContainer { +public class LibertyServerContainer extends ApplicationServerContainer { public static final String NAME = "Liberty"; // About the image - static final String IMAGE = "open-liberty"; + public static final String IMAGE = "open-liberty"; - static final String DEFAULT_TAG = "23.0.0.3-full-java17-openj9"; + public static final String DEFAULT_TAG = "23.0.0.3-full-java17-openj9"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); @@ -38,37 +38,33 @@ public class LibertyContainer extends ApplicationContainer { private MountableFile serverConfiguration = MountableFile.forClasspathResource("default/config/defaultServer.xml"); // Constructors - - public LibertyContainer() { - this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + public LibertyServerContainer(String imageName) { + this(DockerImageName.parse(imageName)); } - public LibertyContainer(@NonNull Future image) { - super(image); - preconfigure(); - } - - public LibertyContainer(DockerImageName dockerImageName) { + 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 + // Copy default server configuration Objects.requireNonNull(serverConfiguration); withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); - - } - - @Override - protected Duration getDefaultWaitTimeout() { - return DEFAULT_WAIT_TIMEOUT; } @Override @@ -78,20 +74,14 @@ protected String getApplicationInstallDirectory() { // Configuration - /** - * Setup default configurations that can be overridden by users - */ - private void preconfigure() { - withHttpPort(DEFAULT_HTTP_PORT); - } - /** * The server configuration file that will be copied to the Liberty container * * @param serverConfig - server.xml * @return self */ - public LibertyContainer withServerConfiguration(@NonNull MountableFile serverConfig) { + public LibertyServerContainer withServerConfiguration(@NonNull MountableFile serverConfig) { + System.out.println("KJA1017 serverConfig called: " + serverConfig.getFilesystemPath()); this.serverConfiguration = serverConfig; return this; } diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java index 95c191e0f86..e17037731b0 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -8,7 +8,8 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import org.testcontainers.utility.MountableFile; +import org.testcontainers.applicationserver.ApplicationServerContainer; +import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; @@ -16,8 +17,11 @@ public class LibertyContainerTest { - private static ApplicationContainer testContainer = new LibertyContainer() - .withApplicationArchvies(createDeployment()) + private static final DockerImageName libertyImage = DockerImageName.parse(LibertyServerContainer.IMAGE) + .withTag(LibertyServerContainer.DEFAULT_TAG); + + private static ApplicationServerContainer testContainer = new LibertyServerContainer(libertyImage) + .withArchvies(createDeployment()) .withAppContextRoot("test/app/service/"); private static Archive createDeployment() { @@ -39,7 +43,7 @@ public static void teardown() { public void testURLs() { String expectedURL, actualURL; - expectedURL = "http://" + testContainer.getHost() + ":" + testContainer.getMappedPort(LibertyContainer.DEFAULT_HTTP_PORT); + expectedURL = "http://" + testContainer.getHost() + ":" + testContainer.getMappedPort(LibertyServerContainer.DEFAULT_HTTP_PORT); actualURL = testContainer.getBaseURL(); assertThat(actualURL).isEqualTo(expectedURL); From 3d48b930ced52a2ce6bec9d16b0a565b17dd7d84 Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Thu, 8 Jun 2023 14:17:02 -0500 Subject: [PATCH 4/8] More improvements --- .../ApplicationServerContainer.java | 2 +- .../containers/LibertyServerContainer.java | 75 ++++++++++++++++--- .../default/config/defaultServer.xml | 5 -- .../containers/LibertyContainerTest.java | 2 +- 4 files changed, 68 insertions(+), 16 deletions(-) delete mode 100644 modules/liberty/src/main/resources/default/config/defaultServer.xml 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 index dbee0d937f8..ec7d7a5f0f2 100644 --- 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 @@ -304,7 +304,7 @@ protected boolean isHttpPortSet() { * * @return - The temporary directory path */ - private static Path getTempDirectory() { + protected static Path getTempDirectory() { if( Objects.nonNull(tempDirectory) ) { return tempDirectory; } diff --git a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java index 29c1eb579e6..45246b8ab5a 100644 --- a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java +++ b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java @@ -5,7 +5,16 @@ import org.testcontainers.utility.MountableFile; import org.testcontainers.applicationserver.ApplicationServerContainer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Objects; /** @@ -18,8 +27,6 @@ public class LibertyServerContainer extends ApplicationServerContainer { // About the image public static final String IMAGE = "open-liberty"; - public static final String DEFAULT_TAG = "23.0.0.3-full-java17-openj9"; - private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); // Container defaults @@ -33,9 +40,12 @@ public class LibertyServerContainer extends ApplicationServerContainer { private static final String APPLICATION_DROPIN_DIR = "/config/dropins/"; + private static final List DEFAULT_FEATURES = Arrays.asList("webProfile-10.0"); + // Container fields - @NonNull - private MountableFile serverConfiguration = MountableFile.forClasspathResource("default/config/defaultServer.xml"); + private MountableFile serverConfiguration; + + private List features = new ArrayList<>(); // Constructors public LibertyServerContainer(String imageName) { @@ -62,9 +72,19 @@ private void preconfigure() { public void configure() { super.configure(); - // Copy default server configuration - Objects.requireNonNull(serverConfiguration); - withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + // Copy server configuration + if( Objects.nonNull(serverConfiguration) ) { + withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + return; + } + + if ( ! features.isEmpty() ) { + withCopyFileToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml"); + return; + } + + withCopyFileToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml"); + } @Override @@ -75,14 +95,51 @@ protected String getApplicationInstallDirectory() { // Configuration /** - * The server configuration file that will be copied to the Liberty container + * 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) { - System.out.println("KJA1017 serverConfig called: " + serverConfig.getFilesystemPath()); 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 MountableFile generateServerConfiguration(List features) { + String configContents = ""; + configContents += ""; + for(String feature : features) { + configContents += "" + feature + ""; + } + configContents += ""; + configContents += System.lineSeparator(); + + Path generatedConfigPath = Paths.get(getTempDirectory().toString(), "generatedServer.xml"); + + try { + Files.write(generatedConfigPath, configContents.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } catch (IOException ioe) { + throw new RuntimeException("Unable to generate server configuration at runtime", ioe); + } + + return MountableFile.forHostPath(generatedConfigPath); + } } diff --git a/modules/liberty/src/main/resources/default/config/defaultServer.xml b/modules/liberty/src/main/resources/default/config/defaultServer.xml deleted file mode 100644 index d0fa56070f5..00000000000 --- a/modules/liberty/src/main/resources/default/config/defaultServer.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - webProfile-10.0 - - diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java index e17037731b0..e85e0572871 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -18,7 +18,7 @@ public class LibertyContainerTest { private static final DockerImageName libertyImage = DockerImageName.parse(LibertyServerContainer.IMAGE) - .withTag(LibertyServerContainer.DEFAULT_TAG); + .withTag("23.0.0.3-full-java17-openj9"); private static ApplicationServerContainer testContainer = new LibertyServerContainer(libertyImage) .withArchvies(createDeployment()) From 94348f17eb0d21e1da02166eeb72b4013ceb65ae Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Fri, 9 Jun 2023 10:50:57 -0500 Subject: [PATCH 5/8] enhancement lazy env vars --- .../ApplicationServerContainer.java | 34 +++++++++++++++++++ .../containers/LibertyContainerTest.java | 23 +++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) 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 index ec7d7a5f0f2..b88bf8fc14d 100644 --- 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 @@ -18,9 +18,14 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -56,6 +61,9 @@ public abstract class ApplicationServerContainer extends GenericContainer> lazyEnvVars = new HashMap<>(); + //Constructors public ApplicationServerContainer(@NonNull final Future image) { @@ -83,6 +91,11 @@ protected void configure() { for(MountableFile archive : archives) { withCopyFileToContainer(archive, getApplicationInstallDirectory() + extractApplicationName(archive)); } + + // Add lazy env variables + for(Map.Entry> entry : lazyEnvVars.entrySet()) { + withEnv(entry.getKey(), entry.getValue().get()); + } } @Override @@ -236,6 +249,27 @@ public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) return this; } + /** + * An environment variable whose value needs to wait until the configuration stage to be evaluated. + * The common use case for this would be to pass inter-container connection data. + * + * For example an application container often needs to have connection data to a dependent database: + *
+     *     OracleContainer db = new OracleContainer(...).withExposedPorts(1521);
+     *     ApplicationContainer app = new ApplicationContainer(...)
+     *       .withLazyEnv( "oracle.url", () -> db.getJdbcUrl() ) // Need to wait until 'db' container is configured
+     *       .dependsOn(db);
+     * 
+ * + * @param key - the key + * @param valueSupplier - the function that supplies the value + * @return self + */ + public ApplicationServerContainer withLazyEnv(String key, Supplier valueSupplier) { + lazyEnvVars.put(key, valueSupplier); + return this; + } + //Getters /** diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java index e85e0572871..95b02eb2aeb 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -17,13 +17,32 @@ public class LibertyContainerTest { + /* + * Typically application servers have a dependency on a database or other microservice during integration testing + * Use a mockServer to ensure dependsOn and lazy env variables work. + */ + public static final DockerImageName mockImage = DockerImageName + .parse("mockserver/mockserver") + .withTag("mockserver-5.15.0"); + + private static GenericContainer dependantService = new GenericContainer<>(mockImage) + .withExposedPorts(80,81); + + //The liberty test container private static final DockerImageName libertyImage = DockerImageName.parse(LibertyServerContainer.IMAGE) .withTag("23.0.0.3-full-java17-openj9"); private static ApplicationServerContainer testContainer = new LibertyServerContainer(libertyImage) .withArchvies(createDeployment()) - .withAppContextRoot("test/app/service/"); - + .withAppContextRoot("test/app/service/") + .withLazyEnv("mock.port", () -> "" + dependantService.getMappedPort(80)) + .dependsOn(dependantService); + + /** + * Creates a deployment using Shrinkwrap at runtime. + * + * @return - the application archive + */ private static Archive createDeployment() { return ShrinkWrap.create(WebArchive.class, "test.war") .addPackage("org.testcontainers.containers.app"); From 2a0359c0d728c24e464d274f046c715824c78bdf Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Mon, 19 Jun 2023 10:23:31 -0500 Subject: [PATCH 6/8] additional feedback --- .../ApplicationServerContainer.java | 21 ++-------- .../ApplicationServerContainerTest.java | 5 ++- .../containers/LibertyServerContainer.java | 40 ++++++------------- .../containers/LibertyContainerTest.java | 2 +- 4 files changed, 20 insertions(+), 48 deletions(-) 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 index b88bf8fc14d..82f1f6930d3 100644 --- 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 @@ -23,8 +23,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Future; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -137,10 +135,10 @@ public void setExposedPorts(List exposedPorts) { * @param archives - An archive created using shrinkwrap and test runtime * @return self */ - public ApplicationServerContainer withArchvies(@NonNull Archive... archives) { + public ApplicationServerContainer withArchives(@NonNull Archive... archives) { Stream.of(archives).forEach(archive -> { String name = archive.getName(); - Path target = Paths.get(getTempDirectory().toString(), name); + Path target = Paths.get(createTempDirectory().toString(), name); archive.as(ZipExporter.class).exportTo(target.toFile(), true); this.archives.add(MountableFile.forHostPath(target)); }); @@ -310,17 +308,6 @@ public String getBaseURL() { return "http://" + getHost() + ':' + getMappedPort(this.httpPort); } - /** - * Gets the current state of the HTTP port. - * Helpful during the configure step for implementations to set a default port - * if none was configured by the user. - * - * @return true if an httpPort was set, false otherwise. - */ - protected boolean isHttpPortSet() { - return Objects.nonNull(httpPort); - } - // Abstract /** @@ -329,7 +316,7 @@ protected boolean isHttpPortSet() { * * @return - the application install directory */ - abstract protected String getApplicationInstallDirectory(); + protected abstract String getApplicationInstallDirectory(); // Helpers @@ -338,7 +325,7 @@ protected boolean isHttpPortSet() { * * @return - The temporary directory path */ - protected static Path getTempDirectory() { + protected static Path createTempDirectory() { if( Objects.nonNull(tempDirectory) ) { return tempDirectory; } 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 index a4514384ed6..d3e3086d07d 100644 --- 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 @@ -5,6 +5,7 @@ import org.testcontainers.utility.DockerImageName; import java.time.Duration; +import java.util.Arrays; import java.util.List; import java.util.concurrent.Future; @@ -39,7 +40,7 @@ public void testNormalizePath() { public void httpPortMapping() { List expected, actual; - expected = List.of(8080, 9080, 9443); + expected = Arrays.asList(8080, 9080, 9443); // Test expose ports, then add httpPort testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); @@ -60,7 +61,7 @@ public void httpPortMapping() { //Test httpPort then set exposed ports testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withHttpPort(8080); - testContainer.setExposedPorts(List.of(9080, 9443)); + testContainer.setExposedPorts(Arrays.asList(9080, 9443)); actual = testContainer.getExposedPorts(); assertThat(actual).containsExactlyElementsOf(expected); diff --git a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java index 45246b8ab5a..32e9c0f75f5 100644 --- a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java +++ b/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java @@ -1,16 +1,11 @@ package org.testcontainers.containers; import lombok.NonNull; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import org.testcontainers.applicationserver.ApplicationServerContainer; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -22,19 +17,17 @@ */ public class LibertyServerContainer extends ApplicationServerContainer { - public static final String NAME = "Liberty"; - // About the image - public static final String IMAGE = "open-liberty"; + static final String IMAGE = "open-liberty"; - private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); + static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE); // Container defaults - public static final int DEFAULT_HTTP_PORT = 9080; + static final int DEFAULT_HTTP_PORT = 9080; - public static final int DEFAULT_HTTPS_PORT = 9443; + static final int DEFAULT_HTTPS_PORT = 9443; - private static final Duration DEFAULT_WAIT_TIMEOUT = Duration.ofSeconds(30); + static final Duration DEFAULT_WAIT_TIMEOUT = Duration.ofSeconds(30); private static final String SERVER_CONFIG_DIR = "/config/"; @@ -43,7 +36,7 @@ public class LibertyServerContainer extends ApplicationServerContainer { private static final List DEFAULT_FEATURES = Arrays.asList("webProfile-10.0"); // Container fields - private MountableFile serverConfiguration; + private Transferable serverConfiguration; private List features = new ArrayList<>(); @@ -74,16 +67,16 @@ public void configure() { // Copy server configuration if( Objects.nonNull(serverConfiguration) ) { - withCopyFileToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); + withCopyToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); return; } if ( ! features.isEmpty() ) { - withCopyFileToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml"); + withCopyToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml"); return; } - withCopyFileToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml"); + withCopyToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml"); } @@ -122,7 +115,7 @@ public LibertyServerContainer withFeatures(String... features) { // Helpers - private static final MountableFile generateServerConfiguration(List features) { + private static final Transferable generateServerConfiguration(List features) { String configContents = ""; configContents += ""; for(String feature : features) { @@ -131,15 +124,6 @@ private static final MountableFile generateServerConfiguration(List feat configContents += ""; configContents += System.lineSeparator(); - Path generatedConfigPath = Paths.get(getTempDirectory().toString(), "generatedServer.xml"); - - try { - Files.write(generatedConfigPath, configContents.getBytes(StandardCharsets.UTF_8), - StandardOpenOption.CREATE, StandardOpenOption.WRITE); - } catch (IOException ioe) { - throw new RuntimeException("Unable to generate server configuration at runtime", ioe); - } - - return MountableFile.forHostPath(generatedConfigPath); + return Transferable.of(configContents); } } diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java index 95b02eb2aeb..da1331c12fd 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java @@ -33,7 +33,7 @@ public class LibertyContainerTest { .withTag("23.0.0.3-full-java17-openj9"); private static ApplicationServerContainer testContainer = new LibertyServerContainer(libertyImage) - .withArchvies(createDeployment()) + .withArchives(createDeployment()) .withAppContextRoot("test/app/service/") .withLazyEnv("mock.port", () -> "" + dependantService.getMappedPort(80)) .dependsOn(dependantService); From a6566549b28d7bf9bfba8a0f9123a9ea94435a46 Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Mon, 19 Jun 2023 10:42:16 -0500 Subject: [PATCH 7/8] spotless --- .../ApplicationServerContainer.java | 45 ++++++++++--------- .../ApplicationServerContainerTest.java | 10 +++-- 2 files changed, 29 insertions(+), 26 deletions(-) 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 index 82f1f6930d3..5710e8dc209 100644 --- 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 @@ -80,31 +80,32 @@ protected void configure() { // Setup default wait strategy waitingFor( - Wait.forHttp(readinessPath != null ? readinessPath : appContextRoot) + Wait + .forHttp(readinessPath != null ? readinessPath : appContextRoot) .forPort(readinessPort != null ? readinessPort : httpPort) .withStartupTimeout(httpWaitTimeout) ); // Copy applications - for(MountableFile archive : archives) { + for (MountableFile archive : archives) { withCopyFileToContainer(archive, getApplicationInstallDirectory() + extractApplicationName(archive)); } // Add lazy env variables - for(Map.Entry> entry : lazyEnvVars.entrySet()) { + for (Map.Entry> entry : lazyEnvVars.entrySet()) { withEnv(entry.getKey(), entry.getValue().get()); } } @Override protected void containerIsCreated(String containerId) { - if( Objects.isNull(tempDirectory) ) { + if (Objects.isNull(tempDirectory)) { return; } try { //Delete files in temp directory - for(String file : tempDirectory.toFile().list()) { + for (String file : tempDirectory.toFile().list()) { Files.deleteIfExists(Paths.get(tempDirectory.toString(), file)); } //Delete temp directory @@ -112,12 +113,11 @@ protected void containerIsCreated(String containerId) { } catch (IOException e) { logger().info("Unable to delete temporary directory " + tempDirectory.toString(), e); } - } @Override public void setExposedPorts(List exposedPorts) { - if( Objects.isNull(this.httpPort) ) { + if (Objects.isNull(this.httpPort)) { super.setExposedPorts(exposedPorts); return; } @@ -136,12 +136,14 @@ public void setExposedPorts(List exposedPorts) { * @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)); - }); + 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; } @@ -169,7 +171,7 @@ public ApplicationServerContainer withArchives(@NonNull MountableFile... archive * @return self */ public ApplicationServerContainer withHttpPort(int httpPort) { - if( Objects.nonNull(this.httpPort) && this.httpPort == this.readinessPort ) { + 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)); @@ -190,7 +192,7 @@ public ApplicationServerContainer withHttpPort(int httpPort) { * @return self */ public ApplicationServerContainer withAppContextRoot(@NonNull String appContextRoot) { - if( Objects.nonNull(this.appContextRoot) && this.appContextRoot == this.readinessPath ) { + if (Objects.nonNull(this.appContextRoot) && this.appContextRoot == this.readinessPath) { this.readinessPath = this.appContextRoot = normalizePath(appContextRoot); } else { this.appContextRoot = normalizePath(appContextRoot); @@ -258,7 +260,7 @@ public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) * .withLazyEnv( "oracle.url", () -> db.getJdbcUrl() ) // Need to wait until 'db' container is configured * .dependsOn(db); * - * + * * @param key - the key * @param valueSupplier - the function that supplies the value * @return self @@ -278,7 +280,7 @@ public ApplicationServerContainer withLazyEnv(String key, Supplier value * @return - The readiness URL */ public String getReadinessURL() { - if ( Objects.isNull(this.readinessPath) || Objects.isNull(this.readinessPort) ) { + if (Objects.isNull(this.readinessPath) || Objects.isNull(this.readinessPort)) { return getApplicationURL(); } @@ -326,7 +328,7 @@ public String getBaseURL() { * @return - The temporary directory path */ protected static Path createTempDirectory() { - if( Objects.nonNull(tempDirectory) ) { + if (Objects.nonNull(tempDirectory)) { return tempDirectory; } @@ -346,7 +348,7 @@ protected static Path createTempDirectory() { 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") ) { + if (path.matches(".*\\.jar|.*\\.war|.*\\.ear|.*\\.rar")) { return path.substring(path.lastIndexOf("/")); } throw new IllegalArgumentException("File did not contain an application archive"); @@ -366,7 +368,6 @@ protected static List appendPort(List portList, int appendingP 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. @@ -393,10 +394,10 @@ protected static List replacePort(List portList, Integer oldPo * @return The normalized path */ protected static String normalizePath(String... paths) { - return Stream.of(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 index d3e3086d07d..ec3894a244b 100644 --- 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 @@ -4,7 +4,6 @@ import org.junit.Test; import org.testcontainers.utility.DockerImageName; -import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.concurrent.Future; @@ -43,7 +42,8 @@ public void httpPortMapping() { expected = Arrays.asList(8080, 9080, 9443); // Test expose ports, then add httpPort - testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withExposedPorts(9080, 9443); testContainer.withHttpPort(8080); @@ -51,7 +51,8 @@ public void httpPortMapping() { assertThat(actual).containsExactlyElementsOf(expected); // Test httpPort then expose ports - testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withHttpPort(8080); testContainer.withExposedPorts(9080, 9443); @@ -59,7 +60,8 @@ public void httpPortMapping() { assertThat(actual).containsExactlyElementsOf(expected); //Test httpPort then set exposed ports - testContainer = new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); + testContainer = + new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9")); testContainer.withHttpPort(8080); testContainer.setExposedPorts(Arrays.asList(9080, 9443)); From ee4c13fb1167995ed3bd8560dac2e5f0e6cfc776 Mon Sep 17 00:00:00 2001 From: Kyle Aure Date: Fri, 30 Jun 2023 15:26:58 -0500 Subject: [PATCH 8/8] More feedback --- docs/modules/liberty.md | 56 ++++++++ .../ApplicationServerContainer.java | 32 ----- modules/liberty/build.gradle | 1 + .../LibertyServerContainer.java | 11 +- .../containers/LibertyContainerTest.java | 106 -------------- .../liberty/LibertyContainerTest.java | 132 ++++++++++++++++++ .../{containers => liberty}/app/TestApp.java | 5 +- .../liberty/app/TestResource.java | 37 +++++ .../app/TestService.java | 3 +- .../src/test/resources/expectation.json | 10 ++ 10 files changed, 245 insertions(+), 148 deletions(-) create mode 100644 docs/modules/liberty.md rename modules/liberty/src/main/java/org/testcontainers/{containers => liberty}/LibertyServerContainer.java (95%) delete mode 100644 modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java create mode 100644 modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java rename modules/liberty/src/test/java/org/testcontainers/{containers => liberty}/app/TestApp.java (54%) create mode 100644 modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java rename modules/liberty/src/test/java/org/testcontainers/{containers => liberty}/app/TestService.java (97%) create mode 100644 modules/liberty/src/test/resources/expectation.json 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/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java index 5710e8dc209..0190a4beb94 100644 --- 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 @@ -18,12 +18,9 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.Future; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -59,9 +56,6 @@ public abstract class ApplicationServerContainer extends GenericContainer> lazyEnvVars = new HashMap<>(); - //Constructors public ApplicationServerContainer(@NonNull final Future image) { @@ -90,11 +84,6 @@ protected void configure() { for (MountableFile archive : archives) { withCopyFileToContainer(archive, getApplicationInstallDirectory() + extractApplicationName(archive)); } - - // Add lazy env variables - for (Map.Entry> entry : lazyEnvVars.entrySet()) { - withEnv(entry.getKey(), entry.getValue().get()); - } } @Override @@ -249,27 +238,6 @@ public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) return this; } - /** - * An environment variable whose value needs to wait until the configuration stage to be evaluated. - * The common use case for this would be to pass inter-container connection data. - * - * For example an application container often needs to have connection data to a dependent database: - *
-     *     OracleContainer db = new OracleContainer(...).withExposedPorts(1521);
-     *     ApplicationContainer app = new ApplicationContainer(...)
-     *       .withLazyEnv( "oracle.url", () -> db.getJdbcUrl() ) // Need to wait until 'db' container is configured
-     *       .dependsOn(db);
-     * 
- * - * @param key - the key - * @param valueSupplier - the function that supplies the value - * @return self - */ - public ApplicationServerContainer withLazyEnv(String key, Supplier valueSupplier) { - lazyEnvVars.put(key, valueSupplier); - return this; - } - //Getters /** diff --git a/modules/liberty/build.gradle b/modules/liberty/build.gradle index 4b50b748e8e..a797a1af727 100644 --- a/modules/liberty/build.gradle +++ b/modules/liberty/build.gradle @@ -7,6 +7,7 @@ dependencies { 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/containers/LibertyServerContainer.java b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java similarity index 95% rename from modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java rename to modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java index 32e9c0f75f5..6d84b0a230b 100644 --- a/modules/liberty/src/main/java/org/testcontainers/containers/LibertyServerContainer.java +++ b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java @@ -1,10 +1,10 @@ -package org.testcontainers.containers; +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 org.testcontainers.applicationserver.ApplicationServerContainer; import java.time.Duration; import java.util.ArrayList; @@ -66,18 +66,17 @@ public void configure() { super.configure(); // Copy server configuration - if( Objects.nonNull(serverConfiguration) ) { + if (Objects.nonNull(serverConfiguration)) { withCopyToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml"); return; } - if ( ! features.isEmpty() ) { + if (!features.isEmpty()) { withCopyToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml"); return; } withCopyToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml"); - } @Override @@ -118,7 +117,7 @@ public LibertyServerContainer withFeatures(String... features) { private static final Transferable generateServerConfiguration(List features) { String configContents = ""; configContents += ""; - for(String feature : features) { + for (String feature : features) { configContents += "" + feature + ""; } configContents += ""; diff --git a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java deleted file mode 100644 index da1331c12fd..00000000000 --- a/modules/liberty/src/test/java/org/testcontainers/containers/LibertyContainerTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.testcontainers.containers; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.jboss.shrinkwrap.api.Archive; -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.utility.DockerImageName; - -import static org.assertj.core.api.Assertions.assertThat; - -import static io.restassured.RestAssured.given; - -public class LibertyContainerTest { - - /* - * Typically application servers have a dependency on a database or other microservice during integration testing - * Use a mockServer to ensure dependsOn and lazy env variables work. - */ - public static final DockerImageName mockImage = DockerImageName - .parse("mockserver/mockserver") - .withTag("mockserver-5.15.0"); - - private static GenericContainer dependantService = new GenericContainer<>(mockImage) - .withExposedPorts(80,81); - - //The liberty test container - private static final DockerImageName libertyImage = DockerImageName.parse(LibertyServerContainer.IMAGE) - .withTag("23.0.0.3-full-java17-openj9"); - - private static ApplicationServerContainer testContainer = new LibertyServerContainer(libertyImage) - .withArchives(createDeployment()) - .withAppContextRoot("test/app/service/") - .withLazyEnv("mock.port", () -> "" + dependantService.getMappedPort(80)) - .dependsOn(dependantService); - - /** - * Creates a deployment using Shrinkwrap at runtime. - * - * @return - the application archive - */ - private static Archive createDeployment() { - return ShrinkWrap.create(WebArchive.class, "test.war") - .addPackage("org.testcontainers.containers.app"); - } - - @BeforeClass - public static void setup() { - testContainer.start(); - } - - @AfterClass - public static void teardown() { - testContainer.stop(); - } - - @Test - public void testURLs() { - String expectedURL, actualURL; - - expectedURL = "http://" + testContainer.getHost() + ":" + testContainer.getMappedPort(LibertyServerContainer.DEFAULT_HTTP_PORT); - actualURL = testContainer.getBaseURL(); - assertThat(actualURL).isEqualTo(expectedURL); - - expectedURL += "/test/app/service"; - actualURL = testContainer.getApplicationURL(); - assertThat(actualURL).isEqualTo(expectedURL); - - actualURL = testContainer.getReadinessURL(); - assertThat(actualURL).isEqualTo(expectedURL); - } - - @Test - public void testRestEndpoint() { - RequestSpecification request = new RequestSpecBuilder() - .setBaseUri(testContainer.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); - } -} 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/containers/app/TestApp.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java similarity index 54% rename from modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java rename to modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java index 05d712091fe..87e62f8d948 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/app/TestApp.java +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java @@ -1,8 +1,7 @@ -package org.testcontainers.containers.app; +package org.testcontainers.liberty.app; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("/app") -public class TestApp extends Application { -} +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/containers/app/TestService.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java similarity index 97% rename from modules/liberty/src/test/java/org/testcontainers/containers/app/TestService.java rename to modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java index 6208f27fd65..e0b6e852311 100644 --- a/modules/liberty/src/test/java/org/testcontainers/containers/app/TestService.java +++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers.app; +package org.testcontainers.liberty.app; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -17,6 +17,7 @@ @Produces(MediaType.TEXT_PLAIN) @Consumes(MediaType.TEXT_PLAIN) public class TestService { + private static final List cache = new ArrayList<>(); @GET 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!" + } + } +]