Skip to content

Commit cec67f1

Browse files
committed
MongoDB: support username/password credentials
Previously MongoDB did not support passing credentials, although the underlying image does support them. However, as the current container implementation always sets up replication, auth gets a bit more complicated. A keyfile neds to be generated and a wait needs to be added for auth setup. In future major version, it would make sense to drop the automatic replication from constructor and move it to option like withReplication Closes testcontainers#988
1 parent e4fa915 commit cec67f1

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,56 @@ describe("MongodbContainer", { timeout: 240_000 }, () => {
6161
await mongodbContainer.stop();
6262
});
6363
// }
64+
65+
it("uses custom credentials (Mongo4, old commands)", async () => {
66+
const mongodbContainer = await new MongoDBContainer("mongo:4.0.1")
67+
.withUsername("CustomUserName")
68+
.withPassword("CustomPassword")
69+
.start();
70+
71+
const db = mongoose.createConnection(mongodbContainer.getConnectionString(), { directConnection: true });
72+
73+
const fooCollection = db.collection("foo");
74+
const obj = { value: 1 };
75+
76+
const session = await db.startSession();
77+
await session.withTransaction(async () => {
78+
await fooCollection.insertOne(obj);
79+
});
80+
81+
expect(
82+
await fooCollection.findOne({
83+
value: 1,
84+
})
85+
).toEqual(obj);
86+
87+
await mongoose.disconnect();
88+
await mongodbContainer.stop();
89+
});
90+
91+
it("uses custom credentials (Mongo8, new commands)", async () => {
92+
const mongodbContainer = await new MongoDBContainer("mongo:8.0.8")
93+
.withUsername("CustomUserName")
94+
.withPassword("CustomPassword")
95+
.start();
96+
97+
const db = mongoose.createConnection(mongodbContainer.getConnectionString(), { directConnection: true });
98+
99+
const fooCollection = db.collection("foo");
100+
const obj = { value: 1 };
101+
102+
const session = await db.startSession();
103+
await session.withTransaction(async () => {
104+
await fooCollection.insertOne(obj);
105+
});
106+
107+
expect(
108+
await fooCollection.findOne({
109+
value: 1,
110+
})
111+
).toEqual(obj);
112+
113+
await mongoose.disconnect();
114+
await mongodbContainer.stop();
115+
});
64116
});

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { AbstractStartedContainer, ExecResult, GenericContainer, StartedTestCont
33
const MONGODB_PORT = 27017;
44

55
export class MongoDBContainer extends GenericContainer {
6+
private username: string | null = null;
7+
private password: string | null = null;
8+
69
constructor(image = "mongo:4.0.1") {
710
super(image);
811
this.withExposedPorts(MONGODB_PORT)
@@ -11,16 +14,41 @@ export class MongoDBContainer extends GenericContainer {
1114
.withStartupTimeout(120_000);
1215
}
1316

17+
public withUsername(username: string): this {
18+
this.username = username;
19+
return this;
20+
}
21+
22+
public withPassword(rootPassword: string): this {
23+
this.password = rootPassword;
24+
return this;
25+
}
26+
1427
public override async start(): Promise<StartedMongoDBContainer> {
15-
return new StartedMongoDBContainer(await super.start());
28+
if (this.username && this.password) {
29+
const containerKeyfilePath = "/tmp/mongo-keyfile";
30+
this.withCommand([
31+
"/bin/sh",
32+
"-c",
33+
`
34+
openssl rand -base64 756 > ${containerKeyfilePath} &&
35+
chmod 600 ${containerKeyfilePath} &&
36+
chown mongodb:mongodb ${containerKeyfilePath} &&
37+
exec mongod --replSet rs0 --keyFile ${containerKeyfilePath} --bind_ip_all
38+
`,
39+
]);
40+
this.withEnvironment({ MONGO_INITDB_ROOT_USERNAME: this.username, MONGO_INITDB_ROOT_PASSWORD: this.password });
41+
}
42+
43+
return new StartedMongoDBContainer(await super.start(), this.username, this.password);
1644
}
1745

1846
protected override async containerStarted(startedTestContainer: StartedTestContainer): Promise<void> {
1947
await this.initReplicaSet(startedTestContainer);
2048
}
2149

2250
private async initReplicaSet(startedTestContainer: StartedTestContainer) {
23-
await this.executeMongoEvalCommand(startedTestContainer, "rs.initiate();");
51+
await this.executeMongoEvalCommand(startedTestContainer, `rs.initiate();`);
2452
await this.executeMongoEvalCommand(startedTestContainer, this.buildMongoWaitCommand());
2553
}
2654

@@ -30,7 +58,15 @@ export class MongoDBContainer extends GenericContainer {
3058
}
3159

3260
private buildMongoEvalCommand(command: string) {
33-
return [this.getMongoCmdBasedOnImageTag(), "--eval", command];
61+
const cmd = [this.getMongoCmdBasedOnImageTag()];
62+
63+
if (this.username && this.password) {
64+
cmd.push("--username", this.username, "--password", this.password, "--authenticationDatabase", "admin");
65+
}
66+
67+
cmd.push("--eval", command);
68+
69+
return cmd;
3470
}
3571

3672
private getMongoCmdBasedOnImageTag() {
@@ -58,11 +94,19 @@ export class MongoDBContainer extends GenericContainer {
5894
}
5995

6096
export class StartedMongoDBContainer extends AbstractStartedContainer {
61-
constructor(startedTestContainer: StartedTestContainer) {
97+
constructor(
98+
startedTestContainer: StartedTestContainer,
99+
private readonly username: string | null,
100+
private readonly password: string | null
101+
) {
62102
super(startedTestContainer);
63103
}
64104

65105
public getConnectionString(): string {
106+
if (this.username !== null && this.password !== null) {
107+
return `mongodb://${this.username}:${this.password}@${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}`;
108+
}
109+
66110
return `mongodb://${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}`;
67111
}
68112
}

0 commit comments

Comments
 (0)