diff --git a/modules/mongodb/build.gradle b/modules/mongodb/build.gradle index 57db1be21de..dfc1432089b 100644 --- a/modules/mongodb/build.gradle +++ b/modules/mongodb/build.gradle @@ -4,4 +4,5 @@ dependencies { compile project(':testcontainers') testCompile("org.mongodb:mongodb-driver-sync:4.0.2") + testCompile("org.assertj:assertj-core:3.19.0") } diff --git a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java index 662c4b575f7..ff9ec4875ec 100644 --- a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java +++ b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java @@ -8,6 +8,8 @@ import org.testcontainers.utility.DockerImageName; import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Constructs a single node MongoDB replica set for testing transactions. @@ -23,6 +25,7 @@ public class MongoDBContainer extends GenericContainer { private static final int MONGODB_INTERNAL_PORT = 27017; private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60; private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; + private static final String DOCKER_ENTRYPOINT_INIT_DIR = "docker-entrypoint-initdb.d"; /** * @deprecated use {@link MongoDBContainer(DockerImageName)} instead @@ -64,9 +67,7 @@ public String getReplicaSetUrl() { * @return a replica set url. */ public String getReplicaSetUrl(final String databaseName) { - if (!isRunning()) { - throw new IllegalStateException("MongoDBContainer should be started first"); - } + checkIfRunning(); return String.format( "mongodb://%s:%d/%s", getContainerIpAddress(), @@ -75,6 +76,34 @@ public String getReplicaSetUrl(final String databaseName) { ); } + private void checkIfRunning() { + if (!isRunning()) { + throw new IllegalStateException("MongoDBContainer should be started first"); + } + } + + /** + * Loads and executes JavaScript files directly. + * Note that all the files are supposed to be delivered to a container via proper commands before starting. + * Should be used as an alternative to docker-entrypoint-initdb.d. This is because at docker-entrypoint-initdb.d + * stage, a replica set is not yet initialized and thus cannot accept operations. + * + * @param paths directory or file names as an array of strings. + * @see GenericContainer#withClasspathResourceMapping(String, String, BindMode) etc. + */ + @SneakyThrows(value = {IOException.class, InterruptedException.class}) + public void loadAndExecuteJsFiles(final String... paths) { + checkIfRunning(); + final String loadCommand = + Stream.of(paths).map(it -> "load(\"" + it + "\")").collect(Collectors.joining(";")); + final ExecResult execResult = execInContainer(buildMongoEvalCommand(loadCommand)); + if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) { + final String errorMessage = String.format("An error occurred: %s", execResult.getStdout()); + log.error(errorMessage); + throw new LoadAndExecuteJsFilesException(errorMessage); + } + } + @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { initReplicaSet(); @@ -92,6 +121,28 @@ private void checkMongoNodeExitCode(final Container.ExecResult execResult) { } } + @SneakyThrows(value = {IOException.class, InterruptedException.class}) + private void checkDockerEntrypointDirIsEmpty() { + final ExecResult execResult = execInContainer( + "/bin/bash", + "-c", + String.format( + "if [ -n \"$(find \"%s\" -maxdepth 0 -type d -empty 2>/dev/null)\" ]; then exit 0; else exit -1; fi", + DOCKER_ENTRYPOINT_INIT_DIR + ) + ); + if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) { + throw new DockerEntrypointInitDirIsNotEmptyException( + String.format( + "%s is supposed to be empty while running with the --replSet command-line option. " + + "Consider using loadAndExecuteJsFiles(...). Error: %s", + DOCKER_ENTRYPOINT_INIT_DIR, + execResult.getStdout() + ) + ); + } + } + private String buildMongoWaitCommand() { return String.format( "var attempt = 0; " + @@ -122,6 +173,7 @@ private void checkMongoNodeExitCodeAfterWaiting( @SneakyThrows(value = {IOException.class, InterruptedException.class}) private void initReplicaSet() { + checkDockerEntrypointDirIsEmpty(); log.debug("Initializing a single node node replica set..."); final ExecResult execResultInitRs = execInContainer( buildMongoEvalCommand("rs.initiate();") @@ -146,4 +198,16 @@ public static class ReplicaSetInitializationException extends RuntimeException { super(errorMessage); } } + + public static class DockerEntrypointInitDirIsNotEmptyException extends RuntimeException { + DockerEntrypointInitDirIsNotEmptyException(final String errorMessage) { + super(errorMessage); + } + } + + public static class LoadAndExecuteJsFilesException extends RuntimeException { + LoadAndExecuteJsFilesException(final String errorMessage) { + super(errorMessage); + } + } } diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java index ff5243acea7..d0b8b2e5785 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java @@ -13,7 +13,10 @@ import org.junit.Test; import org.testcontainers.utility.DockerImageName; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; @@ -95,4 +98,49 @@ public void shouldTestDatabaseName() { assertThat(mongoDBContainer.getReplicaSetUrl(databaseName), endsWith(databaseName)); } } + + @Test + public void shouldTestLoadAndExecuteJsFiles() { + final String file1 = "mongo_init-1.js"; + final String file2 = "mongo_init-2.js"; + final String targetDir2 = "my-scripts-2/"; + final String targetFile1 = "my-scripts-1/" + file1; + final String targetFile2 = targetDir2 + file2; + try ( + final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10")) + .withClasspathResourceMapping(file1, targetFile1, BindMode.READ_ONLY) + .withClasspathResourceMapping(file2, targetFile2, BindMode.READ_ONLY) + ) { + mongoDBContainer.start(); + mongoDBContainer.loadAndExecuteJsFiles(targetFile1, targetDir2); + final String mongoRsUrl = mongoDBContainer.getReplicaSetUrl(); + assertThat(mongoRsUrl, notNullValue()); + try ( + final MongoClient mongoSyncClient = MongoClients.create(mongoRsUrl) + ) { + assertThat( + mongoSyncClient.getDatabase("test").getCollection("foo").countDocuments(), + is(3L) + ); + assertThat( + mongoSyncClient.getDatabase("test").getCollection("bar").countDocuments(), + is(2L) + ); + } + assertThatThrownBy(() -> mongoDBContainer.loadAndExecuteJsFiles("non-existed-file")) + .isExactlyInstanceOf(MongoDBContainer.LoadAndExecuteJsFilesException.class); + } + } + + @Test + public void shouldNotStartBecauseOfDockerEntrypointInitDirectoryIsNotEmpty() { + try ( + final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10")) + .withClasspathResourceMapping("mongo_init-1.js", "/docker-entrypoint-initdb.d/mongo_init-1.js", BindMode.READ_ONLY) + ) { + assertThatThrownBy(mongoDBContainer::start) + .isExactlyInstanceOf(ContainerLaunchException.class) + .hasRootCauseExactlyInstanceOf(MongoDBContainer.DockerEntrypointInitDirIsNotEmptyException.class); + } + } } diff --git a/modules/mongodb/src/test/resources/mongo_init-1.js b/modules/mongodb/src/test/resources/mongo_init-1.js new file mode 100644 index 00000000000..b0a0853301d --- /dev/null +++ b/modules/mongodb/src/test/resources/mongo_init-1.js @@ -0,0 +1,4 @@ +db.foo.insert({'id': 1}); +db.foo.insert({'id': 2}); +db.foo.insert({'id': 3}); + diff --git a/modules/mongodb/src/test/resources/mongo_init-2.js b/modules/mongodb/src/test/resources/mongo_init-2.js new file mode 100644 index 00000000000..8d028c16a87 --- /dev/null +++ b/modules/mongodb/src/test/resources/mongo_init-2.js @@ -0,0 +1,3 @@ +db.bar.insert({'id': 1}); +db.bar.insert({'id': 2}); +