Skip to content

Commit c0571da

Browse files
authored
Add support for MongoDBContainer credentials (#1070)
1 parent 1afb781 commit c0571da

File tree

6 files changed

+93
-86
lines changed

6 files changed

+93
-86
lines changed

docs/modules/mongodb.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ npm install @testcontainers/mongodb --save-dev
1111
## Examples
1212

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

1717
<!--codeinclude-->
18-
[MongoDB 6.0.x:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connect6
18+
[Connect with credentials:](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectWithCredentials
1919
<!--/codeinclude-->

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ markdown_extensions:
2929
- pymdownx.details
3030
- pymdownx.superfences
3131
- pymdownx.tabbed:
32-
alternate_style: true
32+
alternate_style: false
3333
- toc:
3434
permalink: true
3535

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/modules/mongodb/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"mongoose": "^8.16.4"
3333
},
3434
"dependencies": {
35-
"testcontainers": "^11.3.2"
35+
"testcontainers": "^11.3.2",
36+
"compare-versions": "^6.1.1"
3637
}
3738
}

packages/modules/mongodb/src/mongodb-container.test.ts

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,42 @@ import { MongoDBContainer } from "./mongodb-container";
44

55
const IMAGE = getImage(__dirname);
66

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

12-
// directConnection: true is required as the testcontainer is created as a MongoDB Replica Set.
1312
const db = mongoose.createConnection(mongodbContainer.getConnectionString(), { directConnection: true });
14-
15-
// You can also add the default connection flag as a query parameter
16-
// const connectionString = `${mongodbContainer.getConnectionString()}?directConnection=true`;
17-
// const db = mongoose.createConnection(connectionString);
18-
1913
const fooCollection = db.collection("foo");
2014
const obj = { value: 1 };
2115

2216
const session = await db.startSession();
23-
await session.withTransaction(async () => {
24-
await fooCollection.insertOne(obj);
25-
});
17+
await session.withTransaction(async () => await fooCollection.insertOne(obj));
2618

27-
expect(
28-
await fooCollection.findOne({
29-
value: 1,
30-
})
31-
).toEqual(obj);
19+
const result = await fooCollection.findOne({ value: 1 });
20+
expect(result).toEqual(obj);
3221

33-
await mongoose.disconnect();
22+
await db.close();
23+
// }
3424
});
35-
// }
3625

37-
// connect6 {
38-
it("should work using version 6.0.1", async () => {
39-
await using mongodbContainer = await new MongoDBContainer("mongo:6.0.1").start();
26+
it("should connect with credentials", async () => {
27+
// connectWithCredentials {
28+
await using mongodbContainer = await new MongoDBContainer(IMAGE)
29+
.withUsername("mongo_user")
30+
.withPassword("mongo_password")
31+
.start();
4032

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

44-
// You can also add the default connection flag as a query parameter
45-
// const connectionString = `${mongodbContainer.getConnectionString()}?directConnection=true`;
46-
// const db = mongoose.createConnection(connectionString);
47-
48-
const fooCollection = db.collection("foo");
49-
const obj = { value: 1 };
50-
51-
const session = await db.startSession();
52-
await session.withTransaction(async () => {
53-
await fooCollection.insertOne(obj);
54-
});
35+
const result = await db.collection("testcontainers").insertOne({ title: "testcontainers" });
36+
const resultId = result.insertedId.toString();
37+
expect(resultId).toBeTruthy();
5538

56-
expect(
57-
await fooCollection.findOne({
58-
value: 1,
59-
})
60-
).toEqual(obj);
39+
const rsStatus = await db.db?.admin().replSetGetStatus();
40+
expect(rsStatus?.set).toBe("rs0");
6141

62-
await mongoose.disconnect();
42+
await db.close();
43+
// }
6344
});
64-
// }
6545
});
Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,90 @@
1-
import { AbstractStartedContainer, ExecResult, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
1+
import { satisfies } from "compare-versions";
2+
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
23

34
const MONGODB_PORT = 27017;
45

56
export class MongoDBContainer extends GenericContainer {
7+
private username: string | undefined;
8+
private password: string | undefined;
9+
610
constructor(image: string) {
711
super(image);
8-
this.withExposedPorts(MONGODB_PORT)
9-
.withCommand(["--replSet", "rs0"])
10-
.withWaitStrategy(Wait.forLogMessage(/.*waiting for connections.*/i))
11-
.withStartupTimeout(120_000);
12+
this.withExposedPorts(MONGODB_PORT).withWaitStrategy(Wait.forHealthCheck()).withStartupTimeout(120_000);
1213
}
1314

14-
public override async start(): Promise<StartedMongoDBContainer> {
15-
return new StartedMongoDBContainer(await super.start());
15+
public withUsername(username: string): this {
16+
if (!username) throw new Error("Username should not be empty.");
17+
this.username = username;
18+
return this;
1619
}
1720

18-
protected override async containerStarted(startedTestContainer: StartedTestContainer): Promise<void> {
19-
await this.initReplicaSet(startedTestContainer);
21+
public withPassword(password: string): this {
22+
if (!password) throw new Error("Password should not be empty.");
23+
this.password = password;
24+
return this;
2025
}
2126

22-
private async initReplicaSet(startedTestContainer: StartedTestContainer) {
23-
await this.executeMongoEvalCommand(startedTestContainer, "rs.initiate();");
24-
await this.executeMongoEvalCommand(startedTestContainer, this.buildMongoWaitCommand());
27+
public override async start(): Promise<StartedMongoDBContainer> {
28+
const cmdArgs = ["--replSet", "rs0"];
29+
if (!this.healthCheck) this.withWaitForRsHealthCheck();
30+
if (this.username && this.password) {
31+
cmdArgs.push("--keyFile", "/data/db/key.txt");
32+
this.withEnvironment({
33+
MONGO_INITDB_ROOT_USERNAME: this.username,
34+
MONGO_INITDB_ROOT_PASSWORD: this.password,
35+
})
36+
.withCopyContentToContainer([
37+
{
38+
content: "1111111111",
39+
mode: 0o400,
40+
target: "/data/db/key.txt",
41+
},
42+
])
43+
.withCommand(cmdArgs);
44+
} else {
45+
this.withCommand(cmdArgs);
46+
}
47+
return new StartedMongoDBContainer(await super.start(), this.username, this.password);
2548
}
2649

27-
private async executeMongoEvalCommand(startedTestContainer: StartedTestContainer, command: string) {
28-
const execResult = await startedTestContainer.exec(this.buildMongoEvalCommand(command));
29-
this.checkMongoNodeExitCode(execResult);
50+
private withWaitForRsHealthCheck(): this {
51+
return this.withHealthCheck({
52+
test: [
53+
"CMD-SHELL",
54+
this.buildMongoEvalCommand(
55+
`'try { rs.initiate(); } catch (e){} while (db.runCommand({isMaster: 1}).ismaster==false) { sleep(100); }'`
56+
),
57+
],
58+
interval: 250,
59+
timeout: 60000,
60+
retries: 1000,
61+
});
3062
}
3163

3264
private buildMongoEvalCommand(command: string) {
33-
return [this.getMongoCmdBasedOnImageTag(), "--eval", command];
34-
}
35-
36-
private getMongoCmdBasedOnImageTag() {
37-
return parseInt(this.imageName.tag[0]) >= 5 ? "mongosh" : "mongo";
38-
}
39-
40-
private checkMongoNodeExitCode(execResult: ExecResult) {
41-
const { exitCode, output } = execResult;
42-
if (execResult.exitCode !== 0) {
43-
throw new Error(`Error running mongo command. Exit code ${exitCode}: ${output}`);
44-
}
45-
}
46-
47-
private buildMongoWaitCommand() {
48-
return `
49-
var attempt = 0;
50-
while(db.runCommand({isMaster: 1}).ismaster==false) {
51-
if (attempt > 60) {
52-
quit(1);
53-
}
54-
print(attempt); sleep(100); attempt++;
55-
}
56-
`;
65+
const useMongosh = satisfies(this.imageName.tag, ">=5.0.0");
66+
const args = [];
67+
if (useMongosh) args.push("mongosh");
68+
else args.push("mongo", "admin");
69+
if (this.username && this.password) args.push("-u", this.username, "-p", this.password);
70+
args.push("--quiet", "--eval", command);
71+
return args.join(" ");
5772
}
5873
}
5974

6075
export class StartedMongoDBContainer extends AbstractStartedContainer {
61-
constructor(startedTestContainer: StartedTestContainer) {
76+
private readonly username: string | undefined;
77+
private readonly password: string | undefined;
78+
79+
constructor(startedTestContainer: StartedTestContainer, username: string | undefined, password: string | undefined) {
6280
super(startedTestContainer);
81+
this.username = username;
82+
this.password = password;
6383
}
6484

6585
public getConnectionString(): string {
86+
if (this.username && this.password)
87+
return `mongodb://${this.username}:${this.password}@${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}?authSource=admin`;
6688
return `mongodb://${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}`;
6789
}
6890
}

0 commit comments

Comments
 (0)