diff --git a/tests/integration/common/connectionManager.oidc.test.ts b/tests/integration/common/connectionManager.oidc.test.ts index 9f30cf32e..3d949bc88 100644 --- a/tests/integration/common/connectionManager.oidc.test.ts +++ b/tests/integration/common/connectionManager.oidc.test.ts @@ -137,14 +137,19 @@ describe.skipIf(process.platform !== "linux")("ConnectionManager OIDC Tests", as addCb?.(oidcIt); }, - () => oidcConfig, - () => ({ - ...setupDriverConfig({ - config: oidcConfig, - defaults: {}, - }), - }), - { runner: true, downloadOptions: { enterprise: true, version: mongodbVersion }, serverArgs } + { + getUserConfig: () => oidcConfig, + getDriverOptions: () => + setupDriverConfig({ + config: oidcConfig, + defaults: {}, + }), + downloadOptions: { + runner: true, + downloadOptions: { enterprise: true, version: mongodbVersion }, + serverArgs, + }, + } ); } diff --git a/tests/integration/elicitation.test.ts b/tests/integration/elicitation.test.ts index 0626fd51a..d4664882b 100644 --- a/tests/integration/elicitation.test.ts +++ b/tests/integration/elicitation.test.ts @@ -1,46 +1,194 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { describe, it, expect } from "vitest"; -import { defaultDriverOptions, type UserConfig } from "../../src/common/config.js"; -import { defaultTestConfig, setupIntegrationTest } from "./helpers.js"; +import { describe, it, expect, afterEach } from "vitest"; +import { type UserConfig } from "../../src/common/config.js"; +import { defaultTestConfig } from "./helpers.js"; import { Elicitation } from "../../src/elicitation.js"; import { createMockElicitInput } from "../utils/elicitationMocks.js"; +import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js"; + +function createTestConfig(config: Partial = {}): UserConfig { + return { + ...defaultTestConfig, + telemetry: "disabled", + // Add fake API credentials so Atlas tools get registered + apiClientId: "test-client-id", + apiClientSecret: "test-client-secret", + ...config, + }; +} describe("Elicitation Integration Tests", () => { - function createTestConfig(config: Partial = {}): UserConfig { - return { - ...defaultTestConfig, - telemetry: "disabled", - // Add fake API credentials so Atlas tools get registered - apiClientId: "test-client-id", - apiClientSecret: "test-client-secret", - ...config, - }; - } - - describe("with elicitation support", () => { - const mockElicitInput = createMockElicitInput(); - const integration = setupIntegrationTest( - () => createTestConfig(), - () => defaultDriverOptions, - { elicitInput: mockElicitInput } - ); - - describe("tools requiring confirmation by default", () => { - it("should request confirmation for drop-database tool and proceed when confirmed", async () => { - mockElicitInput.confirmYes(); + const mockElicitInput = createMockElicitInput(); + afterEach(() => { + mockElicitInput.clear(); + }); + + describeWithMongoDB( + "with elicitation support", + (integration) => { + describe("tools requiring confirmation by default", () => { + it("should request confirmation for drop-database tool and proceed when confirmed", async () => { + mockElicitInput.confirmYes(); + + const result = await integration.mcpClient().callTool({ + name: "drop-database", + arguments: { database: "test-db" }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + message: expect.stringContaining("You are about to drop the `test-db` database"), + requestedSchema: Elicitation.CONFIRMATION_SCHEMA, + }); + + // Should attempt to execute (will fail due to no connection, but confirms flow worked) + expect(result.isError).toBe(true); + expect(result.content).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "text", + text: expect.stringContaining("You need to connect to a MongoDB instance"), + }), + ]) + ); + }); + + it("should not proceed when user declines confirmation", async () => { + mockElicitInput.confirmNo(); + + const result = await integration.mcpClient().callTool({ + name: "drop-database", + arguments: { database: "test-db" }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(result.isError).toBeFalsy(); + expect(result.content).toEqual([ + { + type: "text", + text: "User did not confirm the execution of the `drop-database` tool so the operation was not performed.", + }, + ]); + }); + + it("should request confirmation for drop-collection tool", async () => { + mockElicitInput.confirmYes(); + + await integration.mcpClient().callTool({ + name: "drop-collection", + arguments: { database: "test-db", collection: "test-collection" }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + message: expect.stringContaining("You are about to drop the `test-collection` collection"), + requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), + }); + }); + + it("should request confirmation for delete-many tool", async () => { + mockElicitInput.confirmYes(); + + await integration.mcpClient().callTool({ + name: "delete-many", + arguments: { + database: "test-db", + collection: "test-collection", + filter: { status: "inactive" }, + }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + message: expect.stringContaining("You are about to delete documents"), + requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), + }); + }); + it("should request confirmation for create-db-user tool", async () => { + mockElicitInput.confirmYes(); + + await integration.mcpClient().callTool({ + name: "atlas-create-db-user", + arguments: { + projectId: "507f1f77bcf86cd799439011", // Valid 24-char hex string + username: "test-user", + roles: [{ roleName: "read", databaseName: "test-db" }], + }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + message: expect.stringContaining("You are about to create a database user"), + requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), + }); + }); + + it("should request confirmation for create-access-list tool", async () => { + mockElicitInput.confirmYes(); + + await integration.mcpClient().callTool({ + name: "atlas-create-access-list", + arguments: { + projectId: "507f1f77bcf86cd799439011", // Valid 24-char hex string + ipAddresses: ["192.168.1.1"], + }, + }); + + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + message: expect.stringContaining( + "You are about to add the following entries to the access list" + ), + requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), + }); + }); + }); + + describe("tools not requiring confirmation by default", () => { + it("should not request confirmation for read operations", async () => { + const result = await integration.mcpClient().callTool({ + name: "list-databases", + arguments: {}, + }); + + expect(mockElicitInput.mock).not.toHaveBeenCalled(); + // Should fail with connection error since we're not connected + expect(result.isError).toBe(true); + }); + + it("should not request confirmation for find operations", async () => { + const result = await integration.mcpClient().callTool({ + name: "find", + arguments: { + database: "test-db", + collection: "test-collection", + }, + }); + + expect(mockElicitInput.mock).not.toHaveBeenCalled(); + // Should fail with connection error since we're not connected + expect(result.isError).toBe(true); + }); + }); + }, + { + getUserConfig: () => createTestConfig(), + getMockElicitationInput: () => mockElicitInput, + } + ); + + describeWithMongoDB( + "without elicitation support", + (integration) => { + it("should proceed without confirmation for default confirmation-required tools when client lacks elicitation support", async () => { const result = await integration.mcpClient().callTool({ name: "drop-database", arguments: { database: "test-db" }, }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringContaining("You are about to drop the `test-db` database"), - requestedSchema: Elicitation.CONFIRMATION_SCHEMA, - }); - - // Should attempt to execute (will fail due to no connection, but confirms flow worked) + // Note: No mock assertions needed since elicitation is disabled + // Should fail with connection error since we're not connected, but confirms flow bypassed confirmation expect(result.isError).toBe(true); expect(result.content).toEqual( expect.arrayContaining([ @@ -51,265 +199,126 @@ describe("Elicitation Integration Tests", () => { ]) ); }); - - it("should not proceed when user declines confirmation", async () => { - mockElicitInput.confirmNo(); - - const result = await integration.mcpClient().callTool({ - name: "drop-database", - arguments: { database: "test-db" }, - }); - - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(result.isError).toBeFalsy(); - expect(result.content).toEqual([ - { - type: "text", - text: "User did not confirm the execution of the `drop-database` tool so the operation was not performed.", - }, - ]); - }); - - it("should request confirmation for drop-collection tool", async () => { + }, + { + getUserConfig: () => createTestConfig(), + getClientCapabilities: () => ({}), + } + ); + + describeWithMongoDB( + "custom confirmation configuration", + (integration) => { + it("should confirm with a generic message with custom configurations for other tools", async () => { mockElicitInput.confirmYes(); await integration.mcpClient().callTool({ - name: "drop-collection", - arguments: { database: "test-db", collection: "test-collection" }, + name: "list-databases", + arguments: {}, }); expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringContaining("You are about to drop the `test-collection` collection"), + message: expect.stringMatching( + /You are about to execute the `list-databases` tool which requires additional confirmation. Would you like to proceed\?/ + ), requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), }); }); - it("should request confirmation for delete-many tool", async () => { - mockElicitInput.confirmYes(); - - await integration.mcpClient().callTool({ - name: "delete-many", - arguments: { - database: "test-db", - collection: "test-collection", - filter: { status: "inactive" }, - }, + it("should not request confirmation when tool is removed from default confirmationRequiredTools", async () => { + const result = await integration.mcpClient().callTool({ + name: "drop-database", + arguments: { database: "test-db" }, }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringContaining("You are about to delete documents"), - requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), - }); + expect(mockElicitInput.mock).not.toHaveBeenCalled(); + // Should fail with connection error since we're not connected + expect(result.isError).toBe(true); }); - - it("should request confirmation for create-db-user tool", async () => { + }, + { + getUserConfig: () => createTestConfig({ confirmationRequiredTools: ["list-databases"] }), + getMockElicitationInput: () => mockElicitInput, + } + ); + + describeWithMongoDB( + "confirmation message content validation", + (integration) => { + it("should include specific details in create-db-user confirmation", async () => { mockElicitInput.confirmYes(); await integration.mcpClient().callTool({ name: "atlas-create-db-user", arguments: { projectId: "507f1f77bcf86cd799439011", // Valid 24-char hex string - username: "test-user", - roles: [{ roleName: "read", databaseName: "test-db" }], + username: "myuser", + password: "mypassword", + roles: [ + { roleName: "readWrite", databaseName: "mydb" }, + { roleName: "read", databaseName: "logs", collectionName: "events" }, + ], + clusters: ["cluster1", "cluster2"], }, }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringContaining("You are about to create a database user"), + message: expect.stringMatching(/project.*507f1f77bcf86cd799439011/), requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), }); }); - it("should request confirmation for create-access-list tool", async () => { + it("should include filter details in delete-many confirmation", async () => { mockElicitInput.confirmYes(); await integration.mcpClient().callTool({ - name: "atlas-create-access-list", + name: "delete-many", arguments: { - projectId: "507f1f77bcf86cd799439011", // Valid 24-char hex string - ipAddresses: ["192.168.1.1"], + database: "mydb", + collection: "users", + filter: { status: "inactive", lastLogin: { $lt: "2023-01-01" } }, }, }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringContaining("You are about to add the following entries to the access list"), + message: expect.stringMatching(/mydb.*database/), requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), }); }); - }); - - describe("tools not requiring confirmation by default", () => { - it("should not request confirmation for read operations", async () => { - const result = await integration.mcpClient().callTool({ - name: "list-databases", - arguments: {}, - }); - - expect(mockElicitInput.mock).not.toHaveBeenCalled(); - // Should fail with connection error since we're not connected - expect(result.isError).toBe(true); - }); + }, + { + getUserConfig: () => createTestConfig(), + getMockElicitationInput: () => mockElicitInput, + } + ); + + describeWithMongoDB( + "error handling in confirmation flow", + (integration) => { + it("should handle confirmation errors gracefully", async () => { + mockElicitInput.rejectWith(new Error("Confirmation service unavailable")); - it("should not request confirmation for find operations", async () => { const result = await integration.mcpClient().callTool({ - name: "find", - arguments: { - database: "test-db", - collection: "test-collection", - }, + name: "drop-database", + arguments: { database: "test-db" }, }); - expect(mockElicitInput.mock).not.toHaveBeenCalled(); - // Should fail with connection error since we're not connected + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); expect(result.isError).toBe(true); + expect(result.content).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "text", + text: expect.stringContaining("Error running drop-database"), + }), + ]) + ); }); - }); - }); - - describe("without elicitation support", () => { - const integration = setupIntegrationTest( - () => createTestConfig(), - () => defaultDriverOptions, - { getClientCapabilities: () => ({}) } - ); - - it("should proceed without confirmation for default confirmation-required tools when client lacks elicitation support", async () => { - const result = await integration.mcpClient().callTool({ - name: "drop-database", - arguments: { database: "test-db" }, - }); - - // Note: No mock assertions needed since elicitation is disabled - // Should fail with connection error since we're not connected, but confirms flow bypassed confirmation - expect(result.isError).toBe(true); - expect(result.content).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining("You need to connect to a MongoDB instance"), - }), - ]) - ); - }); - }); - - describe("custom confirmation configuration", () => { - const mockElicitInput = createMockElicitInput(); - const integration = setupIntegrationTest( - () => createTestConfig({ confirmationRequiredTools: ["list-databases"] }), - () => defaultDriverOptions, - { elicitInput: mockElicitInput } - ); - - it("should confirm with a generic message with custom configurations for other tools", async () => { - mockElicitInput.confirmYes(); - - await integration.mcpClient().callTool({ - name: "list-databases", - arguments: {}, - }); - - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringMatching( - /You are about to execute the `list-databases` tool which requires additional confirmation. Would you like to proceed\?/ - ), - requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), - }); - }); - - it("should not request confirmation when tool is removed from default confirmationRequiredTools", async () => { - const result = await integration.mcpClient().callTool({ - name: "drop-database", - arguments: { database: "test-db" }, - }); - - expect(mockElicitInput.mock).not.toHaveBeenCalled(); - // Should fail with connection error since we're not connected - expect(result.isError).toBe(true); - }); - }); - - describe("confirmation message content validation", () => { - const mockElicitInput = createMockElicitInput(); - const integration = setupIntegrationTest( - () => createTestConfig(), - () => defaultDriverOptions, - { elicitInput: mockElicitInput } - ); - - it("should include specific details in create-db-user confirmation", async () => { - mockElicitInput.confirmYes(); - - await integration.mcpClient().callTool({ - name: "atlas-create-db-user", - arguments: { - projectId: "507f1f77bcf86cd799439011", // Valid 24-char hex string - username: "myuser", - password: "mypassword", - roles: [ - { roleName: "readWrite", databaseName: "mydb" }, - { roleName: "read", databaseName: "logs", collectionName: "events" }, - ], - clusters: ["cluster1", "cluster2"], - }, - }); - - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringMatching(/project.*507f1f77bcf86cd799439011/), - requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), - }); - }); - - it("should include filter details in delete-many confirmation", async () => { - mockElicitInput.confirmYes(); - - await integration.mcpClient().callTool({ - name: "delete-many", - arguments: { - database: "mydb", - collection: "users", - filter: { status: "inactive", lastLogin: { $lt: "2023-01-01" } }, - }, - }); - - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - message: expect.stringMatching(/mydb.*database/), - requestedSchema: expect.objectContaining(Elicitation.CONFIRMATION_SCHEMA), - }); - }); - }); - - describe("error handling in confirmation flow", () => { - const mockElicitInput = createMockElicitInput(); - const integration = setupIntegrationTest( - () => createTestConfig(), - () => defaultDriverOptions, - { elicitInput: mockElicitInput } - ); - - it("should handle confirmation errors gracefully", async () => { - mockElicitInput.rejectWith(new Error("Confirmation service unavailable")); - - const result = await integration.mcpClient().callTool({ - name: "drop-database", - arguments: { database: "test-db" }, - }); - - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(result.isError).toBe(true); - expect(result.content).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: "text", - text: expect.stringContaining("Error running drop-database"), - }), - ]) - ); - }); - }); + }, + { + getUserConfig: () => createTestConfig(), + getMockElicitationInput: () => mockElicitInput, + } + ); }); diff --git a/tests/integration/indexCheck.test.ts b/tests/integration/indexCheck.test.ts index 438cd86fe..b99209201 100644 --- a/tests/integration/indexCheck.test.ts +++ b/tests/integration/indexCheck.test.ts @@ -313,10 +313,12 @@ describe("IndexCheck integration tests", () => { }); }); }, - () => ({ - ...defaultTestConfig, - indexCheck: true, // Enable indexCheck - }) + { + getUserConfig: () => ({ + ...defaultTestConfig, + indexCheck: true, // Enable indexCheck + }), + } ); }); @@ -424,10 +426,12 @@ describe("IndexCheck integration tests", () => { expect(content).not.toContain("Index check failed"); }); }, - () => ({ - ...defaultTestConfig, - indexCheck: false, // Disable indexCheck - }) + { + getUserConfig: () => ({ + ...defaultTestConfig, + indexCheck: false, // Disable indexCheck + }), + } ); }); @@ -456,10 +460,12 @@ describe("IndexCheck integration tests", () => { expect(response.isError).toBeFalsy(); }); }, - () => ({ - ...defaultTestConfig, - // indexCheck not specified, should default to false - }) + { + getUserConfig: () => ({ + ...defaultTestConfig, + // indexCheck not specified, should default to false + }), + } ); }); }); diff --git a/tests/integration/resources/exportedData.test.ts b/tests/integration/resources/exportedData.test.ts index 6e361bf03..5903fb23d 100644 --- a/tests/integration/resources/exportedData.test.ts +++ b/tests/integration/resources/exportedData.test.ts @@ -173,5 +173,7 @@ describeWithMongoDB( }); }); }, - () => userConfig + { + getUserConfig: () => userConfig, + } ); diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index ef98075a7..090df4a53 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -1,4 +1,4 @@ -import { defaultDriverOptions, defaultTestConfig, expectDefined, setupIntegrationTest } from "./helpers.js"; +import { defaultTestConfig, expectDefined } from "./helpers.js"; import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js"; import { describe, expect, it } from "vitest"; @@ -15,81 +15,84 @@ describe("Server integration test", () => { expect(atlasTools.length).toBeLessThanOrEqual(0); }); }, - () => ({ - ...defaultTestConfig, - apiClientId: undefined, - apiClientSecret: undefined, - }), - () => defaultDriverOptions + { + getUserConfig: () => ({ + ...defaultTestConfig, + apiClientId: undefined, + apiClientSecret: undefined, + }), + } ); - describe("with atlas", () => { - const integration = setupIntegrationTest( - () => ({ + describeWithMongoDB( + "with atlas", + (integration) => { + describe("list capabilities", () => { + it("should return positive number of tools and have some atlas tools", async () => { + const tools = await integration.mcpClient().listTools(); + expectDefined(tools); + expect(tools.tools.length).toBeGreaterThan(0); + + const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-")); + expect(atlasTools.length).toBeGreaterThan(0); + }); + + it("should return no prompts", async () => { + await expect(() => integration.mcpClient().listPrompts()).rejects.toMatchObject({ + message: "MCP error -32601: Method not found", + }); + }); + + it("should return capabilities", () => { + const capabilities = integration.mcpClient().getServerCapabilities(); + expectDefined(capabilities); + expectDefined(capabilities?.logging); + expectDefined(capabilities?.completions); + expectDefined(capabilities?.tools); + expectDefined(capabilities?.resources); + expect(capabilities.experimental).toBeUndefined(); + expect(capabilities.prompts).toBeUndefined(); + }); + }); + }, + { + getUserConfig: () => ({ ...defaultTestConfig, apiClientId: "test", apiClientSecret: "test", }), - () => defaultDriverOptions - ); + } + ); - describe("list capabilities", () => { - it("should return positive number of tools and have some atlas tools", async () => { + describeWithMongoDB( + "with read-only mode", + (integration) => { + it("should only register read and metadata operation tools when read-only mode is enabled", async () => { const tools = await integration.mcpClient().listTools(); expectDefined(tools); expect(tools.tools.length).toBeGreaterThan(0); - const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-")); - expect(atlasTools.length).toBeGreaterThan(0); - }); - - it("should return no prompts", async () => { - await expect(() => integration.mcpClient().listPrompts()).rejects.toMatchObject({ - message: "MCP error -32601: Method not found", - }); - }); + // Check that we have some tools available (the read and metadata ones) + expect(tools.tools.some((tool) => tool.name === "find")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "collection-schema")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "list-databases")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "atlas-list-orgs")).toBe(true); + expect(tools.tools.some((tool) => tool.name === "atlas-list-projects")).toBe(true); - it("should return capabilities", () => { - const capabilities = integration.mcpClient().getServerCapabilities(); - expectDefined(capabilities); - expectDefined(capabilities?.logging); - expectDefined(capabilities?.completions); - expectDefined(capabilities?.tools); - expectDefined(capabilities?.resources); - expect(capabilities.experimental).toBeUndefined(); - expect(capabilities.prompts).toBeUndefined(); + // Check that non-read tools are NOT available + expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "update-many")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "delete-one")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "drop-collection")).toBe(false); }); - }); - }); - - describe("with read-only mode", () => { - const integration = setupIntegrationTest( - () => ({ + }, + { + getUserConfig: () => ({ ...defaultTestConfig, readOnly: true, apiClientId: "test", apiClientSecret: "test", }), - () => defaultDriverOptions - ); - - it("should only register read and metadata operation tools when read-only mode is enabled", async () => { - const tools = await integration.mcpClient().listTools(); - expectDefined(tools); - expect(tools.tools.length).toBeGreaterThan(0); - - // Check that we have some tools available (the read and metadata ones) - expect(tools.tools.some((tool) => tool.name === "find")).toBe(true); - expect(tools.tools.some((tool) => tool.name === "collection-schema")).toBe(true); - expect(tools.tools.some((tool) => tool.name === "list-databases")).toBe(true); - expect(tools.tools.some((tool) => tool.name === "atlas-list-orgs")).toBe(true); - expect(tools.tools.some((tool) => tool.name === "atlas-list-projects")).toBe(true); - - // Check that non-read tools are NOT available - expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false); - expect(tools.tools.some((tool) => tool.name === "update-many")).toBe(false); - expect(tools.tools.some((tool) => tool.name === "delete-one")).toBe(false); - expect(tools.tools.some((tool) => tool.name === "drop-collection")).toBe(false); - }); - }); + } + ); }); diff --git a/tests/integration/tools/mongodb/connect/connect.test.ts b/tests/integration/tools/mongodb/connect/connect.test.ts index 46526fe5b..132e0fd90 100644 --- a/tests/integration/tools/mongodb/connect/connect.test.ts +++ b/tests/integration/tools/mongodb/connect/connect.test.ts @@ -1,13 +1,11 @@ import { describeWithMongoDB } from "../mongodbHelpers.js"; import { - defaultDriverOptions, getResponseContent, getResponseElements, validateThrowsForInvalidArguments, validateToolMetadata, } from "../../../helpers.js"; -import { config } from "../../../../../src/common/config.js"; -import { defaultTestConfig, setupIntegrationTest } from "../../../helpers.js"; +import { defaultTestConfig } from "../../../helpers.js"; import { beforeEach, describe, expect, it } from "vitest"; describeWithMongoDB( @@ -82,89 +80,88 @@ describeWithMongoDB( }); }); }, - (mdbIntegration) => ({ - ...config, - connectionString: mdbIntegration.connectionString(), - }) + { + getUserConfig: (mdbIntegration) => ({ + ...defaultTestConfig, + connectionString: mdbIntegration.connectionString(), + }), + } ); -describeWithMongoDB( - "Connect tool", - (integration) => { - validateToolMetadata( - integration, - "connect", - "Connect to a MongoDB instance. The config resource captures if the server is already connected to a MongoDB cluster. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new MongoDB cluster.", - [ - { - name: "connectionString", - description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)", - type: "string", - required: true, - }, - ] - ); +describeWithMongoDB("Connect tool", (integration) => { + validateToolMetadata( + integration, + "connect", + "Connect to a MongoDB instance. The config resource captures if the server is already connected to a MongoDB cluster. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new MongoDB cluster.", + [ + { + name: "connectionString", + description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)", + type: "string", + required: true, + }, + ] + ); - validateThrowsForInvalidArguments(integration, "connect", [{}, { connectionString: 123 }]); + validateThrowsForInvalidArguments(integration, "connect", [{}, { connectionString: 123 }]); - it("doesn't have the switch-connection tool registered", async () => { - const { tools } = await integration.mcpClient().listTools(); - const tool = tools.find((tool) => tool.name === "switch-connection"); - expect(tool).toBeUndefined(); - }); + it("doesn't have the switch-connection tool registered", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "switch-connection"); + expect(tool).toBeUndefined(); + }); - describe("with connection string", () => { - it("connects to the database", async () => { - const response = await integration.mcpClient().callTool({ - name: "connect", - arguments: { - connectionString: integration.connectionString(), - }, - }); - const content = getResponseContent(response.content); - expect(content).toContain("Successfully connected"); + describe("with connection string", () => { + it("connects to the database", async () => { + const response = await integration.mcpClient().callTool({ + name: "connect", + arguments: { + connectionString: integration.connectionString(), + }, }); + const content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); }); + }); - describe("with invalid connection string", () => { - it("returns error message", async () => { - const response = await integration.mcpClient().callTool({ - name: "connect", - arguments: { connectionString: "mangodb://localhost:12345" }, - }); - const content = getResponseContent(response.content); - expect(content).toContain("The configured connection string is not valid."); + describe("with invalid connection string", () => { + it("returns error message", async () => { + const response = await integration.mcpClient().callTool({ + name: "connect", + arguments: { connectionString: "mangodb://localhost:12345" }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("The configured connection string is not valid."); - // Should not suggest using the config connection string (because we don't have one) - expect(content).not.toContain("Your config lists a different connection string"); + // Should not suggest using the config connection string (because we don't have one) + expect(content).not.toContain("Your config lists a different connection string"); + }); + }); +}); + +describeWithMongoDB( + "Connect tool when disabled", + (integration) => { + it("is not suggested when querying MongoDB disconnected", async () => { + const response = await integration.mcpClient().callTool({ + name: "find", + arguments: { database: "some-db", collection: "some-collection" }, }); + + const elements = getResponseElements(response); + expect(elements).toHaveLength(2); + expect(elements[0]?.text).toContain( + "You need to connect to a MongoDB instance before you can access its data." + ); + expect(elements[1]?.text).toContain( + "There are no tools available to connect. Please update the configuration to include a connection string and restart the server." + ); }); }, - () => config -); - -describe("Connect tool when disabled", () => { - const integration = setupIntegrationTest( - () => ({ + { + getUserConfig: () => ({ ...defaultTestConfig, disabledTools: ["connect"], }), - () => defaultDriverOptions - ); - - it("is not suggested when querying MongoDB disconnected", async () => { - const response = await integration.mcpClient().callTool({ - name: "find", - arguments: { database: "some-db", collection: "some-collection" }, - }); - - const elements = getResponseElements(response); - expect(elements).toHaveLength(2); - expect(elements[0]?.text).toContain( - "You need to connect to a MongoDB instance before you can access its data." - ); - expect(elements[1]?.text).toContain( - "There are no tools available to connect. Please update the configuration to include a connection string and restart the server." - ); - }); -}); + } +); diff --git a/tests/integration/tools/mongodb/delete/dropIndex.test.ts b/tests/integration/tools/mongodb/delete/dropIndex.test.ts index a1aac591e..46360b81b 100644 --- a/tests/integration/tools/mongodb/delete/dropIndex.test.ts +++ b/tests/integration/tools/mongodb/delete/dropIndex.test.ts @@ -3,15 +3,12 @@ import type { Collection } from "mongodb"; import { databaseCollectionInvalidArgs, databaseCollectionParameters, - defaultDriverOptions, - defaultTestConfig, getDataFromUntrustedContent, getResponseContent, - setupIntegrationTest, validateThrowsForInvalidArguments, validateToolMetadata, } from "../../../helpers.js"; -import { describeWithMongoDB, setupMongoDBIntegrationTest } from "../mongodbHelpers.js"; +import { describeWithMongoDB } from "../mongodbHelpers.js"; import { createMockElicitInput } from "../../../../utils/elicitationMocks.js"; import { Elicitation } from "../../../../../src/elicitation.js"; @@ -113,69 +110,70 @@ describeWithMongoDB("drop-index tool", (integration) => { }); }); -describe("drop-index tool - when invoked via an elicitation enabled client", () => { - const mockElicitInput = createMockElicitInput(); - const mdbIntegration = setupMongoDBIntegrationTest(); - const integration = setupIntegrationTest( - () => defaultTestConfig, - () => defaultDriverOptions, - { elicitInput: mockElicitInput } - ); - let moviesCollection: Collection; - let indexName: string; +const mockElicitInput = createMockElicitInput(); - beforeEach(async () => { - moviesCollection = mdbIntegration.mongoClient().db("mflix").collection("movies"); - await moviesCollection.insertMany([ - { name: "Movie1", year: 1994 }, - { name: "Movie2", year: 2001 }, - ]); - indexName = await moviesCollection.createIndex({ year: 1 }); - await integration.mcpClient().callTool({ - name: "connect", - arguments: { - connectionString: mdbIntegration.connectionString(), - }, - }); - }); - - afterEach(async () => { - await moviesCollection.drop(); - }); +describeWithMongoDB( + "drop-index tool - when invoked via an elicitation enabled client", + (integration) => { + let moviesCollection: Collection; + let indexName: string; - it("should ask for confirmation before proceeding with tool call", async () => { - expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); - mockElicitInput.confirmYes(); - await integration.mcpClient().callTool({ - name: "drop-index", - arguments: { database: "mflix", collection: "movies", indexName }, + beforeEach(async () => { + moviesCollection = integration.mongoClient().db("mflix").collection("movies"); + await moviesCollection.insertMany([ + { name: "Movie1", year: 1994 }, + { name: "Movie2", year: 2001 }, + ]); + indexName = await moviesCollection.createIndex({ year: 1 }); + await integration.mcpClient().callTool({ + name: "connect", + arguments: { + connectionString: integration.connectionString(), + }, + }); }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - message: expect.stringContaining( - "You are about to drop the `year_1` index from the `mflix.movies` namespace" - ), - requestedSchema: Elicitation.CONFIRMATION_SCHEMA, + + afterEach(async () => { + await moviesCollection.drop(); }); - expect(await moviesCollection.listIndexes().toArray()).toHaveLength(1); - }); - it("should not drop the index if the confirmation was not provided", async () => { - expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); - mockElicitInput.confirmNo(); - await integration.mcpClient().callTool({ - name: "drop-index", - arguments: { database: "mflix", collection: "movies", indexName }, + it("should ask for confirmation before proceeding with tool call", async () => { + expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); + mockElicitInput.confirmYes(); + await integration.mcpClient().callTool({ + name: "drop-index", + arguments: { database: "mflix", collection: "movies", indexName }, + }); + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + message: expect.stringContaining( + "You are about to drop the `year_1` index from the `mflix.movies` namespace" + ), + requestedSchema: Elicitation.CONFIRMATION_SCHEMA, + }); + expect(await moviesCollection.listIndexes().toArray()).toHaveLength(1); }); - expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); - expect(mockElicitInput.mock).toHaveBeenCalledWith({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - message: expect.stringContaining( - "You are about to drop the `year_1` index from the `mflix.movies` namespace" - ), - requestedSchema: Elicitation.CONFIRMATION_SCHEMA, + + it("should not drop the index if the confirmation was not provided", async () => { + expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); + mockElicitInput.confirmNo(); + await integration.mcpClient().callTool({ + name: "drop-index", + arguments: { database: "mflix", collection: "movies", indexName }, + }); + expect(mockElicitInput.mock).toHaveBeenCalledTimes(1); + expect(mockElicitInput.mock).toHaveBeenCalledWith({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + message: expect.stringContaining( + "You are about to drop the `year_1` index from the `mflix.movies` namespace" + ), + requestedSchema: Elicitation.CONFIRMATION_SCHEMA, + }); + expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); }); - expect(await moviesCollection.listIndexes().toArray()).toHaveLength(2); - }); -}); + }, + { + getMockElicitationInput: () => mockElicitInput, + } +); diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index e3a332ae8..7c6da4874 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -16,6 +16,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from import { EJSON } from "bson"; import { MongoDBClusterProcess } from "./mongodbClusterProcess.js"; import type { MongoClusterConfiguration } from "./mongodbClusterProcess.js"; +import type { createMockElicitInput, MockClientCapabilities } from "../../../utils/elicitationMocks.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -66,22 +67,40 @@ export type MongoDBIntegrationTestCase = IntegrationTest & export type MongoSearchConfiguration = { search: true; image?: string }; +export type TestSuiteConfig = { + getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig; + getDriverOptions: (mdbIntegration: MongoDBIntegrationTest) => DriverOptions; + downloadOptions: MongoClusterConfiguration; + getMockElicitationInput?: () => ReturnType; + getClientCapabilities?: () => MockClientCapabilities; +}; + +const defaultTestSuiteConfig: TestSuiteConfig = { + getUserConfig: () => defaultTestConfig, + getDriverOptions: () => defaultDriverOptions, + downloadOptions: DEFAULT_MONGODB_PROCESS_OPTIONS, +}; + export function describeWithMongoDB( name: string, fn: (integration: MongoDBIntegrationTestCase) => void, - getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig, - getDriverOptions: (mdbIntegration: MongoDBIntegrationTest) => DriverOptions = () => defaultDriverOptions, - downloadOptions: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS + partialTestSuiteConfig?: Partial ): void { + const { getUserConfig, getDriverOptions, downloadOptions, getMockElicitationInput, getClientCapabilities } = { + ...defaultTestSuiteConfig, + ...partialTestSuiteConfig, + }; describe.skipIf(!MongoDBClusterProcess.isConfigurationSupportedInCurrentEnv(downloadOptions))(name, () => { const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions); + const mockElicitInput = getMockElicitationInput?.(); const integration = setupIntegrationTest( () => ({ ...getUserConfig(mdbIntegration), }), () => ({ ...getDriverOptions(mdbIntegration), - }) + }), + { elicitInput: mockElicitInput, getClientCapabilities } ); fn({ diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index 3f0a99a58..d585d5786 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -282,7 +282,9 @@ describeWithMongoDB( ); }); }, - () => ({ ...defaultTestConfig, maxDocumentsPerQuery: 20 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: 20 }), + } ); describeWithMongoDB( @@ -339,7 +341,9 @@ describeWithMongoDB( ); }); }, - () => ({ ...defaultTestConfig, maxBytesPerQuery: 200 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxBytesPerQuery: 200 }), + } ); describeWithMongoDB( @@ -369,5 +373,7 @@ describeWithMongoDB( expect(content).toContain(`Returning 990 documents.`); }); }, - () => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 }), + } ); diff --git a/tests/integration/tools/mongodb/read/export.test.ts b/tests/integration/tools/mongodb/read/export.test.ts index b20ca7229..2c0310b6e 100644 --- a/tests/integration/tools/mongodb/read/export.test.ts +++ b/tests/integration/tools/mongodb/read/export.test.ts @@ -458,5 +458,7 @@ describeWithMongoDB( }); }); }, - () => userConfig + { + getUserConfig: () => userConfig, + } ); diff --git a/tests/integration/tools/mongodb/read/find.test.ts b/tests/integration/tools/mongodb/read/find.test.ts index 3619e423c..c466650fa 100644 --- a/tests/integration/tools/mongodb/read/find.test.ts +++ b/tests/integration/tools/mongodb/read/find.test.ts @@ -341,7 +341,9 @@ describeWithMongoDB( ); }); }, - () => ({ ...defaultTestConfig, maxDocumentsPerQuery: 10 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: 10 }), + } ); describeWithMongoDB( @@ -391,7 +393,9 @@ describeWithMongoDB( ); }); }, - () => ({ ...defaultTestConfig, maxBytesPerQuery: 100 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxBytesPerQuery: 100 }), + } ); describeWithMongoDB( @@ -441,5 +445,7 @@ describeWithMongoDB( ); }); }, - () => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 }) + { + getUserConfig: () => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 }), + } ); diff --git a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts index 97571c0a9..fa69fa721 100644 --- a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts +++ b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts @@ -117,9 +117,9 @@ describeWithMongoDB( ); }); }, - undefined, // default user config - undefined, // default driver config - { search: true } // use a search cluster + { + downloadOptions: { search: true }, + } ); async function waitUntilSearchIsReady(provider: NodeDriverServiceProvider, abortSignal: AbortSignal): Promise {