diff --git a/README.md b/README.md index ab001d6537b..22c800f03be 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.testcontainers/testcontainers/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.testcontainers/testcontainers) +[![Netlify Status](https://api.netlify.com/api/v1/badges/189f28a2-7faa-42ff-b03c-738142079cc9/deploy-status)](https://app.netlify.com/sites/testcontainers/deploys) + [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=33816473&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.testcontainers.org/scans) diff --git a/build.gradle b/build.gradle index af1f408b35e..6cc5c2b8fcf 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { plugins { id 'io.franzbecker.gradle-lombok' version '5.0.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.gradleup.shadow' version '8.3.0' id 'me.champeau.gradle.japicmp' version '0.4.3' apply false id 'com.diffplug.spotless' version '6.13.0' apply false } diff --git a/core/build.gradle b/core/build.gradle index 10368b59f1f..a4e57d9473b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' description = "Testcontainers Core" diff --git a/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java b/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java index 71aba726dda..8fc662a118e 100644 --- a/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java +++ b/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java @@ -37,9 +37,11 @@ public void setUp() { @Test public void testProfileOption() { try ( + // composeContainerWithLocalCompose { ComposeContainer compose = new ComposeContainer(COMPOSE_FILE) - .withOptions("--profile=cache") .withLocalCompose(true) + // } + .withOptions("--profile=cache") ) { compose.start(); assertThat(compose.listChildContainers()).hasSize(1); diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java index c101da6b6b3..39c7ff2baf5 100644 --- a/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java @@ -18,21 +18,30 @@ public class ComposeContainerTest extends BaseComposeTest { @Rule + // composeContainerConstructor { public ComposeContainer environment = new ComposeContainer( new File("src/test/resources/composev2/compose-test.yml") ) .withExposedService("redis-1", REDIS_PORT) .withExposedService("db-1", 3306); + // } + @Override protected ComposeContainer getEnvironment() { return environment; } @Test - public void testGetServicePort() { + public void testGetServiceHostAndPort() { + // getServiceHostAndPort { + String serviceHost = environment.getServiceHost("redis-1", REDIS_PORT); int serviceWithInstancePort = environment.getServicePort("redis-1", REDIS_PORT); + // } + + assertThat(serviceHost).as("Service host is not blank").isNotBlank(); assertThat(serviceWithInstancePort).as("Port is set for service with instance number").isNotNull(); + int serviceWithoutInstancePort = environment.getServicePort("redis", REDIS_PORT); assertThat(serviceWithoutInstancePort).as("Port is set for service with instance number").isNotNull(); assertThat(serviceWithoutInstancePort).as("Service ports are the same").isEqualTo(serviceWithInstancePort); diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java index 390ddd505fb..2b1e2cd4081 100644 --- a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java @@ -49,11 +49,13 @@ public void testWithFileCopyInclusionUsingFilePath() throws IOException { @Test public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException { try ( + // composeContainerWithCopyFiles { ComposeContainer environment = new ComposeContainer( new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test") + // } ) { environment.start(); diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java new file mode 100644 index 00000000000..1304413dce5 --- /dev/null +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java @@ -0,0 +1,54 @@ +package org.testcontainers.junit; + +import org.junit.Test; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComposeContainerWithWaitStrategies { + + private static final int REDIS_PORT = 6379; + + @Test + public void testComposeContainerConstructor() { + try ( + // composeContainerWithCombinedWaitStrategies { + ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml")) + .withExposedService("redis-1", REDIS_PORT, Wait.forSuccessfulCommand("redis-cli ping")) + .withExposedService("db-1", 3306, Wait.forLogMessage(".*ready for connections.*\\n", 1)) + // } + ) { + compose.start(); + containsStartedServices(compose, "redis-1", "db-1"); + } + } + + @Test + public void testComposeContainerWaitForPortWithTimeout() { + try ( + // composeContainerWaitForPortWithTimeout { + ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml")) + .withExposedService( + "redis-1", + REDIS_PORT, + Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)) + ) + // } + ) { + compose.start(); + containsStartedServices(compose, "redis-1"); + } + } + + private void containsStartedServices(ComposeContainer compose, String... expectedServices) { + for (String serviceName : expectedServices) { + assertThat(compose.getContainerByServiceName(serviceName)) + .as("Container should be found by service name %s", serviceName) + .isPresent(); + } + } +} diff --git a/docs/modules/docker_compose.md b/docs/modules/docker_compose.md index 9032e7c6ee2..2ac45912479 100644 --- a/docs/modules/docker_compose.md +++ b/docs/modules/docker_compose.md @@ -2,161 +2,121 @@ ## Benefits -Similar to generic containers support, it's also possible to run a bespoke set of services -specified in a `docker-compose.yml` file. +Similar to generic container support, it's also possible to run a bespoke set of services specified in a +`docker-compose.yml` file. -This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define -services that an application may be dependent upon. +This is especially useful for projects where Docker Compose is already used in development +or other environments to define services that an application may be dependent upon. -Behind the scenes, Testcontainers actually launches a temporary Docker Compose client - in a container, of course, so -it's not necessary to have it installed on all developer/test machines. +The `ComposeContainer` leverages [Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/), +making it easy to use the same dependencies from the development environment within tests. ## Example -A single class rule, pointing to a `docker-compose.yml` file, should be sufficient to launch any number of services -required by your tests: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT); -``` +A single class `ComposeContainer`, defined based on a `docker-compose.yml` file, +should be sufficient to launch any number of services required by our tests: + + +[Create a ComposeContainer](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:composeContainerConstructor + -In this example, `compose-test.yml` should have content such as: +!!! note + Make sure the service names use a `-` rather than `_` as separator. + +In this example, Docker Compose file should have content such as: ```yaml -redis: - image: redis -elasticsearch: - image: elasticsearch +services: + redis: + image: redis + db: + image: mysql:8.0.36 ``` -Note that it is not necessary to define ports to be exposed in the YAML file; this would inhibit reuse/inclusion of the -file in other contexts. +Note that it is not necessary to define ports to be exposed in the YAML file, +as this would inhibit the reuse/inclusion of the file in other contexts. + +Instead, Testcontainers will spin up a small `ambassador` container, +which will proxy between the Compose-managed containers and ports that are accessible to our tests. -Instead, Testcontainers will spin up a small 'ambassador' container, which will proxy -between the Compose-managed containers and ports that are accessible to your tests. This is done using a separate, minimal -container that runs socat as a TCP proxy. +## ComposeContainer vs DockerComposeContainer -## Accessing a container from tests +So far, we discussed `ComposeContainer`, which supports docker compose [version 2](https://www.docker.com/blog/announcing-compose-v2-general-availability/). -The rule provides methods for discovering how your tests can interact with the containers: +On the other hand, `DockerComposeContainer` utilizes Compose V1, which has been marked deprecated by Docker. + +The two APIs are quite similar, and most examples provided on this page can be applied to both of them. + +## Accessing a Container + +`ComposeContainer` provides methods for discovering how your tests can interact with the containers: * `getServiceHost(serviceName, servicePort)` returns the IP address where the container is listening (via an ambassador container) * `getServicePort(serviceName, servicePort)` returns the Docker mapped port for a port that has been exposed (via an ambassador container) -For example, with the Redis example above, the following will allow your tests to access the Redis service: -```java -String redisUrl = environment.getServiceHost("redis_1", REDIS_PORT) - + ":" + - environment.getServicePort("redis_1", REDIS_PORT); -``` +Let's use this API to create the URL that will enable our tests to access the Redis service: + +[Access a Service's host and port](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:getServiceHostAndPort + -## Startup timeout +## Wait Strategies and Startup Timeouts Ordinarily Testcontainers will wait for up to 60 seconds for each exposed container's first mapped network port to start listening. - This simple measure provides a basic check whether a container is ready for use. -There are overloaded `withExposedService` methods that take a `WaitStrategy` so you can specify a timeout strategy per container. +There are overloaded `withExposedService` methods that take a `WaitStrategy` +where we can specify a timeout strategy per container. -### Waiting for startup examples +We can either use the fluent API to crate a [custom strategy](../features/startup_and_waits.md) or use one of the already existing ones, +accessible via the static factory methods from of the `Wait` class. -Waiting for exposed port to start listening: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, - Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30))); -``` +For instance, we can wait for exposed port and set a custom timeout: + +[Wait for the exposed port and use a custom timeout](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWaitForPortWithTimeout + -Wait for arbitrary status codes on an HTTPS endpoint: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +Needless to say, we can define different strategies for each service in our Docker Compose setup. -Separate wait strategies for each container: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +For example, our Redis container can wait for a successful redis-cli command, +while our db service waits for a specific log message: -Alternatively, you can use `waitingFor(serviceName, waitStrategy)`, -for example if you need to wait on a log message from a service, but don't need to expose a port. + +[Wait for a custom command and a log message](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWithCombinedWaitStrategies + -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)); -``` -## 'Local compose' mode -You can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -This will generally yield an experience that is closer to running docker-compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)) - .withLocalCompose(true); -``` +## The 'Local Compose' Mode -## Compose V2 +We can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -[Compose V2 is GA](https://www.docker.com/blog/announcing-compose-v2-general-availability/) and it relies on the `docker` command itself instead of `docker-compose`. -Testcontainers provides `ComposeContainer` if you want to use Compose V2. +This will generally yield an experience that is closer to running _docker compose_ locally, +with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static ComposeContainer environment = - new ComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db-1", Wait.forLogMessage("started", 1)); -``` - -!!! note - Make sure the service name use a `-` instead of `_` as separator using `ComposeContainer`. + +[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java) inside_block:composeContainerWithLocalCompose + -## Build working directory +## Build Working Directory -You can select what files should be copied only via `withCopyFilesInContainer`: +We can select what files should be copied only via `withCopyFilesInContainer`: -```java -public static ComposeContainer environment = - new ComposeContainer(new File("compose.yml")) - .withCopyFilesInContainer(".env"); -``` + +[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java) inside_block:composeContainerWithCopyFiles + -In this example, only `compose.yml` and `.env` are copied over into the container that will run the Docker Compose file. +In this example, only docker compose and env files are copied over into the container that will run the Docker Compose file. By default, all files in the same directory as the compose file are copied over. -This can be used with `DockerComposeContainer` and `ComposeContainer`. -You can use file and directory references. +We can use file and directory references. They are always resolved relative to the directory where the compose file resides. !!! note - This only work with containarized Compose, not with `Local Compose` mode. + This can be used with `DockerComposeContainer` and `ComposeContainer`, but **only in the containerized Compose (not with `Local Compose` mode)**. ## Using private repositories in Docker compose -When Docker Compose is used in container mode (not local), it's needs to be made aware of Docker settings for private repositories. +When Docker Compose is used in container mode (not local), it needs to be made aware of Docker +settings for private repositories. By default, those setting are located in `$HOME/.docker/config.json`. There are 3 ways to specify location of the `config.json` for Docker Compose: diff --git a/gradle/shading.gradle b/gradle/shading.gradle index fc7f6587b3e..10ceb5086a4 100644 --- a/gradle/shading.gradle +++ b/gradle/shading.gradle @@ -1,6 +1,6 @@ import java.util.jar.JarFile -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' configurations { shaded @@ -30,7 +30,7 @@ project.afterEvaluate { return it.dependencyProject.tasks.findByName("shadowJar")?.relocators ?: [] } - // See https://github.com/johnrengelman/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy + // See https://github.com/GradleUp/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy Set packages = [] for (artifact in project.configurations.shaded.resolvedConfiguration.resolvedArtifacts) { diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java index e68ecce405b..8a100855b3c 100644 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java +++ b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java @@ -15,6 +15,7 @@ import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.data.v2.models.TableId; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.Rule; @@ -23,7 +24,6 @@ import java.io.IOException; import java.util.List; -import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -43,28 +43,26 @@ public class BigtableEmulatorContainerTest { @Test // testWithEmulatorContainer { - public void testSimple() throws IOException, InterruptedException, ExecutionException { + public void testSimple() throws IOException { ManagedChannel channel = ManagedChannelBuilder.forTarget(emulator.getEmulatorEndpoint()).usePlaintext().build(); TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( GrpcTransportChannel.create(channel) ); NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create(); - - try { - createTable(channelProvider, credentialsProvider, "test-table"); - + createTable(channelProvider, credentialsProvider, "test-table"); + try ( BigtableDataClient client = BigtableDataClient.create( BigtableDataSettings .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort()) .setProjectId(PROJECT_ID) .setInstanceId(INSTANCE_ID) .build() - ); - - client.mutateRow(RowMutation.create("test-table", "1").setCell("name", "firstName", "Ray")); + ) + ) { + client.mutateRow(RowMutation.create(TableId.of("test-table"), "1").setCell("name", "firstName", "Ray")); - Row row = client.readRow("test-table", "1"); + Row row = client.readRow(TableId.of("test-table"), "1"); List cells = row.getCells("name", "firstName"); assertThat(cells).isNotNull().hasSize(1); diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java index 5246fc6b8ad..c704ce15d41 100644 --- a/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java +++ b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java @@ -24,12 +24,16 @@ public class AbstractPulsar { protected void testPulsarFunctionality(String pulsarBrokerUrl) throws Exception { try ( PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).build(); - Consumer consumer = client.newConsumer().topic(TEST_TOPIC).subscriptionName("test-subs").subscribe(); + Consumer consumer = client + .newConsumer() + .topic(TEST_TOPIC) + .subscriptionName("test-subs") + .subscribe(); Producer producer = client.newProducer().topic(TEST_TOPIC).create() ) { producer.send("test containers".getBytes()); - CompletableFuture future = consumer.receiveAsync(); - Message message = future.get(5, TimeUnit.SECONDS); + CompletableFuture> future = consumer.receiveAsync(); + Message message = future.get(5, TimeUnit.SECONDS); assertThat(new String(message.getData())).isEqualTo("test containers"); } diff --git a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java index bac998b85ac..9eff3fa83f2 100644 --- a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java +++ b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java @@ -9,7 +9,6 @@ import org.testcontainers.utility.MountableFile; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -76,14 +75,16 @@ public RabbitMQContainer(final DockerImageName dockerImageName) { addExposedPorts(DEFAULT_AMQP_PORT, DEFAULT_AMQPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); - this.waitStrategy = - Wait.forLogMessage(".*Server startup complete.*", 1).withStartupTimeout(Duration.ofSeconds(60)); + waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)); } @Override protected void configure() { - if (adminPassword != null) { - addEnv("RABBITMQ_DEFAULT_PASS", adminPassword); + if (this.adminUsername != null) { + addEnv("RABBITMQ_DEFAULT_USER", this.adminUsername); + } + if (this.adminPassword != null) { + addEnv("RABBITMQ_DEFAULT_PASS", this.adminPassword); } } @@ -105,11 +106,14 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * @return The admin password for the admin account */ public String getAdminPassword() { - return adminPassword; + return this.adminPassword; } + /** + * @return The admin user for the admin account + */ public String getAdminUsername() { - return adminUsername; + return this.adminUsername; } public Integer getAmqpPort() { @@ -156,6 +160,17 @@ public String getHttpsUrl() { return "https://" + getHost() + ":" + getHttpsPort(); } + /** + * Sets the user for the admin (default is
guest
) + * + * @param adminUsername The admin user. + * @return This container. + */ + public RabbitMQContainer withAdminUser(final String adminUsername) { + this.adminUsername = adminUsername; + return this; + } + /** * Sets the password for the admin (default is
guest
) * diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java index d26b4d6e924..728b2622b17 100644 --- a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java +++ b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java @@ -5,13 +5,15 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DeliverCallback; import org.junit.Test; import org.testcontainers.containers.RabbitMQContainer.SslVerification; import org.testcontainers.utility.MountableFile; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -19,6 +21,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Collections; +import java.util.concurrent.TimeoutException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -40,32 +43,19 @@ public class RabbitMQContainerTest { @Test public void shouldCreateRabbitMQContainer() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { + container.start(); + assertThat(container.getAdminPassword()).isEqualTo("guest"); assertThat(container.getAdminUsername()).isEqualTo("guest"); - container.start(); - assertThat(container.getAmqpsUrl()) - .isEqualTo( - String.format("amqps://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQPS_PORT)) - ); + .isEqualTo(String.format("amqps://%s:%d", container.getHost(), container.getAmqpsPort())); assertThat(container.getAmqpUrl()) - .isEqualTo( - String.format("amqp://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQP_PORT)) - ); + .isEqualTo(String.format("amqp://%s:%d", container.getHost(), container.getAmqpPort())); assertThat(container.getHttpsUrl()) - .isEqualTo( - String.format("https://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTPS_PORT)) - ); + .isEqualTo(String.format("https://%s:%d", container.getHost(), container.getHttpsPort())); assertThat(container.getHttpUrl()) - .isEqualTo( - String.format("http://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTP_PORT)) - ); - - assertThat(container.getHttpsPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTPS_PORT)); - assertThat(container.getHttpPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTP_PORT)); - assertThat(container.getAmqpsPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQPS_PORT)); - assertThat(container.getAmqpPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQP_PORT)); + .isEqualTo(String.format("http://%s:%d", container.getHost(), container.getHttpPort())); assertThat(container.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder( @@ -74,6 +64,24 @@ public void shouldCreateRabbitMQContainer() { container.getMappedPort(DEFAULT_HTTP_PORT), container.getMappedPort(DEFAULT_HTTPS_PORT) ); + + assertFunctionality(container); + } + } + + @Test + public void shouldCreateRabbitMQContainerWithCustomCredentials() { + try ( + RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE) + .withAdminUser("admin") + .withAdminPassword("admin") + ) { + container.start(); + + assertThat(container.getAdminPassword()).isEqualTo("admin"); + assertThat(container.getAdminUsername()).isEqualTo("admin"); + + assertFunctionality(container); } } @@ -283,7 +291,7 @@ private SSLContext createSslContext( KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load( - new FileInputStream(new File(classLoader.getResource(keystoreFile).getFile())), + Files.newInputStream(new File(classLoader.getResource(keystoreFile).getFile()).toPath()), keystorePassword.toCharArray() ); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); @@ -291,7 +299,7 @@ private SSLContext createSslContext( KeyStore trustStore = KeyStore.getInstance("PKCS12"); trustStore.load( - new FileInputStream(new File(classLoader.getResource(truststoreFile).getFile())), + Files.newInputStream(new File(classLoader.getResource(truststoreFile).getFile()).toPath()), truststorePassword.toCharArray() ); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); @@ -301,4 +309,27 @@ private SSLContext createSslContext( c.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); return c; } + + private void assertFunctionality(RabbitMQContainer container) { + String queueName = "test-queue"; + String text = "Hello World!"; + + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(container.getHost()); + factory.setPort(container.getAmqpPort()); + factory.setUsername(container.getAdminUsername()); + factory.setPassword(container.getAdminPassword()); + try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { + channel.queueDeclare(queueName, false, false, false, null); + channel.basicPublish("", queueName, null, text.getBytes()); + + DeliverCallback deliverCallback = (consumerTag, delivery) -> { + String message = new String(delivery.getBody(), StandardCharsets.UTF_8); + assertThat(message).isEqualTo(text); + }; + channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {}); + } catch (IOException | TimeoutException e) { + throw new RuntimeException(e); + } + } } diff --git a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java index 691950f61dc..18aca269235 100644 --- a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java +++ b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java @@ -104,7 +104,7 @@ public int getZookeeperPort() { @SneakyThrows protected void configure() { if (configuration.getSolrSchema() != null && configuration.getSolrConfiguration() == null) { - throw new IllegalStateException("Solr needs to have a configuration is you want to use a schema"); + throw new IllegalStateException("Solr needs to have a configuration if you want to use a schema"); } // Generate Command Builder String command = "solr -f";