From c68f390030e457f1e3f3d58edd5df43f34ae6465 Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 10:11:09 +0900 Subject: [PATCH 1/6] Add MongoDB Atlas Local support --- docs/modules/mongodb.md | 31 ++++ packages/modules/mongodb/src/index.ts | 1 + .../src/mongodb-atlas-local-container.test.ts | 137 ++++++++++++++++++ .../src/mongodb-atlas-local-container.ts | 71 +++++++++ 4 files changed, 240 insertions(+) create mode 100644 packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts create mode 100644 packages/modules/mongodb/src/mongodb-atlas-local-container.ts diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 96571b56b..925f0498a 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -27,3 +27,34 @@ Choose an image from the [container registry](https://hub.docker.com/_/mongo) an [](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectWithCredentials + +## MongoDB Atlas Local + +The MongoDB Atlas Local image combines the MongoDB database engine with MongoT for Atlas Search. + +Choose an image from the [container registry](https://hub.docker.com/r/mongodb/mongodb-atlas-local) and substitute `IMAGE`. +The connection string returned by `getConnectionString()` does not include `directConnection=true`; pass `directConnection: true` to your MongoDB client options instead. + +### Get a connection string + + +[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocal + + +### Get a database connection string + + +[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocalDatabase + + +### Connect with credentials + + +[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocalWithCredentials + + +### Create an Atlas Search index and search it + + +[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:createAtlasIndexAndSearchIt + diff --git a/packages/modules/mongodb/src/index.ts b/packages/modules/mongodb/src/index.ts index c057e7fce..e9e0621a8 100644 --- a/packages/modules/mongodb/src/index.ts +++ b/packages/modules/mongodb/src/index.ts @@ -1 +1,2 @@ +export { MongoDBAtlasLocalContainer, StartedMongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; export { MongoDBContainer, StartedMongoDBContainer } from "./mongodb-container"; diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts new file mode 100644 index 000000000..56741da65 --- /dev/null +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -0,0 +1,137 @@ +import mongoose from "mongoose"; +import { IntervalRetry } from "../../../testcontainers/src/common"; +import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; + +const IMAGE = "mongodb/mongodb-atlas-local:8.2.2"; +const ATLAS_SEARCH_INDEX = { + mappings: { + dynamic: false, + fields: { + test: { + type: "string", + }, + test2: { + type: "number", + representation: "int64", + indexDoubles: false, + }, + test3: { + type: "boolean", + }, + }, + }, +}; + +describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { + it("should provide a connection string", async () => { + // connectAtlasLocal { + await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + // } + + expect(container.getConnectionString()).toBe(`mongodb://${container.getHost()}:${container.getMappedPort(27017)}/`); + }); + + it("should provide a database connection string", async () => { + // connectAtlasLocalDatabase { + await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + // } + + expect(container.getDatabaseConnectionString()).toBe( + `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/test` + ); + }); + + it("should provide connection strings with credentials", async () => { + // connectAtlasLocalWithCredentials { + await using container = await new MongoDBAtlasLocalContainer(IMAGE) + .withUsername("customUsername") + .withPassword("customPassword") + .start(); + // } + + expect(container.getConnectionString()).toBe( + `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/` + ); + expect(container.getDatabaseConnectionString()).toBe( + `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/test` + ); + }); + + it("should connect to mongodb atlas local", async () => { + await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + + const db = mongoose.createConnection(container.getConnectionString(), { directConnection: true }); + + const obj = { value: 1 }; + const collection = db.collection("test"); + await collection.insertOne(obj); + + const result = await collection.findOne({ value: 1 }); + expect(result).toEqual(obj); + + await db.close(); + }); + + // createAtlasIndexAndSearchIt { + it("should create an atlas search index and search it", async () => { + await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(IMAGE).start(); + + const db = mongoose.createConnection(atlasLocalContainer.getConnectionString(), { + dbName: "test", + directConnection: true, + }); + + try { + const collection = db.collection("test"); + + await db.createCollection("test"); + + await collection.createSearchIndex({ + name: "AtlasSearchIndex", + definition: ATLAS_SEARCH_INDEX, + }); + + await new IntervalRetry(10).retryUntil( + async () => { + const searchIndexes = await collection.listSearchIndexes("AtlasSearchIndex").toArray(); + return (searchIndexes[0] as { status?: string } | undefined)?.status; + }, + (status) => status?.toUpperCase() === "READY", + () => new Error("Atlas Search index did not become ready in time"), + 5_000 + ); + + await collection.insertOne({ test: "tests", test2: 123, test3: true }); + + const found = await new IntervalRetry | null, Error>(10).retryUntil( + async () => + collection + .aggregate([ + { + $search: { + index: "AtlasSearchIndex", + text: { + query: "test", + path: "test", + fuzzy: {}, + }, + }, + }, + ]) + .next(), + (result) => result !== null, + () => new Error("Atlas Search did not return a result in time"), + 5_000 + ); + + if (found instanceof Error) { + throw found; + } + + expect(found).toMatchObject({ test: "tests", test2: 123, test3: true }); + } finally { + await db.close(); + } + }); + // } +}); diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts new file mode 100644 index 000000000..ef4aa4142 --- /dev/null +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts @@ -0,0 +1,71 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const MONGODB_PORT = 27017; +const DEFAULT_DATABASE_NAME = "test"; + +export class MongoDBAtlasLocalContainer extends GenericContainer { + private username: string | undefined; + private password: string | undefined; + + constructor(image: string) { + super(image); + this.withExposedPorts(MONGODB_PORT) + .withWaitStrategy(Wait.forSuccessfulCommand("runner healthcheck")) + .withStartupTimeout(120_000); + } + + public withUsername(username: string): this { + if (!username) throw new Error("Username should not be empty."); + this.username = username; + return this; + } + + public withPassword(password: string): this { + if (!password) throw new Error("Password should not be empty."); + this.password = password; + return this; + } + + public override async start(): Promise { + if (this.username && this.password) { + this.withEnvironment({ + MONGODB_INITDB_ROOT_USERNAME: this.username, + MONGODB_INITDB_ROOT_PASSWORD: this.password, + }); + } + + return new StartedMongoDBAtlasLocalContainer(await super.start(), this.username, this.password); + } +} + +export class StartedMongoDBAtlasLocalContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly username: string | undefined, + private readonly password: string | undefined + ) { + super(startedTestContainer); + } + + public getConnectionString(): string { + return this.buildConnectionString(); + } + + public getDatabaseConnectionString(databaseName = DEFAULT_DATABASE_NAME): string { + return this.buildConnectionString(databaseName); + } + + private buildConnectionString(databaseName?: string): string { + const url = new URL("mongodb://"); + url.hostname = this.getHost(); + url.port = this.getMappedPort(MONGODB_PORT).toString(); + url.pathname = databaseName ? `/${databaseName}` : "/"; + + if (this.username && this.password) { + url.username = this.username; + url.password = this.password; + } + + return url.toString(); + } +} From 91296cf7fb4b84b58ef28d0929a9341a71c5d1fb Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 20:39:19 +0900 Subject: [PATCH 2/6] Name MongoDB doc sections after their container classes Rename `## Examples` to `## MongoDBContainer` and `## MongoDB Atlas Local` to `## MongoDBAtlasLocalContainer` so both containers are peer H2 sections whose headings match their exported class names. --- docs/modules/mongodb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 925f0498a..6df0efeaa 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -6,7 +6,7 @@ npm install @testcontainers/mongodb --save-dev ``` -## Examples +## MongoDBContainer These examples use the following libraries: @@ -28,7 +28,7 @@ Choose an image from the [container registry](https://hub.docker.com/_/mongo) an [](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectWithCredentials -## MongoDB Atlas Local +## MongoDBAtlasLocalContainer The MongoDB Atlas Local image combines the MongoDB database engine with MongoT for Atlas Search. From 6448dae4de1a95b83ed3489b0ab86dd059a333de Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 20:53:18 +0900 Subject: [PATCH 3/6] Set authSource=admin on credentialed Atlas Local connection strings The root user is created in the admin database, so connection strings that carry credentials must authenticate against it. Add an authenticated test that inserts a document to cover the behavior. --- .../src/mongodb-atlas-local-container.test.ts | 22 +++++++++++++++++-- .../src/mongodb-atlas-local-container.ts | 1 + 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index 56741da65..35da60015 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -50,10 +50,10 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { // } expect(container.getConnectionString()).toBe( - `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/` + `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/?authSource=admin` ); expect(container.getDatabaseConnectionString()).toBe( - `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/test` + `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/test?authSource=admin` ); }); @@ -72,6 +72,24 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { await db.close(); }); + it("should connect to mongodb atlas local with credentials", async () => { + await using container = await new MongoDBAtlasLocalContainer(IMAGE) + .withUsername("customUsername") + .withPassword("customPassword") + .start(); + + const db = mongoose.createConnection(container.getDatabaseConnectionString(), { directConnection: true }); + + const obj = { value: 1 }; + const collection = db.collection("test"); + await collection.insertOne(obj); + + const result = await collection.findOne({ value: 1 }); + expect(result).toEqual(obj); + + await db.close(); + }); + // createAtlasIndexAndSearchIt { it("should create an atlas search index and search it", async () => { await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(IMAGE).start(); diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts index ef4aa4142..c81f6f23e 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts @@ -64,6 +64,7 @@ export class StartedMongoDBAtlasLocalContainer extends AbstractStartedContainer if (this.username && this.password) { url.username = this.username; url.password = this.password; + url.searchParams.set("authSource", "admin"); } return url.toString(); From cb94c7bb661fa5c1de67a68eff883353248ed640 Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 20:53:32 +0900 Subject: [PATCH 4/6] Pin Atlas Local test image in the Dockerfile Add the Atlas Local image as a second FROM line and read it via getImage(__dirname, 1) instead of hardcoding the tag, so Renovate keeps it up to date. --- packages/modules/mongodb/Dockerfile | 1 + .../modules/mongodb/src/mongodb-atlas-local-container.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/modules/mongodb/Dockerfile b/packages/modules/mongodb/Dockerfile index 1c757679a..a11cb3936 100644 --- a/packages/modules/mongodb/Dockerfile +++ b/packages/modules/mongodb/Dockerfile @@ -1 +1,2 @@ FROM mongo:8.2.11 +FROM mongodb/mongodb-atlas-local:8.2.2 diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index 35da60015..9d536ff92 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -1,8 +1,9 @@ import mongoose from "mongoose"; import { IntervalRetry } from "../../../testcontainers/src/common"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; -const IMAGE = "mongodb/mongodb-atlas-local:8.2.2"; +const IMAGE = getImage(__dirname, 1); const ATLAS_SEARCH_INDEX = { mappings: { dynamic: false, From c09787527d808d8a0da1caaeac4d28ccd1951e2c Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 20:53:42 +0900 Subject: [PATCH 5/6] Document the intentional MONGODB_INITDB_ env var naming The Atlas Local image uses MONGODB_INITDB_* rather than the MONGO_INITDB_* used by mongodb-container.ts; add a comment so it is not "fixed" to match the sibling file. --- packages/modules/mongodb/src/mongodb-atlas-local-container.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts index c81f6f23e..1b940fa87 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts @@ -28,6 +28,8 @@ export class MongoDBAtlasLocalContainer extends GenericContainer { public override async start(): Promise { if (this.username && this.password) { + // Note: the Atlas Local image uses MONGODB_INITDB_* (vs MONGO_INITDB_* in mongodb-container.ts). + // This difference is intentional — do not "fix" it to match the sibling file. this.withEnvironment({ MONGODB_INITDB_ROOT_USERNAME: this.username, MONGODB_INITDB_ROOT_PASSWORD: this.password, From ffe34f6ec5df4bc6f5cc51a57487d6c104f4fed9 Mon Sep 17 00:00:00 2001 From: Jake Son Date: Wed, 1 Jul 2026 20:53:55 +0900 Subject: [PATCH 6/6] Move Atlas Search codeinclude marker inside the test body Keep the marker inside the it() body like the other snippets in the file, so the generated doc example excludes the test wrapper and try/finally. --- .../modules/mongodb/src/mongodb-atlas-local-container.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index 9d536ff92..fa6dd7a3e 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -91,8 +91,8 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { await db.close(); }); - // createAtlasIndexAndSearchIt { it("should create an atlas search index and search it", async () => { + // createAtlasIndexAndSearchIt { await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(IMAGE).start(); const db = mongoose.createConnection(atlasLocalContainer.getConnectionString(), { @@ -151,6 +151,6 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { } finally { await db.close(); } + // } }); - // } });