diff --git a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java index c61be6675ba..ca08c0d7223 100644 --- a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java +++ b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java @@ -6,10 +6,16 @@ import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; /** * Testcontainers implementation for MongoDB. @@ -41,10 +47,16 @@ public class MongoDBContainer extends GenericContainer { private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; + private static final String SCRIPT_DESTINATION_DEFAULT = "/docker-entrypoint-initdb.d/init.js"; + + private static final String SCRIPT_DESTINATION_MANUAL = "/tmp/init.js"; + private boolean shardingEnabled; private boolean rsEnabled; + private String initScriptPath; + public MongoDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } @@ -68,6 +80,69 @@ protected void containerIsStarted(InspectContainerResponse containerInfo, boolea if (this.rsEnabled) { initReplicaSet(reused); } + + boolean isClusterMode = this.shardingEnabled || this.rsEnabled; + + if (isClusterMode && this.initScriptPath != null) { + executeInitScriptInContainer(); + } + } + + /** + * Configures the container. + *

+ * This method handles the transfer of the initialization script to the container. + * Unlike standard file copying mechanisms, this implementation explicitly reads the script content as bytes + * and uses {@link org.testcontainers.images.builder.Transferable} to copy it. + *

+ * This approach is necessary to strictly support filenames containing special characters + * (e.g., "#", spaces, etc.) on the classpath. Standard resource loading methods may misinterpret + * these characters (e.g., treating "#" as a URL fragment), causing resolution failures. + * By manually resolving the file path and transferring the raw bytes, we ensure the script + * is correctly deployed regardless of its filename complexity. + */ + @Override + protected void configure() { + super.configure(); + boolean isClusterMode = this.shardingEnabled || this.rsEnabled; + if (this.initScriptPath != null) { + try { + Path scriptPath = Paths.get(this.initScriptPath); + String fileName = scriptPath.getFileName().toString(); + Path parentDir = scriptPath.getParent(); + String resourceDir = (parentDir == null) ? "" : parentDir.toString(); + + Enumeration resources = this.getClass().getClassLoader().getResources(resourceDir); + byte[] fileContent = null; + + while (resources.hasMoreElements()) { + URL dirUrl = resources.nextElement(); + + if ("file".equals(dirUrl.getProtocol())) { + Path dirPath = Paths.get(dirUrl.toURI()); + Path candidatePath = dirPath.resolve(fileName); + + if (Files.exists(candidatePath) && !Files.isDirectory(candidatePath)) { + fileContent = Files.readAllBytes(candidatePath); + break; + } + } + } + + if (fileContent == null) { + throw new RuntimeException("Could not find init script on classpath: " + this.initScriptPath); + } + + String destination = isClusterMode ? SCRIPT_DESTINATION_MANUAL : SCRIPT_DESTINATION_DEFAULT; + withCopyToContainer(Transferable.of(fileContent, 0777), destination); + } catch (Exception e) { + throw new RuntimeException("Failed to read or transfer init script: " + this.initScriptPath, e); + } + } + + if (this.initScriptPath != null && !isClusterMode) { + this.waitStrategy = Wait.forLogMessage("(?i).*waiting for connections.*", 2); + } } private String[] buildMongoEvalCommand(String command) { @@ -204,4 +279,46 @@ public String getReplicaSetUrl(String databaseName) { } return getConnectionString() + "/" + databaseName; } + + /** + * Executes a MongoDB initialization script from the classpath during startup. + *

+ * In standalone mode, the script will be copied to {@code /docker-entrypoint-initdb.d/init.js}, + * and the {@link org.testcontainers.containers.wait.strategy.WaitStrategy} is adjusted + * to expect the "waiting for connections" log message twice. + *

+ * In Replica Set or Sharding mode, the script is copied to a temporary location and executed + * manually after the cluster is initialized. + * + * @param scriptPath the path to the init script file on the classpath + * @return this container instance + */ + public MongoDBContainer withInitScript(String scriptPath) { + this.initScriptPath = scriptPath; + return this; + } + + @SneakyThrows + private void executeInitScriptInContainer() { + String cmd = + "mongosh " + + MONGODB_DATABASE_NAME_DEFAULT + + " " + + SCRIPT_DESTINATION_MANUAL + + " || mongo " + + MONGODB_DATABASE_NAME_DEFAULT + + " " + + SCRIPT_DESTINATION_MANUAL; + + ExecResult result = execInContainer("sh", "-c", cmd); + if (result.getExitCode() != CONTAINER_EXIT_CODE_OK) { + throw new IllegalStateException( + String.format( + "Failed to execute init script.\nStdout: %s\nStderr: %s", + result.getStdout(), + result.getStderr() + ) + ); + } + } } diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java index 816243d769e..4f23a060b1a 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java @@ -38,4 +38,72 @@ void shouldTestDatabaseName() { assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName); } } + + @Test + void shouldExecuteInitScript() { + try (MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("init.js")) { + mongoDB.start(); + assertInitScriptExecuted(mongoDB); + } + } + + @Test + void shouldExecuteInitScriptWithEdgeCases() { + try ( + MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js") + ) { + mongoDB.start(); + + try ( + com.mongodb.client.MongoClient client = com.mongodb.client.MongoClients.create( + mongoDB.getReplicaSetUrl() + ) + ) { + String expectedComplexName = "test_col_\"_with_specials_!@#%^&*()"; + String expectedCollectionWithSpecialChars = "col with spaces & symbols !@#"; + + com.mongodb.client.MongoDatabase database = client.getDatabase("test"); + + assertThat(database.listCollectionNames()) + .contains(expectedComplexName, expectedCollectionWithSpecialChars); + + com.mongodb.client.MongoCollection collection = database.getCollection( + expectedComplexName + ); + + org.bson.Document doc = collection.find(new org.bson.Document("_id", 1)).first(); + + assertThat(doc).isNotNull(); + + assertThat(doc.getString("key_with_quotes")).isEqualTo("This is a \"double quoted\" string"); + + assertThat(doc.getString("key_with_json_chars")).isEqualTo("{ } [ ] : ,"); + + assertThat(doc.getString("description")) + .isEqualTo("Insertion test for collection with special symbols"); + } + } + } + + @Test + void shouldExecuteInitScriptWithReplicaSet() { + try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withInitScript("init.js").withReplicaSet()) { + mongo.start(); + assertInitScriptExecuted(mongo); + } + } + + @Test + void shouldExecuteInitScriptWithSharding() { + try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withInitScript("init.js").withSharding()) { + mongo.start(); + assertInitScriptExecuted(mongo); + } + } + + private void assertInitScriptExecuted(MongoDBContainer mongo) { + try (com.mongodb.client.MongoClient client = com.mongodb.client.MongoClients.create(mongo.getReplicaSetUrl())) { + assertThat(client.getDatabase("test").listCollectionNames()).contains("test_collection"); + } + } } diff --git a/modules/mongodb/src/test/resources/init.js b/modules/mongodb/src/test/resources/init.js new file mode 100644 index 00000000000..fe29f7f7d2f --- /dev/null +++ b/modules/mongodb/src/test/resources/init.js @@ -0,0 +1 @@ +db.createCollection("test_collection"); \ No newline at end of file diff --git a/modules/mongodb/src/test/resources/initEdgeCase!@#%^& *'().js b/modules/mongodb/src/test/resources/initEdgeCase!@#%^& *'().js new file mode 100644 index 00000000000..21f8a7658d0 --- /dev/null +++ b/modules/mongodb/src/test/resources/initEdgeCase!@#%^& *'().js @@ -0,0 +1,16 @@ +var complexCollectionName = 'test_col_"_with_specials_!@#%^&*()'; + +db.createCollection(complexCollectionName); + +var collectionWithSpecialChars = "col with spaces & symbols !@#"; + +db.createCollection(collectionWithSpecialChars); + +db.getCollection(complexCollectionName).insertOne({ + "_id": 1, + "key_with_quotes": "This is a \"double quoted\" string", + "key_with_json_chars": "{ } [ ] : ,", + "description": "Insertion test for collection with special symbols" +}); + +print("Initialization completed: " + complexCollectionName); \ No newline at end of file