diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 79a679a0..895970a6 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -23,16 +23,9 @@ type ToolInfo = Awaited>["tools"][number]; export interface IntegrationTest { mcpClient: () => Client; mcpServer: () => Server; - mongoClient: () => MongoClient; - connectionString: () => string; - connectMcpClient: () => Promise; - randomDbName: () => string; } export function setupIntegrationTest(userConfig: UserConfig = config): IntegrationTest { - let mongoCluster: runner.MongoCluster | undefined; - let mongoClient: MongoClient | undefined; - let mcpClient: Client | undefined; let mcpServer: Server | undefined; @@ -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"); diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index f7d4802d..36b88c1e 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -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, () => { diff --git a/tests/integration/tools/mongodb/create/createCollection.test.ts b/tests/integration/tools/mongodb/create/createCollection.test.ts index a03c8ed3..1735bad7 100644 --- a/tests/integration/tools/mongodb/create/createCollection.test.ts +++ b/tests/integration/tools/mongodb/create/createCollection.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -8,9 +10,7 @@ import { dbOperationInvalidArgTests, } from "../../../helpers.js"; -describe("createCollection tool", () => { - const integration = setupIntegrationTest(); - +describeMongoDB("createCollection tool", (integration) => { validateToolMetadata( integration, "create-collection", diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index 1dcc1ecd..c2c12417 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -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, { diff --git a/tests/integration/tools/mongodb/create/insertMany.test.ts b/tests/integration/tools/mongodb/create/insertMany.test.ts index f549fbbc..9668647f 100644 --- a/tests/integration/tools/mongodb/create/insertMany.test.ts +++ b/tests/integration/tools/mongodb/create/insertMany.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -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, { diff --git a/tests/integration/tools/mongodb/delete/deleteMany.test.ts b/tests/integration/tools/mongodb/delete/deleteMany.test.ts index accbe218..e5db88f1 100644 --- a/tests/integration/tools/mongodb/delete/deleteMany.test.ts +++ b/tests/integration/tools/mongodb/delete/deleteMany.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -7,9 +9,7 @@ import { validateThrowsForInvalidArguments, } from "../../../helpers.js"; -describe("deleteMany tool", () => { - const integration = setupIntegrationTest(); - +describeMongoDB("deleteMany tool", (integration) => { validateToolMetadata( integration, "delete-many", diff --git a/tests/integration/tools/mongodb/delete/dropCollection.test.ts b/tests/integration/tools/mongodb/delete/dropCollection.test.ts index 0044231d..b2b61f65 100644 --- a/tests/integration/tools/mongodb/delete/dropCollection.test.ts +++ b/tests/integration/tools/mongodb/delete/dropCollection.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -8,9 +10,7 @@ import { dbOperationInvalidArgTests, } from "../../../helpers.js"; -describe("dropCollection tool", () => { - const integration = setupIntegrationTest(); - +describeMongoDB("dropCollection tool", (integration) => { validateToolMetadata( integration, "drop-collection", diff --git a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts index 6ed31afb..0b06f532 100644 --- a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +++ b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -8,9 +10,7 @@ import { dbOperationInvalidArgTests, } from "../../../helpers.js"; -describe("dropDatabase tool", () => { - const integration = setupIntegrationTest(); - +describeMongoDB("dropDatabase tool", (integration) => { validateToolMetadata( integration, "drop-database", diff --git a/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts b/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts index 339dd113..7a14979b 100644 --- a/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +++ b/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseElements, getResponseContent, @@ -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", diff --git a/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts b/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts index 4af84030..fb2259bd 100644 --- a/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +++ b/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, setupIntegrationTest, @@ -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", diff --git a/tests/integration/tools/mongodb/metadata/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts index 3f28a66d..fc80a1be 100644 --- a/tests/integration/tools/mongodb/metadata/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -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", diff --git a/tests/integration/tools/mongodb/metadata/listCollections.test.ts b/tests/integration/tools/mongodb/metadata/listCollections.test.ts index f6fb9bc0..b3e9a7d8 100644 --- a/tests/integration/tools/mongodb/metadata/listCollections.test.ts +++ b/tests/integration/tools/mongodb/metadata/listCollections.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseElements, getResponseContent, @@ -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 }, ]); diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts index 6d8ee7a3..75f039ea 100644 --- a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseElements, getParameters, @@ -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 () => { diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts new file mode 100644 index 00000000..69814f57 --- /dev/null +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -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; + randomDbName: () => string; +} + +export function describeWithMongoDB( + name: number | string | Function | jest.FunctionLike, + fn: (integration: IntegrationTest & MongoDBIntegrationTest) => void +): void { + describe("mongodb", () => { + 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, + }; +} diff --git a/tests/integration/tools/mongodb/read/count.test.ts b/tests/integration/tools/mongodb/read/count.test.ts index 869c1ea2..cf00c750 100644 --- a/tests/integration/tools/mongodb/read/count.test.ts +++ b/tests/integration/tools/mongodb/read/count.test.ts @@ -1,3 +1,5 @@ +import { describeMongoDB } from "../mongodbHelpers.js"; + import { getResponseContent, dbOperationParameters, @@ -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",