Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -41,10 +47,16 @@ public class MongoDBContainer extends GenericContainer<MongoDBContainer> {

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));
}
Expand All @@ -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.
* <p>
* 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.
* <p>
* 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<URL> 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) {
Expand Down Expand Up @@ -204,4 +279,46 @@ public String getReplicaSetUrl(String databaseName) {
}
return getConnectionString() + "/" + databaseName;
}

/**
* Executes a MongoDB initialization script from the classpath during startup.
* <p>
* 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.
* <p>
* 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()
)
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<org.bson.Document> 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");
}
}
}
1 change: 1 addition & 0 deletions modules/mongodb/src/test/resources/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db.createCollection("test_collection");
16 changes: 16 additions & 0 deletions modules/mongodb/src/test/resources/initEdgeCase!@#%^& *'().js
Original file line number Diff line number Diff line change
@@ -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);