-
-
Notifications
You must be signed in to change notification settings - Fork 265
Add MongoDB Atlas Local support #1396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cristianrgreco
merged 6 commits into
testcontainers:main
from
jbl428:feat/mongodb-atlas-local
Jul 1, 2026
+264
−1
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c68f390
Add MongoDB Atlas Local support
jbl428 91296cf
Name MongoDB doc sections after their container classes
jbl428 6448dae
Set authSource=admin on credentialed Atlas Local connection strings
jbl428 cb94c7b
Pin Atlas Local test image in the Dockerfile
jbl428 c097875
Document the intentional MONGODB_INITDB_ env var naming
jbl428 ffe34f6
Move Atlas Search codeinclude marker inside the test body
jbl428 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| FROM mongo:8.2.11 | ||
| FROM mongodb/mongodb-atlas-local:8.2.2 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { MongoDBAtlasLocalContainer, StartedMongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; | ||
| export { MongoDBContainer, StartedMongoDBContainer } from "./mongodb-container"; |
156 changes: 156 additions & 0 deletions
156
packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| 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 = getImage(__dirname, 1); | ||
| 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)}/?authSource=admin` | ||
| ); | ||
| expect(container.getDatabaseConnectionString()).toBe( | ||
| `mongodb://customUsername:customPassword@${container.getHost()}:${container.getMappedPort(27017)}/test?authSource=admin` | ||
| ); | ||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
|
|
||
| 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(), { | ||
| 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<string | undefined, Error>(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<Record<string, unknown> | 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(); | ||
| } | ||
| // } | ||
| }); | ||
| }); | ||
74 changes: 74 additions & 0 deletions
74
packages/modules/mongodb/src/mongodb-atlas-local-container.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| 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<StartedMongoDBAtlasLocalContainer> { | ||
| 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, | ||
|
cristianrgreco marked this conversation as resolved.
|
||
| 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; | ||
| url.searchParams.set("authSource", "admin"); | ||
| } | ||
|
|
||
| return url.toString(); | ||
|
cristianrgreco marked this conversation as resolved.
|
||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.