Skip to content

Commit 31854b5

Browse files
committed
mongodb-container-2
1 parent 51aee53 commit 31854b5

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

package-lock.json

Lines changed: 1 addition & 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.1"
3333
},
3434
"dependencies": {
35-
"testcontainers": "^11.2.1"
35+
"testcontainers": "^11.2.1",
36+
"compare-versions": "^6.1.1"
3637
}
3738
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import mongoose from "mongoose";
2+
import { MongoDBContainer2, StartedMongoDBContainer2 } from "./mongodb-container-2";
3+
4+
describe("MongodbContainer", { timeout: 240_000 }, () => {
5+
test.for([
6+
["mongo:5.0", "true"],
7+
["mongo:8.0", "true"],
8+
["mongo:5.0", "false"],
9+
["mongo:8.0", "false"],
10+
])("should connect to %s (credentials: %s)", async ([image, creds]) => {
11+
const mongodbContainer = await getContainer(image, creds === "true");
12+
await runTest(mongodbContainer);
13+
});
14+
});
15+
16+
async function getContainer(image: string, auth: boolean) {
17+
if (auth) return new MongoDBContainer2(image).withUsername("mongo_user").withPassword("mongo_password").start();
18+
return new MongoDBContainer2(image).start();
19+
}
20+
21+
async function runTest(mongodbContainer: StartedMongoDBContainer2) {
22+
const mongo = await mongoose.connect(mongodbContainer.getConnectionString(), { directConnection: true });
23+
expect(mongo.connection.readyState).toBe(1);
24+
const result = await mongo.connection.collection("testcontainers").insertOne({ title: "testcontainers" });
25+
const id = result.insertedId.toString();
26+
expect(id).not.toBeNull();
27+
expect(id).not.toBe("");
28+
const rsStatus = await mongo.connection.db?.admin().replSetGetStatus();
29+
expect(rsStatus).toBeDefined();
30+
expect(rsStatus?.set).toBe("rs0");
31+
await mongo.disconnect();
32+
await mongodbContainer.stop();
33+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { AbstractStartedContainer, ExecResult, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
2+
3+
const MONGODB_PORT = 27017;
4+
5+
export class MongoDBContainer2 extends GenericContainer {
6+
private username = "";
7+
private password = "";
8+
private database = "testcontainers";
9+
10+
constructor(image: string) {
11+
super(image);
12+
this.withExposedPorts(MONGODB_PORT)
13+
.withWaitStrategy(Wait.forLogMessage(/.*waiting for connections.*/i))
14+
.withStartupTimeout(120_000);
15+
}
16+
17+
public withUsername(username: string): this {
18+
if (username === "") throw new Error("Username should not be empty.");
19+
this.username = username;
20+
return this;
21+
}
22+
23+
public withPassword(password: string): this {
24+
if (password === "") throw new Error("Password should not be empty.");
25+
this.password = password;
26+
return this;
27+
}
28+
29+
public withDatabase(database: string): this {
30+
if (database === "") throw new Error("Database should not be empty.");
31+
this.database = database;
32+
return this;
33+
}
34+
35+
public override async start(): Promise<StartedMongoDBContainer2> {
36+
if (this.authEnabled()) {
37+
this.withEnvironment({
38+
MONGO_INITDB_ROOT_USERNAME: this.username,
39+
MONGO_INITDB_ROOT_PASSWORD: this.password,
40+
MONGO_INITDB_DATABASE: this.database,
41+
})
42+
.withCopyContentToContainer([
43+
{
44+
content: "1111111111",
45+
mode: 0o400,
46+
target: "/data/db/key.txt",
47+
},
48+
])
49+
.withCommand(["--replSet", "rs0", "--keyFile", "/data/db/key.txt"]);
50+
} else {
51+
this.withCommand(["--replSet", "rs0"]);
52+
}
53+
return new StartedMongoDBContainer2(await super.start(), this.username, this.password, this.database);
54+
}
55+
56+
protected override async containerStarted(container: StartedTestContainer): Promise<void> {
57+
await this.executeMongoEvalCommand(container, this.buildMongoRsInitCommand());
58+
await this.executeMongoEvalCommand(container, this.buildMongoWaitCommand());
59+
}
60+
61+
private async executeMongoEvalCommand(container: StartedTestContainer, command: string) {
62+
let totalElapsed = 0;
63+
const waitInterval = 1000;
64+
const timeout = 60 * waitInterval;
65+
while (totalElapsed < timeout) {
66+
const execResult = await container.exec(this.buildMongoEvalCommand(command));
67+
const output = execResult.output?.trimEnd();
68+
if (
69+
output?.endsWith("MongoServerError: Authentication failed.") ||
70+
output?.includes("MongoNetworkError: connect ECONNREFUSED")
71+
) {
72+
await new Promise((resolve) => setTimeout(resolve, waitInterval));
73+
totalElapsed += waitInterval;
74+
} else {
75+
this.checkMongoNodeExitCode(execResult);
76+
break;
77+
}
78+
}
79+
}
80+
81+
private buildMongoEvalCommand(command: string) {
82+
return ["mongosh", "-u", this.username, "-p", this.password, "--eval", command];
83+
}
84+
85+
private checkMongoNodeExitCode(execResult: ExecResult) {
86+
const { exitCode, output } = execResult;
87+
if (execResult.exitCode !== 0) {
88+
throw new Error(`Error running mongo command. Exit code ${exitCode}: ${output}`);
89+
}
90+
}
91+
92+
private buildMongoRsInitCommand() {
93+
return `
94+
while (true) {
95+
try {
96+
rs.initiate();
97+
break;
98+
}
99+
catch {
100+
sleep(1000);
101+
}
102+
}
103+
`;
104+
}
105+
106+
private buildMongoWaitCommand() {
107+
return `
108+
while (true) {
109+
try {
110+
rs.status();
111+
break;
112+
}
113+
catch {
114+
sleep(1000);
115+
}
116+
}
117+
`;
118+
}
119+
120+
private authEnabled() {
121+
return this.username && this.password;
122+
}
123+
}
124+
125+
export class StartedMongoDBContainer2 extends AbstractStartedContainer {
126+
private readonly username: string = "";
127+
private readonly password: string = "";
128+
private readonly database: string = "";
129+
130+
constructor(startedTestContainer: StartedTestContainer, username: string, password: string, database: string) {
131+
super(startedTestContainer);
132+
this.username = username;
133+
this.password = password;
134+
this.database = database;
135+
}
136+
137+
public getConnectionString(): string {
138+
if (this.username && this.password)
139+
return `mongodb://${this.username}:${this.password}@${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}/${this.database}?authSource=admin`;
140+
return `mongodb://${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}/${this.database}`;
141+
}
142+
}

0 commit comments

Comments
 (0)