Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -41,10 +41,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 +74,26 @@ protected void containerIsStarted(InspectContainerResponse containerInfo, boolea
if (this.rsEnabled) {
initReplicaSet(reused);
}

boolean isClusterMode = this.shardingEnabled || this.rsEnabled;

if (isClusterMode && this.initScriptPath != null) {
executeInitScriptInContainer();
}
}

@Override
protected void configure() {
super.configure();
boolean isClusterMode = this.shardingEnabled || this.rsEnabled;
if (this.initScriptPath != null) {
String destination = isClusterMode ? SCRIPT_DESTINATION_MANUAL : SCRIPT_DESTINATION_DEFAULT;
withCopyFileToContainer(MountableFile.forClasspathResource(this.initScriptPath), destination);
}

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 +230,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 @@ -2,6 +2,8 @@

import org.junit.jupiter.api.Test;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

class MongoDBContainerTest extends AbstractMongo {
Expand Down Expand Up @@ -38,4 +40,102 @@ void shouldTestDatabaseName() {
assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName);
}
}

@Test
void shouldExecuteInitScript() {
try (
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10")
.withInitScript("init.js")
.withStartupTimeout(Duration.ofSeconds(30))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

) {
mongoDB.start();
assertThat(mongoDB.isRunning()).isTrue();
}
}

@Test
void shouldExecuteInitScriptWithEdgeCases() {
try (
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10")
.withInitScript("initEdgeCase.js")
.withEnv("LANG", "C.UTF-8")
.withEnv("LC_ALL", "C.UTF-8")
.withStartupTimeout(Duration.ofSeconds(30))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

) {
mongoDB.start();

try (
com.mongodb.client.MongoClient client = com.mongodb.client.MongoClients.create(
mongoDB.getReplicaSetUrl()
)
) {
String expectedComplexName = "test_col_\"_with_specials_!@#%^&*()";
String expectedJapaneseName = "日本語 コレクション テスト";

com.mongodb.client.MongoDatabase database = client.getDatabase("test");

assertThat(database.listCollectionNames()).contains(expectedComplexName, expectedJapaneseName);

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).as("Document with _id=1 should exist").isNotNull();

assertThat(doc.getString("key_with_quotes"))
.as("Double quotes should be preserved correctly")
.isEqualTo("This is a \"double quoted\" string");

assertThat(doc.getString("key_with_json_chars"))
.as("JSON special chars should be treated as plain text")
.isEqualTo("{ } [ ] : ,");

assertThat(doc.getString("description"))
.as("Japanese text should be preserved correctly")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove as

.isEqualTo("特殊記号を含むコレクションへの挿入テスト");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use plain english, please?

}
}
}

@Test
void shouldExecuteInitScriptWithReplicaSet() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withInitScript("init.js").withReplicaSet()) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}

@Test
void shouldExecuteInitScriptWithReplicaSetConfiguredFirst() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withReplicaSet().withInitScript("init.js")) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove it. There is no difference with the previous test


@Test
void shouldExecuteInitScriptWithSharding() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withInitScript("init.js").withSharding()) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}

@Test
void shouldExecuteInitScriptWithShardingConfiguredFirst() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withSharding().withInitScript("init.js")) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove it. There is no difference with the previous test


private void assertInitScriptExecuted(MongoDBContainer mongo) {
try (com.mongodb.client.MongoClient client = com.mongodb.client.MongoClients.create(mongo.getReplicaSetUrl())) {
assertThat(client.getDatabase("test").listCollectionNames())
.as("Check if init.js created the collection")
.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 japaneseCollectionName = "日本語 コレクション テスト";

db.createCollection(japaneseCollectionName);

db.getCollection(complexCollectionName).insertOne({
"_id": 1,
"key_with_quotes": "This is a \"double quoted\" string",
"key_with_json_chars": "{ } [ ] : ,",
"description": "特殊記号を含むコレクションへの挿入テスト"
});

print("Initialization completed: " + complexCollectionName);