From bdf93556d5cf6a0339e41d194166202bb8f39327 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 25 Apr 2025 12:32:22 +0200 Subject: [PATCH 1/4] chore: add type-safe ESLint --- eslint.config.js | 39 ++++-- global.d.ts | 1 + package-lock.json | 111 +++++++++++++++++- package.json | 4 +- scripts/apply.ts | 16 +-- scripts/filter.ts | 5 +- tests/integration/helpers.ts | 35 ++++-- tests/integration/inMemoryTransport.ts | 5 +- tests/integration/server.test.ts | 14 +-- .../tools/atlas/accessLists.test.ts | 13 +- tests/integration/tools/atlas/atlasHelpers.ts | 2 +- .../integration/tools/atlas/clusters.test.ts | 19 +-- tests/integration/tools/atlas/dbUsers.test.ts | 13 +- tests/integration/tools/atlas/orgs.test.ts | 4 +- .../integration/tools/atlas/projects.test.ts | 13 +- .../tools/mongodb/create/createIndex.test.ts | 6 +- .../tools/mongodb/create/insertMany.test.ts | 3 +- .../tools/mongodb/delete/dropDatabase.test.ts | 3 +- .../mongodb/metadata/collectionSchema.test.ts | 9 +- .../metadata/collectionStorageSize.test.ts | 5 +- .../tools/mongodb/metadata/connect.test.ts | 2 +- .../tools/mongodb/metadata/dbStats.test.ts | 14 ++- .../tools/mongodb/metadata/explain.test.ts | 1 - .../mongodb/metadata/listDatabases.test.ts | 8 +- .../tools/mongodb/metadata/logs.test.ts | 4 +- .../tools/mongodb/mongodbHelpers.ts | 24 ++-- .../tools/mongodb/read/aggregate.test.ts | 3 +- .../mongodb/read/collectionIndexes.test.ts | 5 +- .../tools/mongodb/read/count.test.ts | 1 - .../tools/mongodb/read/find.test.ts | 22 ++-- .../mongodb/update/renameCollection.test.ts | 2 - .../tools/mongodb/update/updateMany.test.ts | 1 - tsconfig.jest.json | 3 +- tsconfig.json | 2 +- tsconfig.lint.json | 8 ++ 35 files changed, 299 insertions(+), 121 deletions(-) create mode 100644 global.d.ts create mode 100644 tsconfig.lint.json diff --git a/eslint.config.js b/eslint.config.js index b6263450..072bef1d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,18 +2,34 @@ import { defineConfig, globalIgnores } from "eslint/config"; import js from "@eslint/js"; import globals from "globals"; import tseslint from "typescript-eslint"; -import eslintConfigPrettier from "eslint-config-prettier/flat"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import jestPlugin from "eslint-plugin-jest"; -const files = ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.test.ts", "eslint.config.js", "jest.config.js"]; +const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"]; + +const files = [...testFiles, "src/**/*.ts", "scripts/**/*.ts"]; export default defineConfig([ { files, plugins: { js }, extends: ["js/recommended"] }, { files, languageOptions: { globals: globals.node } }, + { + files: testFiles, + plugins: { + jest: jestPlugin, + }, + languageOptions: { + globals: { + ...globals.node, + ...jestPlugin.environments.globals.globals, + }, + }, + }, tseslint.configs.recommendedTypeChecked, { + files, languageOptions: { parserOptions: { - projectService: true, + project: "./tsconfig.lint.json", tsconfigRootDir: import.meta.dirname, }, }, @@ -25,11 +41,14 @@ export default defineConfig([ "@typescript-eslint/no-non-null-assertion": "error", }, }, - // Ignore features specific to TypeScript resolved rules - tseslint.config({ - // TODO: Configure tests and scripts to work with this. - ignores: ["eslint.config.js", "jest.config.js", "tests/**/*.ts", "scripts/**/*.ts"], - }), - globalIgnores(["node_modules", "dist", "src/common/atlas/openapi.d.ts", "coverage"]), - eslintConfigPrettier, + globalIgnores([ + "node_modules", + "dist", + "src/common/atlas/openapi.d.ts", + "coverage", + "global.d.ts", + "eslint.config.js", + "jest.config.js", + ]), + eslintPluginPrettierRecommended, ]); diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..3b47093f --- /dev/null +++ b/global.d.ts @@ -0,0 +1 @@ +import "jest-extended"; diff --git a/package-lock.json b/package-lock.json index 3fa1897a..37c32997 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,9 @@ "@types/simple-oauth2": "^5.0.7", "@types/yargs-parser": "^21.0.3", "eslint": "^9.24.0", - "eslint-config-prettier": "^10.1.1", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-prettier": "^5.2.6", "globals": "^16.0.0", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", @@ -3538,6 +3540,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -7897,6 +7912,63 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-jest": { + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", @@ -8261,6 +8333,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -12114,6 +12193,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -13776,6 +13868,23 @@ "node": ">= 6" } }, + "node_modules/synckit": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/system-ca": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/system-ca/-/system-ca-2.0.1.tgz", diff --git a/package.json b/package.json index e3c02f8f..dc7d6401 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,9 @@ "@types/simple-oauth2": "^5.0.7", "@types/yargs-parser": "^21.0.3", "eslint": "^9.24.0", - "eslint-config-prettier": "^10.1.1", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-prettier": "^5.2.6", "globals": "^16.0.0", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", diff --git a/scripts/apply.ts b/scripts/apply.ts index 420a31d0..fa2a6917 100755 --- a/scripts/apply.ts +++ b/scripts/apply.ts @@ -9,13 +9,15 @@ function findObjectFromRef(obj: T | OpenAPIV3_1.ReferenceObject, openapi: Ope } const paramParts = ref.split("/"); paramParts.shift(); // Remove the first part which is always '#' - let foundObj: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any + + let foundObj: Record = openapi; while (true) { const part = paramParts.shift(); if (!part) { break; } - foundObj = foundObj[part]; + + foundObj = foundObj[part] as Record; } return foundObj as T; } @@ -28,7 +30,7 @@ async function main() { process.exit(1); } - const specFile = (await fs.readFile(spec, "utf8")) as string; + const specFile = await fs.readFile(spec as string, "utf8"); const operations: { path: string; @@ -42,7 +44,7 @@ async function main() { const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document; for (const path in openapi.paths) { for (const method in openapi.paths[path]) { - const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method]; + const operation = openapi.paths[path][method] as OpenAPIV3_1.OperationObject; if (!operation.operationId || !operation.tags?.length) { continue; @@ -101,9 +103,9 @@ async function main() { }) .join("\n"); - const templateFile = (await fs.readFile(file, "utf8")) as string; + const templateFile = await fs.readFile(file as string, "utf8"); const templateLines = templateFile.split("\n"); - let outputLines: string[] = []; + const outputLines: string[] = []; let addLines = true; for (const line of templateLines) { if (line.includes("DO NOT EDIT. This is auto-generated code.")) { @@ -120,7 +122,7 @@ async function main() { } const output = outputLines.join("\n"); - await fs.writeFile(file, output, "utf8"); + await fs.writeFile(file as string, output, "utf8"); } main().catch((error) => { diff --git a/scripts/filter.ts b/scripts/filter.ts index 34a69f7a..4dcdbdcc 100755 --- a/scripts/filter.ts +++ b/scripts/filter.ts @@ -8,6 +8,7 @@ async function readStdin() { reject(err); }); process.stdin.on("data", (chunk) => { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands data += chunk; }); process.stdin.on("end", () => { @@ -42,8 +43,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document { for (const path in openapi.paths) { const filteredMethods = {} as OpenAPIV3_1.PathItemObject; for (const method in openapi.paths[path]) { - if (allowedOperations.includes(openapi.paths[path][method].operationId)) { - filteredMethods[method] = openapi.paths[path][method]; + if (allowedOperations.includes((openapi.paths[path][method] as { operationId: string }).operationId)) { + filteredMethods[method] = openapi.paths[path][method] as OpenAPIV3_1.OperationObject; } } if (Object.keys(filteredMethods).length > 0) { diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 3c458da6..a2f57950 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -1,12 +1,11 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "./inMemoryTransport.js"; import { Server } from "../../src/server.js"; -import { ObjectId } from "mongodb"; import { config, UserConfig } from "../../src/config.js"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Session } from "../../src/session.js"; -import { toIncludeAllMembers } from "jest-extended"; +import { ObjectId } from "bson"; interface ParameterInfo { name: string; @@ -35,8 +34,8 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati await serverTransport.start(); await clientTransport.start(); - clientTransport.output.pipeTo(serverTransport.input); - serverTransport.output.pipeTo(clientTransport.input); + void clientTransport.output.pipeTo(serverTransport.input); + void serverTransport.output.pipeTo(clientTransport.input); mcpClient = new Client( { @@ -66,7 +65,7 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati await mcpClient.connect(clientTransport); }); - beforeEach(async () => { + beforeEach(() => { config.telemetry = "disabled"; randomDbName = new ObjectId().toString(); }); @@ -101,12 +100,14 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati }; } +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents export function getResponseContent(content: unknown | { content: unknown }): string { return getResponseElements(content) .map((item) => item.text) .join("\n"); } +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents export function getResponseElements(content: unknown | { content: unknown }): { type: string; text: string }[] { if (typeof content === "object" && content !== null && "content" in content) { content = (content as { content: unknown }).content; @@ -133,9 +134,9 @@ export async function connect(client: Client, connectionString: string): Promise export function getParameters(tool: ToolInfo): ParameterInfo[] { expect(tool.inputSchema.type).toBe("object"); - expect(tool.inputSchema.properties).toBeDefined(); + expectDefined(tool.inputSchema.properties); - return Object.entries(tool.inputSchema.properties!) + return Object.entries(tool.inputSchema.properties) .sort((a, b) => a[0].localeCompare(b[0])) .map(([key, value]) => { expect(value).toHaveProperty("type"); @@ -167,13 +168,12 @@ export const databaseCollectionInvalidArgs = [ { database: "test" }, { collection: "foo" }, { database: 123, collection: "foo" }, - { database: "test", collection: "foo", extra: "bar" }, { database: "test", collection: 123 }, { database: [], collection: "foo" }, { database: "test", collection: [] }, ]; -export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }, { database: "test", extra: "bar" }]; +export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }]; export function validateToolMetadata( integration: IntegrationTest, @@ -183,8 +183,8 @@ export function validateToolMetadata( ): void { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const tool = tools.find((tool) => tool.name === name)!; - expect(tool).toBeDefined(); + const tool = tools.find((tool) => tool.name === name); + expectDefined(tool); expect(tool.description).toBe(description); const toolParameters = getParameters(tool); @@ -203,8 +203,9 @@ export function validateThrowsForInvalidArguments( it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => { try { await integration.mcpClient().callTool({ name, arguments: arg }); - expect.fail("Expected an error to be thrown"); + throw new Error("Expected an error to be thrown"); } catch (error) { + expect((error as Error).message).not.toEqual("Expected an error to be thrown"); expect(error).toBeInstanceOf(McpError); const mcpError = error as McpError; expect(mcpError.code).toEqual(-32602); @@ -214,3 +215,13 @@ export function validateThrowsForInvalidArguments( } }); } + +/** Expects the argument being defined and asserts it */ +export function expectDefined(arg: T): asserts arg is Exclude { + expect(arg).toBeDefined(); +} + +/** Expects the argument being undefined and asserts it */ +export function expectUndefined(arg: unknown): asserts arg is undefined { + expect(arg).toBeUndefined(); +} diff --git a/tests/integration/inMemoryTransport.ts b/tests/integration/inMemoryTransport.ts index a12c4625..c46f87a3 100644 --- a/tests/integration/inMemoryTransport.ts +++ b/tests/integration/inMemoryTransport.ts @@ -39,6 +39,7 @@ export class InMemoryTransport implements Transport { return Promise.resolve(); } + // eslint-disable-next-line @typescript-eslint/require-await async close(): Promise { this.outputController.close(); this.onclose?.(); @@ -49,10 +50,10 @@ export class InMemoryTransport implements Transport { sessionId?: string | undefined; private static getPromise(): [Promise, resolve: () => void] { - let resolve: () => void; + let resolve: () => void = () => {}; const promise = new Promise((res) => { resolve = res; }); - return [promise, resolve!]; + return [promise, resolve]; } } diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index 5130b4b6..a8ed3c3f 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -1,4 +1,4 @@ -import { setupIntegrationTest } from "./helpers"; +import { expectDefined, setupIntegrationTest } from "./helpers.js"; import { config } from "../../src/config.js"; describe("Server integration test", () => { @@ -11,7 +11,7 @@ describe("Server integration test", () => { it("should return positive number of tools and have no atlas tools", async () => { const tools = await integration.mcpClient().listTools(); - expect(tools).toBeDefined(); + expectDefined(tools); expect(tools.tools.length).toBeGreaterThan(0); const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-")); @@ -28,7 +28,7 @@ describe("Server integration test", () => { describe("list capabilities", () => { it("should return positive number of tools and have some atlas tools", async () => { const tools = await integration.mcpClient().listTools(); - expect(tools).toBeDefined(); + expectDefined(tools); expect(tools.tools.length).toBeGreaterThan(0); const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-")); @@ -47,13 +47,13 @@ describe("Server integration test", () => { }); }); - it("should return capabilities", async () => { + it("should return capabilities", () => { const capabilities = integration.mcpClient().getServerCapabilities(); - expect(capabilities).toBeDefined(); + expectDefined(capabilities); expect(capabilities?.completions).toBeUndefined(); expect(capabilities?.experimental).toBeUndefined(); - expect(capabilities?.tools).toBeDefined(); - expect(capabilities?.logging).toBeDefined(); + expectDefined(capabilities?.tools); + expectDefined(capabilities?.logging); expect(capabilities?.prompts).toBeUndefined(); }); }); diff --git a/tests/integration/tools/atlas/accessLists.test.ts b/tests/integration/tools/atlas/accessLists.test.ts index 43e5742d..a194a351 100644 --- a/tests/integration/tools/atlas/accessLists.test.ts +++ b/tests/integration/tools/atlas/accessLists.test.ts @@ -1,5 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { describeWithAtlas, withProject } from "./atlasHelpers.js"; +import { expectDefined } from "../../helpers.js"; function generateRandomIp() { const randomIp: number[] = [192]; @@ -41,10 +42,10 @@ describeWithAtlas("ip access lists", (integration) => { describe("atlas-create-access-list", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const createAccessList = tools.find((tool) => tool.name === "atlas-create-access-list")!; - expect(createAccessList).toBeDefined(); + const createAccessList = tools.find((tool) => tool.name === "atlas-create-access-list"); + expectDefined(createAccessList); expect(createAccessList.inputSchema.type).toBe("object"); - expect(createAccessList.inputSchema.properties).toBeDefined(); + expectDefined(createAccessList.inputSchema.properties); expect(createAccessList.inputSchema.properties).toHaveProperty("projectId"); expect(createAccessList.inputSchema.properties).toHaveProperty("ipAddresses"); expect(createAccessList.inputSchema.properties).toHaveProperty("cidrBlocks"); @@ -73,10 +74,10 @@ describeWithAtlas("ip access lists", (integration) => { describe("atlas-inspect-access-list", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const inspectAccessList = tools.find((tool) => tool.name === "atlas-inspect-access-list")!; - expect(inspectAccessList).toBeDefined(); + const inspectAccessList = tools.find((tool) => tool.name === "atlas-inspect-access-list"); + expectDefined(inspectAccessList); expect(inspectAccessList.inputSchema.type).toBe("object"); - expect(inspectAccessList.inputSchema.properties).toBeDefined(); + expectDefined(inspectAccessList.inputSchema.properties); expect(inspectAccessList.inputSchema.properties).toHaveProperty("projectId"); }); diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index 36b88c1e..f015b2b2 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 describeWithAtlas(name: number | string | Function | jest.FunctionLike, fn: IntegrationTestFunction) { +export function describeWithAtlas(name: string, fn: IntegrationTestFunction) { const testDefinition = () => { const integration = setupIntegrationTest(); describe(name, () => { diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 72f41df0..b3bae979 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,4 +1,5 @@ import { Session } from "../../../../src/session.js"; +import { expectDefined } from "../../helpers.js"; import { describeWithAtlas, withProject, sleep, randomId } from "./atlasHelpers.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; @@ -43,11 +44,11 @@ describeWithAtlas("clusters", (integration) => { describe("atlas-create-free-cluster", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const createFreeCluster = tools.find((tool) => tool.name === "atlas-create-free-cluster")!; + const createFreeCluster = tools.find((tool) => tool.name === "atlas-create-free-cluster"); - expect(createFreeCluster).toBeDefined(); + expectDefined(createFreeCluster); expect(createFreeCluster.inputSchema.type).toBe("object"); - expect(createFreeCluster.inputSchema.properties).toBeDefined(); + expectDefined(createFreeCluster.inputSchema.properties); expect(createFreeCluster.inputSchema.properties).toHaveProperty("projectId"); expect(createFreeCluster.inputSchema.properties).toHaveProperty("name"); expect(createFreeCluster.inputSchema.properties).toHaveProperty("region"); @@ -73,11 +74,11 @@ describeWithAtlas("clusters", (integration) => { describe("atlas-inspect-cluster", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const inspectCluster = tools.find((tool) => tool.name === "atlas-inspect-cluster")!; + const inspectCluster = tools.find((tool) => tool.name === "atlas-inspect-cluster"); - expect(inspectCluster).toBeDefined(); + expectDefined(inspectCluster); expect(inspectCluster.inputSchema.type).toBe("object"); - expect(inspectCluster.inputSchema.properties).toBeDefined(); + expectDefined(inspectCluster.inputSchema.properties); expect(inspectCluster.inputSchema.properties).toHaveProperty("projectId"); expect(inspectCluster.inputSchema.properties).toHaveProperty("clusterName"); }); @@ -98,10 +99,10 @@ describeWithAtlas("clusters", (integration) => { describe("atlas-list-clusters", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const listClusters = tools.find((tool) => tool.name === "atlas-list-clusters")!; - expect(listClusters).toBeDefined(); + const listClusters = tools.find((tool) => tool.name === "atlas-list-clusters"); + expectDefined(listClusters); expect(listClusters.inputSchema.type).toBe("object"); - expect(listClusters.inputSchema.properties).toBeDefined(); + expectDefined(listClusters.inputSchema.properties); expect(listClusters.inputSchema.properties).toHaveProperty("projectId"); }); diff --git a/tests/integration/tools/atlas/dbUsers.test.ts b/tests/integration/tools/atlas/dbUsers.test.ts index 2a5eb02a..892bb89e 100644 --- a/tests/integration/tools/atlas/dbUsers.test.ts +++ b/tests/integration/tools/atlas/dbUsers.test.ts @@ -1,6 +1,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Session } from "../../../../src/session.js"; import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js"; +import { expectDefined } from "../../helpers.js"; describeWithAtlas("db users", (integration) => { const userName = "testuser-" + randomId; @@ -23,10 +24,10 @@ describeWithAtlas("db users", (integration) => { describe("atlas-create-db-user", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const createDbUser = tools.find((tool) => tool.name === "atlas-create-db-user")!; - expect(createDbUser).toBeDefined(); + const createDbUser = tools.find((tool) => tool.name === "atlas-create-db-user"); + expectDefined(createDbUser); expect(createDbUser.inputSchema.type).toBe("object"); - expect(createDbUser.inputSchema.properties).toBeDefined(); + expectDefined(createDbUser.inputSchema.properties); expect(createDbUser.inputSchema.properties).toHaveProperty("projectId"); expect(createDbUser.inputSchema.properties).toHaveProperty("username"); expect(createDbUser.inputSchema.properties).toHaveProperty("password"); @@ -58,10 +59,10 @@ describeWithAtlas("db users", (integration) => { describe("atlas-list-db-users", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const listDbUsers = tools.find((tool) => tool.name === "atlas-list-db-users")!; - expect(listDbUsers).toBeDefined(); + const listDbUsers = tools.find((tool) => tool.name === "atlas-list-db-users"); + expectDefined(listDbUsers); expect(listDbUsers.inputSchema.type).toBe("object"); - expect(listDbUsers.inputSchema.properties).toBeDefined(); + expectDefined(listDbUsers.inputSchema.properties); expect(listDbUsers.inputSchema.properties).toHaveProperty("projectId"); }); it("returns database users by project", async () => { diff --git a/tests/integration/tools/atlas/orgs.test.ts b/tests/integration/tools/atlas/orgs.test.ts index ca86e4b9..83143404 100644 --- a/tests/integration/tools/atlas/orgs.test.ts +++ b/tests/integration/tools/atlas/orgs.test.ts @@ -1,5 +1,5 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { setupIntegrationTest } from "../../helpers.js"; +import { expectDefined } from "../../helpers.js"; import { parseTable, describeWithAtlas } from "./atlasHelpers.js"; describeWithAtlas("orgs", (integration) => { @@ -7,7 +7,7 @@ describeWithAtlas("orgs", (integration) => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); const listOrgs = tools.find((tool) => tool.name === "atlas-list-orgs"); - expect(listOrgs).toBeDefined(); + expectDefined(listOrgs); }); it("returns org names", async () => { diff --git a/tests/integration/tools/atlas/projects.test.ts b/tests/integration/tools/atlas/projects.test.ts index 3f570183..7d773c7e 100644 --- a/tests/integration/tools/atlas/projects.test.ts +++ b/tests/integration/tools/atlas/projects.test.ts @@ -1,6 +1,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ObjectId } from "mongodb"; import { parseTable, describeWithAtlas } from "./atlasHelpers.js"; +import { expectDefined } from "../../helpers.js"; const randomId = new ObjectId().toString(); @@ -28,10 +29,10 @@ describeWithAtlas("projects", (integration) => { describe("atlas-create-project", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const createProject = tools.find((tool) => tool.name === "atlas-create-project")!; - expect(createProject).toBeDefined(); + const createProject = tools.find((tool) => tool.name === "atlas-create-project"); + expectDefined(createProject); expect(createProject.inputSchema.type).toBe("object"); - expect(createProject.inputSchema.properties).toBeDefined(); + expectDefined(createProject.inputSchema.properties); expect(createProject.inputSchema.properties).toHaveProperty("projectName"); expect(createProject.inputSchema.properties).toHaveProperty("organizationId"); }); @@ -48,10 +49,10 @@ describeWithAtlas("projects", (integration) => { describe("atlas-list-projects", () => { it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const listProjects = tools.find((tool) => tool.name === "atlas-list-projects")!; - expect(listProjects).toBeDefined(); + const listProjects = tools.find((tool) => tool.name === "atlas-list-projects"); + expectDefined(listProjects); expect(listProjects.inputSchema.type).toBe("object"); - expect(listProjects.inputSchema.properties).toBeDefined(); + expectDefined(listProjects.inputSchema.properties); expect(listProjects.inputSchema.properties).toHaveProperty("orgId"); }); diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index fa921339..b1a2a5df 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -5,6 +5,7 @@ import { databaseCollectionParameters, validateToolMetadata, validateThrowsForInvalidArguments, + expectDefined, } from "../../../helpers.js"; import { IndexDirection } from "mongodb"; @@ -28,7 +29,6 @@ describeWithMongoDB("createIndex tool", (integration) => { validateThrowsForInvalidArguments(integration, "create-index", [ {}, { collection: "bar", database: 123, keys: { foo: 1 } }, - { collection: "bar", database: "test", keys: { foo: 5 } }, { collection: [], database: "test", keys: { foo: 1 } }, { collection: "bar", database: "test", keys: { foo: 1 }, name: 123 }, { collection: "bar", database: "test", keys: "foo", name: "my-index" }, @@ -44,8 +44,8 @@ describeWithMongoDB("createIndex tool", (integration) => { expect(indexes[0].name).toEqual("_id_"); for (const index of expected) { const foundIndex = indexes.find((i) => i.name === index.name); - expect(foundIndex).toBeDefined(); - expect(foundIndex!.key).toEqual(index.key); + expectDefined(foundIndex); + expect(foundIndex.key).toEqual(index.key); } }; diff --git a/tests/integration/tools/mongodb/create/insertMany.test.ts b/tests/integration/tools/mongodb/create/insertMany.test.ts index b4042029..a49c3a4e 100644 --- a/tests/integration/tools/mongodb/create/insertMany.test.ts +++ b/tests/integration/tools/mongodb/create/insertMany.test.ts @@ -5,6 +5,7 @@ import { databaseCollectionParameters, validateToolMetadata, validateThrowsForInvalidArguments, + expectDefined, } from "../../../helpers.js"; describeWithMongoDB("insertMany tool", (integration) => { @@ -29,7 +30,7 @@ describeWithMongoDB("insertMany tool", (integration) => { const validateDocuments = async (collection: string, expectedDocuments: object[]) => { const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray(); - expect(collections.find((c) => c.name === collection)).toBeDefined(); + expectDefined(collections.find((c) => c.name === collection)); const docs = await integration .mongoClient() diff --git a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts index 6293df40..47fa294c 100644 --- a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +++ b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts @@ -6,6 +6,7 @@ import { validateThrowsForInvalidArguments, databaseParameters, databaseInvalidArgs, + expectDefined, } from "../../../helpers.js"; describeWithMongoDB("dropDatabase tool", (integration) => { @@ -45,7 +46,7 @@ describeWithMongoDB("dropDatabase tool", (integration) => { await integration.mongoClient().db(integration.randomDbName()).createCollection("coll2"); let { databases } = await integration.mongoClient().db("").admin().listDatabases(); - expect(databases.find((db) => db.name === integration.randomDbName())).toBeDefined(); + expectDefined(databases.find((db) => db.name === integration.randomDbName())); const response = await integration.mcpClient().callTool({ name: "drop-database", diff --git a/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts b/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts index ccfc988f..1b7481a2 100644 --- a/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +++ b/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts @@ -56,7 +56,8 @@ describeWithMongoDB("collectionSchema tool", (integration) => { types: [{ bsonType: "String" }], }, age: { - types: [{ bsonType: "Number" as any }], + //@ts-expect-error This is a workaround + types: [{ bsonType: "Number" }], }, }, }, @@ -76,7 +77,8 @@ describeWithMongoDB("collectionSchema tool", (integration) => { types: [{ bsonType: "String" }], }, age: { - types: [{ bsonType: "Number" as any }, { bsonType: "String" }], + // @ts-expect-error This is a workaround + types: [{ bsonType: "Number" }, { bsonType: "String" }], }, country: { types: [{ bsonType: "String" }, { bsonType: "Boolean" }], @@ -109,7 +111,8 @@ describeWithMongoDB("collectionSchema tool", (integration) => { ], }, ageRange: { - types: [{ bsonType: "Array", types: [{ bsonType: "Number" as any }] }, { bsonType: "String" }], + // @ts-expect-error This is a workaround + types: [{ bsonType: "Array", types: [{ bsonType: "Number" }] }, { bsonType: "String" }], }, }, }, diff --git a/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts b/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts index 23e86cde..d8ffafbd 100644 --- a/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +++ b/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts @@ -6,6 +6,7 @@ import { databaseCollectionInvalidArgs, validateToolMetadata, validateThrowsForInvalidArguments, + expectDefined, } from "../../../helpers.js"; import * as crypto from "crypto"; @@ -65,8 +66,8 @@ describeWithMongoDB("collectionStorageSize tool", (integration) => { expect(content).toContain(`The size of "${integration.randomDbName()}.foo" is`); const size = /is `(\d+\.\d+) ([a-zA-Z]*)`/.exec(content); - expect(size?.[1]).toBeDefined(); - expect(size?.[2]).toBeDefined(); + expectDefined(size?.[1]); + expectDefined(size?.[2]); expect(parseFloat(size?.[1] || "")).toBeGreaterThan(0); expect(size?.[2]).toBe(test.expectedScale); }); diff --git a/tests/integration/tools/mongodb/metadata/connect.test.ts b/tests/integration/tools/mongodb/metadata/connect.test.ts index 3b5eb6c5..7992c5f2 100644 --- a/tests/integration/tools/mongodb/metadata/connect.test.ts +++ b/tests/integration/tools/mongodb/metadata/connect.test.ts @@ -75,7 +75,7 @@ describeWithMongoDB("Connect tool", (integration) => { }); describe("with connection string in config", () => { - beforeEach(async () => { + beforeEach(() => { config.connectionString = integration.connectionString(); }); diff --git a/tests/integration/tools/mongodb/metadata/dbStats.test.ts b/tests/integration/tools/mongodb/metadata/dbStats.test.ts index 02a4e4a8..b26a2cf2 100644 --- a/tests/integration/tools/mongodb/metadata/dbStats.test.ts +++ b/tests/integration/tools/mongodb/metadata/dbStats.test.ts @@ -30,7 +30,11 @@ describeWithMongoDB("dbStats tool", (integration) => { expect(elements).toHaveLength(2); expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`); - const stats = JSON.parse(elements[1].text); + const stats = JSON.parse(elements[1].text) as { + db: string; + collections: number; + storageSize: number; + }; expect(stats.db).toBe(integration.randomDbName()); expect(stats.collections).toBe(0); expect(stats.storageSize).toBe(0); @@ -73,10 +77,16 @@ describeWithMongoDB("dbStats tool", (integration) => { expect(elements).toHaveLength(2); expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`); - const stats = JSON.parse(elements[1].text); + const stats = JSON.parse(elements[1].text) as { + db: string; + collections: unknown; + storageSize: unknown; + objects: unknown; + }; expect(stats.db).toBe(integration.randomDbName()); expect(stats.collections).toBe(Object.entries(test.collections).length); expect(stats.storageSize).toBeGreaterThan(1024); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return expect(stats.objects).toBe(Object.values(test.collections).reduce((a, b) => a + b, 0)); }); } diff --git a/tests/integration/tools/mongodb/metadata/explain.test.ts b/tests/integration/tools/mongodb/metadata/explain.test.ts index dafdd238..0aeb92ea 100644 --- a/tests/integration/tools/mongodb/metadata/explain.test.ts +++ b/tests/integration/tools/mongodb/metadata/explain.test.ts @@ -1,6 +1,5 @@ import { databaseCollectionParameters, - setupIntegrationTest, validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements, diff --git a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts index 3288cf30..de803c6c 100644 --- a/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +++ b/tests/integration/tools/mongodb/metadata/listDatabases.test.ts @@ -1,13 +1,13 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js"; -import { getResponseElements, getParameters } from "../../../helpers.js"; +import { getResponseElements, getParameters, expectDefined } from "../../../helpers.js"; describeWithMongoDB("listDatabases tool", (integration) => { const defaultDatabases = ["admin", "config", "local"]; it("should have correct metadata", async () => { const { tools } = await integration.mcpClient().listTools(); - const listDatabases = tools.find((tool) => tool.name === "list-databases")!; - expect(listDatabases).toBeDefined(); + const listDatabases = tools.find((tool) => tool.name === "list-databases"); + expectDefined(listDatabases); expect(listDatabases.description).toBe("List all databases for a MongoDB connection"); const parameters = getParameters(listDatabases); @@ -54,7 +54,7 @@ describeWithMongoDB("listDatabases tool", (integration) => { async () => { const mongoClient = integration.mongoClient(); const { databases } = await mongoClient.db("admin").command({ listDatabases: 1, nameOnly: true }); - for (const db of databases) { + for (const db of databases as { name: string }[]) { if (!defaultDatabases.includes(db.name)) { await mongoClient.db(db.name).dropDatabase(); } diff --git a/tests/integration/tools/mongodb/metadata/logs.test.ts b/tests/integration/tools/mongodb/metadata/logs.test.ts index 33d05927..bc7f79bc 100644 --- a/tests/integration/tools/mongodb/metadata/logs.test.ts +++ b/tests/integration/tools/mongodb/metadata/logs.test.ts @@ -19,7 +19,6 @@ describeWithMongoDB("logs tool", (integration) => { ]); validateThrowsForInvalidArguments(integration, "mongodb-logs", [ - { extra: true }, { type: 123 }, { type: "something" }, { limit: 0 }, @@ -41,6 +40,7 @@ describeWithMongoDB("logs tool", (integration) => { expect(elements[0].text).toMatch(/Found: \d+ messages/); for (let i = 1; i < elements.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const log = JSON.parse(elements[i].text); expect(log).toHaveProperty("t"); expect(log).toHaveProperty("msg"); @@ -59,7 +59,7 @@ describeWithMongoDB("logs tool", (integration) => { const elements = getResponseElements(response); expect(elements.length).toBeLessThanOrEqual(51); for (let i = 1; i < elements.length; i++) { - const log = JSON.parse(elements[i].text); + const log = JSON.parse(elements[i].text) as { tags: string[] }; expect(log).toHaveProperty("t"); expect(log).toHaveProperty("msg"); expect(log).toHaveProperty("tags"); diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index 44584339..b6bd47d7 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -1,9 +1,9 @@ -import runner, { MongoCluster } from "mongodb-runner"; +import { MongoCluster } from "mongodb-runner"; import path from "path"; import fs from "fs/promises"; import { MongoClient, ObjectId } from "mongodb"; import { getResponseContent, IntegrationTest, setupIntegrationTest } from "../../helpers.js"; -import { UserConfig, config } from "../../../../src/config.js"; +import { config } from "../../../../src/config.js"; interface MongoDBIntegrationTest { mongoClient: () => MongoClient; @@ -13,7 +13,7 @@ interface MongoDBIntegrationTest { } export function describeWithMongoDB( - name: number | string | Function | jest.FunctionLike, + name: string, fn: (integration: IntegrationTest & MongoDBIntegrationTest) => void ): void { describe("mongodb", () => { @@ -25,15 +25,17 @@ export function describeWithMongoDB( }); } -export function setupMongoDBIntegrationTest( - integration: IntegrationTest, - userConfig: UserConfig = config -): MongoDBIntegrationTest { - let mongoCluster: runner.MongoCluster | undefined; +export function setupMongoDBIntegrationTest(integration: IntegrationTest): MongoDBIntegrationTest { + let mongoCluster: // TODO: Fix this type once mongodb-runner is updated. + | { + connectionString: string; + close: () => Promise; + } + | undefined; let mongoClient: MongoClient | undefined; let randomDbName: string; - beforeEach(async () => { + beforeEach(() => { randomDbName = new ObjectId().toString(); }); @@ -56,6 +58,8 @@ export function setupMongoDBIntegrationTest( let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs"); for (let i = 0; i < 10; i++) { try { + // TODO: Fix this type once mongodb-runner is updated. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call mongoCluster = await MongoCluster.start({ tmpDir: dbsDir, logDir: path.join(tmpDir, "mongodb-runner", "logs"), @@ -66,11 +70,13 @@ export function setupMongoDBIntegrationTest( } catch (err) { if (i < 5) { // Just wait a little bit and retry + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 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( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.` ); dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`); diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index 148117e1..65d243d9 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -22,7 +22,6 @@ describeWithMongoDB("aggregate tool", (integration) => { { database: "test", collection: "foo" }, { database: test, pipeline: [] }, { database: "test", collection: "foo", pipeline: {} }, - { database: "test", collection: "foo", pipeline: [], extra: "extra" }, { database: "test", collection: [], pipeline: [] }, { database: 123, collection: "foo", pipeline: [] }, ]); @@ -81,8 +80,10 @@ describeWithMongoDB("aggregate tool", (integration) => { const elements = getResponseElements(response.content); expect(elements).toHaveLength(3); expect(elements[0].text).toEqual('Found 2 documents in the collection "people":'); + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ expect(JSON.parse(elements[1].text)).toEqual({ _id: expect.any(Object), name: "Søren", age: 15 }); expect(JSON.parse(elements[2].text)).toEqual({ _id: expect.any(Object), name: "Laura", age: 10 }); + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ }); validateAutoConnectBehavior(integration, "aggregate", () => { diff --git a/tests/integration/tools/mongodb/read/collectionIndexes.test.ts b/tests/integration/tools/mongodb/read/collectionIndexes.test.ts index 2e919080..3c5b2eb1 100644 --- a/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +++ b/tests/integration/tools/mongodb/read/collectionIndexes.test.ts @@ -5,6 +5,7 @@ import { validateThrowsForInvalidArguments, getResponseElements, databaseCollectionInvalidArgs, + expectDefined, } from "../../../helpers.js"; import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js"; @@ -78,14 +79,14 @@ describeWithMongoDB("collectionIndexes tool", (integration) => { for (const indexType of indexTypes) { const index = elements.find((element) => element.text.includes(`prop_${indexType}`)); - expect(index).toBeDefined(); + expectDefined(index); let expectedDefinition = JSON.stringify({ [`prop_${indexType}`]: indexType }); if (indexType === "text") { expectedDefinition = '{"_fts":"text"'; } - expect(index!.text).toContain(`definition: ${expectedDefinition}`); + expect(index.text).toContain(`definition: ${expectedDefinition}`); } }); diff --git a/tests/integration/tools/mongodb/read/count.test.ts b/tests/integration/tools/mongodb/read/count.test.ts index 938285a8..5b288448 100644 --- a/tests/integration/tools/mongodb/read/count.test.ts +++ b/tests/integration/tools/mongodb/read/count.test.ts @@ -22,7 +22,6 @@ describeWithMongoDB("count tool", (integration) => { validateThrowsForInvalidArguments(integration, "count", [ {}, { database: 123, collection: "bar" }, - { foo: "bar", database: "test", collection: "bar" }, { collection: [], database: "test" }, { collection: "bar", database: "test", query: "{ $gt: { foo: 5 } }" }, ]); diff --git a/tests/integration/tools/mongodb/read/find.test.ts b/tests/integration/tools/mongodb/read/find.test.ts index f2a3cfc3..d62d67a9 100644 --- a/tests/integration/tools/mongodb/read/find.test.ts +++ b/tests/integration/tools/mongodb/read/find.test.ts @@ -1,7 +1,6 @@ import { getResponseContent, databaseCollectionParameters, - setupIntegrationTest, validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements, @@ -42,7 +41,6 @@ describeWithMongoDB("find tool", (integration) => { validateThrowsForInvalidArguments(integration, "find", [ {}, { database: 123, collection: "bar" }, - { database: "test", collection: "bar", extra: "extra" }, { database: "test", collection: [] }, { database: "test", collection: "bar", filter: "{ $gt: { foo: 5 } }" }, { database: "test", collection: "bar", projection: "name" }, @@ -86,24 +84,25 @@ describeWithMongoDB("find tool", (integration) => { const testCases: { name: string; - filter?: any; + filter?: unknown; limit?: number; - projection?: any; - sort?: any; - expected: any[]; + projection?: unknown; + sort?: unknown; + expected: unknown[]; }[] = [ { name: "returns all documents when no filter is provided", expected: Array(10) .fill(0) - .map((_, index) => ({ _id: expect.any(Object), value: index })), + .map((_, index) => ({ _id: expect.any(Object) as unknown, value: index })), }, { name: "returns documents matching the filter", filter: { value: { $gt: 5 } }, expected: Array(4) .fill(0) - .map((_, index) => ({ _id: expect.any(Object), value: index + 6 })), + + .map((_, index) => ({ _id: expect.any(Object) as unknown, value: index + 6 })), }, { name: "returns documents matching the filter with projection", @@ -118,8 +117,8 @@ describeWithMongoDB("find tool", (integration) => { filter: { value: { $gt: 5 } }, limit: 2, expected: [ - { _id: expect.any(Object), value: 6 }, - { _id: expect.any(Object), value: 7 }, + { _id: expect.any(Object) as unknown, value: 6 }, + { _id: expect.any(Object) as unknown, value: 7 }, ], }, { @@ -128,7 +127,7 @@ describeWithMongoDB("find tool", (integration) => { sort: { value: -1 }, expected: Array(10) .fill(0) - .map((_, index) => ({ _id: expect.any(Object), value: index })) + .map((_, index) => ({ _id: expect.any(Object) as unknown, value: index })) .reverse(), }, ]; @@ -168,6 +167,7 @@ describeWithMongoDB("find tool", (integration) => { expect(elements[0].text).toEqual('Found 10 documents in the collection "foo":'); for (let i = 0; i < 10; i++) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(JSON.parse(elements[i + 1].text).value).toEqual(i); } }); diff --git a/tests/integration/tools/mongodb/update/renameCollection.test.ts b/tests/integration/tools/mongodb/update/renameCollection.test.ts index 1c904458..e3d00e54 100644 --- a/tests/integration/tools/mongodb/update/renameCollection.test.ts +++ b/tests/integration/tools/mongodb/update/renameCollection.test.ts @@ -1,7 +1,6 @@ import { getResponseContent, databaseCollectionParameters, - setupIntegrationTest, validateToolMetadata, validateThrowsForInvalidArguments, } from "../../../helpers.js"; @@ -28,7 +27,6 @@ describeWithMongoDB("renameCollection tool", (integration) => { validateThrowsForInvalidArguments(integration, "rename-collection", [ {}, { database: 123, collection: "bar" }, - { database: "test", collection: "bar", newName: "foo", extra: "extra" }, { database: "test", collection: [], newName: "foo" }, { database: "test", collection: "bar", newName: 10 }, { database: "test", collection: "bar", newName: "foo", dropTarget: "true" }, diff --git a/tests/integration/tools/mongodb/update/updateMany.test.ts b/tests/integration/tools/mongodb/update/updateMany.test.ts index 6a05f640..77840d95 100644 --- a/tests/integration/tools/mongodb/update/updateMany.test.ts +++ b/tests/integration/tools/mongodb/update/updateMany.test.ts @@ -42,7 +42,6 @@ describeWithMongoDB("updateMany tool", (integration) => { { database: 123, collection: "bar", update: {} }, { database: [], collection: "bar", update: {} }, { database: "test", collection: "bar", update: [] }, - { database: "test", collection: "bar", update: {}, extra: true }, { database: "test", collection: "bar", update: {}, filter: 123 }, { database: "test", collection: "bar", update: {}, upsert: "true" }, { database: "test", collection: "bar", update: {}, filter: {}, upsert: "true" }, diff --git a/tsconfig.jest.json b/tsconfig.jest.json index d92e8897..a53ca484 100644 --- a/tsconfig.jest.json +++ b/tsconfig.jest.json @@ -4,7 +4,8 @@ "module": "esnext", "target": "esnext", "isolatedModules": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["jest", "jest-extended"] }, "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 1fe57f10..dd65f91d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "strict": true, "strictNullChecks": true, "esModuleInterop": true, - "types": ["node"], + "types": ["node", "jest"], "sourceMap": true, "skipLibCheck": true, "resolveJsonModule": true, diff --git a/tsconfig.lint.json b/tsconfig.lint.json new file mode 100644 index 00000000..5b14e470 --- /dev/null +++ b/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["jest"] + }, + "include": ["**/*"] +} From b293015c2727a95f0aacac595efe32a45e7ca690 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 25 Apr 2025 12:43:37 +0200 Subject: [PATCH 2/4] fix: add var exception --- tests/integration/helpers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index a2f57950..6808f216 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -25,6 +25,8 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati let mcpClient: Client | undefined; let mcpServer: Server | undefined; + // This gets used in the scope of tests. + // eslint-disable-next-line @typescript-eslint/no-unused-vars let randomDbName: string; beforeAll(async () => { From 140830c0082bc14738a76289845f3c917e07ec34 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 25 Apr 2025 14:58:33 +0200 Subject: [PATCH 3/4] fix: remove unused variable and helper --- tests/integration/helpers.ts | 10 ---------- tests/integration/server.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 6808f216..51b21657 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -25,10 +25,6 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati let mcpClient: Client | undefined; let mcpServer: Server | undefined; - // This gets used in the scope of tests. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let randomDbName: string; - beforeAll(async () => { const clientTransport = new InMemoryTransport(); const serverTransport = new InMemoryTransport(); @@ -69,7 +65,6 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati beforeEach(() => { config.telemetry = "disabled"; - randomDbName = new ObjectId().toString(); }); afterAll(async () => { @@ -222,8 +217,3 @@ export function validateThrowsForInvalidArguments( export function expectDefined(arg: T): asserts arg is Exclude { expect(arg).toBeDefined(); } - -/** Expects the argument being undefined and asserts it */ -export function expectUndefined(arg: unknown): asserts arg is undefined { - expect(arg).toBeUndefined(); -} diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index a8ed3c3f..3d12f129 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -50,8 +50,8 @@ describe("Server integration test", () => { it("should return capabilities", () => { const capabilities = integration.mcpClient().getServerCapabilities(); expectDefined(capabilities); - expect(capabilities?.completions).toBeUndefined(); - expect(capabilities?.experimental).toBeUndefined(); + expect(capabilities.completions).toBeUndefined(); + expect(capabilities.experimental).toBeUndefined(); expectDefined(capabilities?.tools); expectDefined(capabilities?.logging); expect(capabilities?.prompts).toBeUndefined(); From 6c2ae8d942fd350bd057a110e63b0e541560f8db Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 25 Apr 2025 15:02:12 +0200 Subject: [PATCH 4/4] fix: remove unused var --- tests/integration/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 51b21657..829fce73 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -5,7 +5,6 @@ import { config, UserConfig } from "../../src/config.js"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Session } from "../../src/session.js"; -import { ObjectId } from "bson"; interface ParameterInfo { name: string;