Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion docs/modules/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
npm install @testcontainers/mongodb --save-dev
```

## Examples
## MongoDBContainer

These examples use the following libraries:

Expand All @@ -27,3 +27,34 @@ Choose an image from the [container registry](https://hub.docker.com/_/mongo) an
<!--codeinclude-->
[](../../packages/modules/mongodb/src/mongodb-container.test.ts) inside_block:connectWithCredentials
<!--/codeinclude-->

## MongoDBAtlasLocalContainer

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

<!--codeinclude-->
[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocal
<!--/codeinclude-->

### Get a database connection string

<!--codeinclude-->
[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocalDatabase
<!--/codeinclude-->

### Connect with credentials

<!--codeinclude-->
[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:connectAtlasLocalWithCredentials
<!--/codeinclude-->

### Create an Atlas Search index and search it

<!--codeinclude-->
[](../../packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts) inside_block:createAtlasIndexAndSearchIt
<!--/codeinclude-->
1 change: 1 addition & 0 deletions packages/modules/mongodb/Dockerfile
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
1 change: 1 addition & 0 deletions packages/modules/mongodb/src/index.ts
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 packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts
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 () => {
Comment thread
cristianrgreco marked this conversation as resolved.
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 packages/modules/mongodb/src/mongodb-atlas-local-container.ts
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,
Comment thread
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();
Comment thread
cristianrgreco marked this conversation as resolved.
}
}
Loading