Skip to content

refactor: split test helpers #103

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
merged 5 commits into from
Apr 24, 2025
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
56 changes: 0 additions & 56 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,9 @@ type ToolInfo = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
export interface IntegrationTest {
mcpClient: () => Client;
mcpServer: () => Server;
mongoClient: () => MongoClient;
connectionString: () => string;
connectMcpClient: () => Promise<void>;
randomDbName: () => string;
}

export function setupIntegrationTest(userConfig: UserConfig = config): IntegrationTest {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we were to mix MongoDB + Atlas flows? would that be its own describe statement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would need to do something like describeAtlasMongoDB(...)

let mongoCluster: runner.MongoCluster | undefined;
let mongoClient: MongoClient | undefined;

let mcpClient: Client | undefined;
let mcpServer: Server | undefined;

Expand Down Expand Up @@ -89,55 +82,6 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati
mcpServer = undefined;
});

afterEach(async () => {
await mcpServer?.session.close();
config.connectionString = undefined;

await mongoClient?.close();
mongoClient = undefined;
});

beforeAll(async function () {
// Downloading Windows executables in CI takes a long time because
// they include debug symbols...
const tmpDir = path.join(__dirname, "..", "tmp");
await fs.mkdir(tmpDir, { recursive: true });

// On Windows, we may have a situation where mongod.exe is not fully released by the OS
// before we attempt to run it again, so we add a retry.
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
for (let i = 0; i < 10; i++) {
try {
mongoCluster = await MongoCluster.start({
tmpDir: dbsDir,
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
topology: "standalone",
});

return;
} catch (err) {
if (i < 5) {
// Just wait a little bit and retry
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
// If we still fail after 5 seconds, try another db dir
console.error(
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
);
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
}
}
}

throw new Error("Failed to start cluster after 10 attempts");
}, 120_000);

afterAll(async function () {
await mongoCluster?.close();
mongoCluster = undefined;
});

const getMcpClient = () => {
if (!mcpClient) {
throw new Error("beforeEach() hook not ran yet");
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export function describeAtlas(name: number | string | Function | jest.FunctionLike, fn: IntegrationTestFunction) {
export function describeWithAtlas(name: number | string | Function | jest.FunctionLike, fn: IntegrationTestFunction) {
const testDefinition = () => {
const integration = setupIntegrationTest();
describe(name, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -8,9 +10,7 @@ import {
dbOperationInvalidArgTests,
} from "../../../helpers.js";

describe("createCollection tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("createCollection tool", (integration) => {
validateToolMetadata(
integration,
"create-collection",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/create/createIndex.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -8,9 +10,7 @@ import {
} from "../../../helpers.js";
import { IndexDirection } from "mongodb";

describe("createIndex tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("createIndex tool", (integration) => {
validateToolMetadata(integration, "create-index", "Create an index for a collection", [
...dbOperationParameters,
{
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/create/insertMany.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -7,9 +9,7 @@ import {
validateThrowsForInvalidArguments,
} from "../../../helpers.js";

describe("insertMany tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("insertMany tool", (integration) => {
validateToolMetadata(integration, "insert-many", "Insert an array of documents into a MongoDB collection", [
...dbOperationParameters,
{
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/delete/deleteMany.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -7,9 +9,7 @@ import {
validateThrowsForInvalidArguments,
} from "../../../helpers.js";

describe("deleteMany tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("deleteMany tool", (integration) => {
validateToolMetadata(
integration,
"delete-many",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -8,9 +10,7 @@ import {
dbOperationInvalidArgTests,
} from "../../../helpers.js";

describe("dropCollection tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("dropCollection tool", (integration) => {
validateToolMetadata(
integration,
"drop-collection",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/delete/dropDatabase.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -8,9 +10,7 @@ import {
dbOperationInvalidArgTests,
} from "../../../helpers.js";

describe("dropDatabase tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("dropDatabase tool", (integration) => {
validateToolMetadata(
integration,
"drop-database",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseElements,
getResponseContent,
Expand All @@ -12,9 +14,7 @@ import { Document } from "bson";
import { OptionalId } from "mongodb";
import { SimplifiedSchema } from "mongodb-schema";

describe("collectionSchema tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("collectionSchema tool", (integration) => {
validateToolMetadata(
integration,
"collection-schema",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
setupIntegrationTest,
Expand All @@ -9,9 +11,7 @@ import {
} from "../../../helpers.js";
import * as crypto from "crypto";

describe("collectionStorageSize tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("collectionStorageSize tool", (integration) => {
validateToolMetadata(
integration,
"collection-storage-size",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/metadata/connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import { getResponseContent, setupIntegrationTest, validateToolMetadata } from "../../../helpers.js";

import { config } from "../../../../../src/config.js";

describe("Connect tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("Connect tool", (integration) => {
validateToolMetadata(integration, "connect", "Connect to a MongoDB instance", [
{
name: "options",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseElements,
getResponseContent,
Expand All @@ -8,9 +10,7 @@ import {
dbOperationInvalidArgTests,
} from "../../../helpers.js";

describe("listCollections tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("listCollections tool", (integration) => {
validateToolMetadata(integration, "list-collections", "List all collections for a given database", [
{ name: "database", description: "Database name", type: "string", required: true },
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseElements,
getParameters,
Expand All @@ -6,8 +8,7 @@ import {
} from "../../../helpers.js";
import { toIncludeSameMembers } from "jest-extended";

describe("listDatabases tool", () => {
const integration = setupIntegrationTest();
describeMongoDB("listDatabases tool", (integration) => {
const defaultDatabases = ["admin", "config", "local"];

it("should have correct metadata", async () => {
Expand Down
113 changes: 113 additions & 0 deletions tests/integration/tools/mongodb/mongodbHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import runner, { MongoCluster } from "mongodb-runner";
import path from "path";
import fs from "fs/promises";
import { MongoClient, ObjectId } from "mongodb";
import { IntegrationTest, setupIntegrationTest } from "../../helpers.js";
import { UserConfig, config } from "../../../../src/config.js";

interface MongoDBIntegrationTest {
mongoClient: () => MongoClient;
connectionString: () => string;
connectMcpClient: () => Promise<void>;
randomDbName: () => string;
}

export function describeWithMongoDB(
name: number | string | Function | jest.FunctionLike,
fn: (integration: IntegrationTest & MongoDBIntegrationTest) => void
): void {
describe("mongodb", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we benefit from having this parent MongoDB describe? we could also set the variables inside the child

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason why I added the main one was because need separation of beforeAll/AfterAll from main setup and MongoDB one other ordering is not correct

const integration = setupIntegrationTest();
const mdbIntegration = setupMongoDBIntegrationTest(integration);
describe(name, () => {
fn({ ...integration, ...mdbIntegration });
});
});
}

export function setupMongoDBIntegrationTest(
integration: IntegrationTest,
userConfig: UserConfig = config
): MongoDBIntegrationTest {
let mongoCluster: runner.MongoCluster | undefined;
let mongoClient: MongoClient | undefined;
let randomDbName: string;

beforeEach(async () => {
randomDbName = new ObjectId().toString();
});

afterEach(async () => {
await integration.mcpServer().session.close();
config.connectionString = undefined;

await mongoClient?.close();
mongoClient = undefined;
});

beforeAll(async function () {
// Downloading Windows executables in CI takes a long time because
// they include debug symbols...
const tmpDir = path.join(__dirname, "..", "..", "..", "tmp");
await fs.mkdir(tmpDir, { recursive: true });

// On Windows, we may have a situation where mongod.exe is not fully released by the OS
// before we attempt to run it again, so we add a retry.
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
for (let i = 0; i < 10; i++) {
try {
mongoCluster = await MongoCluster.start({
tmpDir: dbsDir,
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
topology: "standalone",
});

return;
} catch (err) {
if (i < 5) {
// Just wait a little bit and retry
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
// If we still fail after 5 seconds, try another db dir
console.error(
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
);
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
}
}
}

throw new Error("Failed to start cluster after 10 attempts");
}, 120_000);

afterAll(async function () {
await mongoCluster?.close();
mongoCluster = undefined;
});

const getConnectionString = () => {
if (!mongoCluster) {
throw new Error("beforeAll() hook not ran yet");
}

return mongoCluster.connectionString;
};

return {
mongoClient: () => {
if (!mongoClient) {
mongoClient = new MongoClient(getConnectionString());
}
return mongoClient;
},
connectionString: getConnectionString,
connectMcpClient: async () => {
await integration.mcpClient().callTool({
name: "connect",
arguments: { options: [{ connectionString: getConnectionString() }] },
});
},
randomDbName: () => randomDbName,
};
}
6 changes: 3 additions & 3 deletions tests/integration/tools/mongodb/read/count.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { describeMongoDB } from "../mongodbHelpers.js";

import {
getResponseContent,
dbOperationParameters,
Expand All @@ -7,9 +9,7 @@ import {
validateThrowsForInvalidArguments,
} from "../../../helpers.js";

describe("count tool", () => {
const integration = setupIntegrationTest();

describeMongoDB("count tool", (integration) => {
validateToolMetadata(integration, "count", "Gets the number of documents in a MongoDB collection", [
{
name: "query",
Expand Down
Loading