Skip to content
Merged
4 changes: 2 additions & 2 deletions docs/modules/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ npm install @testcontainers/mongodb --save-dev
## Examples

<!--codeinclude-->
[Mongo 4.0.x:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connect4
[Connect:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectMongo
<!--/codeinclude-->

<!--codeinclude-->
[MongoDB 6.0.x:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connect6
[Connect with credentials:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectWithCredentials
<!--/codeinclude-->
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ markdown_extensions:
- pymdownx.details
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
alternate_style: false
- toc:
permalink: true

Expand Down
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/modules/mongodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"mongoose": "^8.16.4"
},
"dependencies": {
"testcontainers": "^11.3.2"
"testcontainers": "^11.3.2",
"compare-versions": "^6.1.1"
}
}
64 changes: 22 additions & 42 deletions packages/modules/mongodb/src/mongodb-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,42 @@ import { MongoDBContainer } from "./mongodb-container";

const IMAGE = getImage(__dirname);

describe("MongodbContainer", { timeout: 240_000 }, () => {
// connect4 {
it("should work using default version 4.0.1", async () => {
await using mongodbContainer = await new MongoDBContainer(IMAGE).start();
describe("MongoDBContainer", { timeout: 240_000 }, () => {
it.each([IMAGE, "mongo:6.0.25", "mongo:4.4.29"])("should work with %s", async (image) => {
// connectMongo {
await using mongodbContainer = await new MongoDBContainer(image).start();

// directConnection: true is required as the testcontainer is created as a MongoDB Replica Set.
const db = mongoose.createConnection(mongodbContainer.getConnectionString(), { directConnection: true });

// You can also add the default connection flag as a query parameter
// const connectionString = `${mongodbContainer.getConnectionString()}?directConnection=true`;
// const db = mongoose.createConnection(connectionString);

const fooCollection = db.collection("foo");
const obj = { value: 1 };

const session = await db.startSession();
await session.withTransaction(async () => {
await fooCollection.insertOne(obj);
});
await session.withTransaction(async () => await fooCollection.insertOne(obj));

expect(
await fooCollection.findOne({
value: 1,
})
).toEqual(obj);
const result = await fooCollection.findOne({ value: 1 });
expect(result).toEqual(obj);

await mongoose.disconnect();
await db.close();
// }
});
// }

// connect6 {
it("should work using version 6.0.1", async () => {
await using mongodbContainer = await new MongoDBContainer("mongo:6.0.1").start();
it("should connect with credentials", async () => {
// connectWithCredentials {
await using mongodbContainer = await new MongoDBContainer(IMAGE)
.withUsername("mongo_user")
.withPassword("mongo_password")
.start();

// directConnection: true is required as the testcontainer is created as a MongoDB Replica Set.
const db = mongoose.createConnection(mongodbContainer.getConnectionString(), { directConnection: true });

// You can also add the default connection flag as a query parameter
// const connectionString = `${mongodbContainer.getConnectionString()}?directConnection=true`;
// const db = mongoose.createConnection(connectionString);

const fooCollection = db.collection("foo");
const obj = { value: 1 };

const session = await db.startSession();
await session.withTransaction(async () => {
await fooCollection.insertOne(obj);
});
const result = await db.collection("testcontainers").insertOne({ title: "testcontainers" });
const resultId = result.insertedId.toString();
expect(resultId).toBeTruthy();

expect(
await fooCollection.findOne({
value: 1,
})
).toEqual(obj);
const rsStatus = await db.db?.admin().replSetGetStatus();
expect(rsStatus?.set).toBe("rs0");

await mongoose.disconnect();
await db.close();
// }
});
// }
});
102 changes: 62 additions & 40 deletions packages/modules/mongodb/src/mongodb-container.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,90 @@
import { AbstractStartedContainer, ExecResult, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { satisfies } from "compare-versions";
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";

const MONGODB_PORT = 27017;

export class MongoDBContainer extends GenericContainer {
private username: string | undefined;
private password: string | undefined;

constructor(image: string) {
super(image);
this.withExposedPorts(MONGODB_PORT)
.withCommand(["--replSet", "rs0"])
.withWaitStrategy(Wait.forLogMessage(/.*waiting for connections.*/i))
.withStartupTimeout(120_000);
this.withExposedPorts(MONGODB_PORT).withWaitStrategy(Wait.forHealthCheck()).withStartupTimeout(120_000);
}

public override async start(): Promise<StartedMongoDBContainer> {
return new StartedMongoDBContainer(await super.start());
public withUsername(username: string): this {
if (!username) throw new Error("Username should not be empty.");
this.username = username;
return this;
}

protected override async containerStarted(startedTestContainer: StartedTestContainer): Promise<void> {
await this.initReplicaSet(startedTestContainer);
public withPassword(password: string): this {
if (!password) throw new Error("Password should not be empty.");
this.password = password;
return this;
}

private async initReplicaSet(startedTestContainer: StartedTestContainer) {
await this.executeMongoEvalCommand(startedTestContainer, "rs.initiate();");
await this.executeMongoEvalCommand(startedTestContainer, this.buildMongoWaitCommand());
public override async start(): Promise<StartedMongoDBContainer> {
const cmdArgs = ["--replSet", "rs0"];
if (!this.healthCheck) this.withWaitForRsHealthCheck();
if (this.username && this.password) {
cmdArgs.push("--keyFile", "/data/db/key.txt");
this.withEnvironment({
MONGO_INITDB_ROOT_USERNAME: this.username,
MONGO_INITDB_ROOT_PASSWORD: this.password,
})
.withCopyContentToContainer([
{
content: "1111111111",
mode: 0o400,
target: "/data/db/key.txt",
},
])
.withCommand(cmdArgs);
} else {
this.withCommand(cmdArgs);
}
return new StartedMongoDBContainer(await super.start(), this.username, this.password);
}

private async executeMongoEvalCommand(startedTestContainer: StartedTestContainer, command: string) {
const execResult = await startedTestContainer.exec(this.buildMongoEvalCommand(command));
this.checkMongoNodeExitCode(execResult);
private withWaitForRsHealthCheck(): this {
return this.withHealthCheck({
test: [
"CMD-SHELL",
this.buildMongoEvalCommand(
`'try { rs.initiate(); } catch (e){} while (db.runCommand({isMaster: 1}).ismaster==false) { sleep(100); }'`
),
],
interval: 250,
timeout: 60000,
retries: 1000,
});
}

private buildMongoEvalCommand(command: string) {
return [this.getMongoCmdBasedOnImageTag(), "--eval", command];
}

private getMongoCmdBasedOnImageTag() {
return parseInt(this.imageName.tag[0]) >= 5 ? "mongosh" : "mongo";
}

private checkMongoNodeExitCode(execResult: ExecResult) {
const { exitCode, output } = execResult;
if (execResult.exitCode !== 0) {
throw new Error(`Error running mongo command. Exit code ${exitCode}: ${output}`);
}
}

private buildMongoWaitCommand() {
return `
var attempt = 0;
while(db.runCommand({isMaster: 1}).ismaster==false) {
if (attempt > 60) {
quit(1);
}
print(attempt); sleep(100); attempt++;
}
`;
const useMongosh = satisfies(this.imageName.tag, ">=5.0.0");
const args = [];
if (useMongosh) args.push("mongosh");
else args.push("mongo", "admin");
if (this.username && this.password) args.push("-u", this.username, "-p", this.password);
args.push("--quiet", "--eval", command);
return args.join(" ");
}
}

export class StartedMongoDBContainer extends AbstractStartedContainer {
constructor(startedTestContainer: StartedTestContainer) {
private readonly username: string | undefined;
private readonly password: string | undefined;

constructor(startedTestContainer: StartedTestContainer, username: string | undefined, password: string | undefined) {
super(startedTestContainer);
this.username = username;
this.password = password;
}

public getConnectionString(): string {
if (this.username && this.password)
return `mongodb://${this.username}:${this.password}@${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}?authSource=admin`;
return `mongodb://${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}`;
}
}
Loading