Skip to content

Commit 6f330a3

Browse files
committed
Use hooks, clean up test setup a little
1 parent fe92d1c commit 6f330a3

File tree

4 files changed

+116
-131
lines changed

4 files changed

+116
-131
lines changed

tests/integration/helpers.ts

Lines changed: 87 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,100 @@ import fs from "fs/promises";
77
import { Session } from "../../src/session.js";
88
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
99

10-
export async function setupIntegrationTest(): Promise<{
11-
client: Client;
12-
server: Server;
13-
teardown: () => Promise<void>;
14-
}> {
15-
const clientTransport = new InMemoryTransport();
16-
const serverTransport = new InMemoryTransport();
17-
18-
await serverTransport.start();
19-
await clientTransport.start();
20-
21-
clientTransport.output.pipeTo(serverTransport.input);
22-
serverTransport.output.pipeTo(clientTransport.input);
23-
24-
const client = new Client(
25-
{
26-
name: "test-client",
27-
version: "1.2.3",
28-
},
29-
{
30-
capabilities: {},
31-
}
32-
);
33-
34-
const server = new Server({
35-
mcpServer: new McpServer({
36-
name: "test-server",
37-
version: "1.2.3",
38-
}),
39-
session: new Session(),
10+
export function jestTestMCPClient(): () => Client {
11+
let client: Client | undefined;
12+
let server: Server | undefined;
13+
14+
beforeEach(async () => {
15+
const clientTransport = new InMemoryTransport();
16+
const serverTransport = new InMemoryTransport();
17+
18+
await serverTransport.start();
19+
await clientTransport.start();
20+
21+
clientTransport.output.pipeTo(serverTransport.input);
22+
serverTransport.output.pipeTo(clientTransport.input);
23+
24+
client = new Client(
25+
{
26+
name: "test-client",
27+
version: "1.2.3",
28+
},
29+
{
30+
capabilities: {},
31+
}
32+
);
33+
34+
server = new Server({
35+
mcpServer: new McpServer({
36+
name: "test-server",
37+
version: "1.2.3",
38+
}),
39+
session: new Session(),
40+
});
41+
await server.connect(serverTransport);
42+
await client.connect(clientTransport);
43+
});
44+
45+
afterEach(async () => {
46+
await client?.close();
47+
client = undefined;
48+
49+
await server?.close();
50+
server = undefined;
4051
});
41-
await server.connect(serverTransport);
42-
await client.connect(clientTransport);
43-
44-
return {
45-
client,
46-
server,
47-
teardown: async () => {
48-
await client.close();
49-
await server.close();
50-
},
52+
53+
return () => {
54+
if (!client) {
55+
throw new Error("beforeEach() hook not ran yet");
56+
}
57+
58+
return client;
5159
};
5260
}
5361

54-
export async function runMongoDB(): Promise<runner.MongoCluster> {
55-
const tmpDir = path.join(__dirname, "..", "tmp");
56-
await fs.mkdir(tmpDir, { recursive: true });
62+
export function jestTestCluster(): () => runner.MongoCluster {
63+
let cluster: runner.MongoCluster | undefined;
5764

58-
try {
59-
const cluster = await MongoCluster.start({
60-
tmpDir: path.join(tmpDir, "mongodb-runner", "dbs"),
61-
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
62-
topology: "standalone",
63-
});
65+
function runMongodb() {}
66+
67+
beforeAll(async function () {
68+
// Downloading Windows executables in CI takes a long time because
69+
// they include debug symbols...
70+
const tmpDir = path.join(__dirname, "..", "tmp");
71+
await fs.mkdir(tmpDir, { recursive: true });
72+
73+
// On Windows, we may have a situation where mongod.exe is not fully released by the OS
74+
// before we attempt to run it again, so we add a retry.
75+
const dbsDir = path.join(tmpDir, "mongodb-runner", `dbs`);
76+
for (let i = 0; i < 10; i++) {
77+
try {
78+
cluster = await MongoCluster.start({
79+
tmpDir: dbsDir,
80+
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
81+
topology: "standalone",
82+
});
83+
84+
return;
85+
} catch (err) {
86+
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
87+
await new Promise((resolve) => setTimeout(resolve, 1000));
88+
}
89+
}
90+
}, 120_000);
91+
92+
afterAll(async function () {
93+
await cluster?.close();
94+
cluster = undefined;
95+
});
96+
97+
return () => {
98+
if (!cluster) {
99+
throw new Error("beforeAll() hook not ran yet");
100+
}
64101

65102
return cluster;
66-
} catch (err) {
67-
throw err;
68-
}
103+
};
69104
}
70105

71106
export function getResponseContent(content: unknown): string {

tests/integration/server.test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
1-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { setupIntegrationTest } from "./helpers.js";
1+
import { jestTestMCPClient } from "./helpers.js";
32

43
describe("Server integration test", () => {
5-
let client: Client;
6-
let teardown: () => Promise<void>;
7-
8-
beforeEach(async () => {
9-
({ client, teardown } = await setupIntegrationTest());
10-
});
11-
12-
afterEach(async () => {
13-
await teardown();
14-
});
4+
const client = jestTestMCPClient();
155

166
describe("list capabilities", () => {
177
it("should return positive number of tools", async () => {
18-
const tools = await client.listTools();
8+
const tools = await client().listTools();
199
expect(tools).toBeDefined();
2010
expect(tools.tools.length).toBeGreaterThan(0);
2111
});
2212

2313
it("should return no resources", async () => {
24-
await expect(() => client.listResources()).rejects.toMatchObject({
14+
await expect(() => client().listResources()).rejects.toMatchObject({
2515
message: "MCP error -32601: Method not found",
2616
});
2717
});
2818

2919
it("should return no prompts", async () => {
30-
await expect(() => client.listPrompts()).rejects.toMatchObject({
20+
await expect(() => client().listPrompts()).rejects.toMatchObject({
3121
message: "MCP error -32601: Method not found",
3222
});
3323
});
3424

3525
it("should return capabilities", async () => {
36-
const capabilities = client.getServerCapabilities();
26+
const capabilities = client().getServerCapabilities();
3727
expect(capabilities).toBeDefined();
3828
expect(capabilities?.completions).toBeUndefined();
3929
expect(capabilities?.experimental).toBeUndefined();

tests/integration/tools/mongodb/metadata/connect.test.ts

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,13 @@
1-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { runMongoDB, setupIntegrationTest, getResponseContent } from "../../../helpers.js";
3-
import runner from "mongodb-runner";
1+
import { getResponseContent, jestTestMCPClient, jestTestCluster } from "../../../helpers.js";
42

53
import config from "../../../../../src/config.js";
64

75
describe("Connect tool", () => {
8-
let client: Client;
9-
let serverClientTeardown: () => Promise<void>;
10-
11-
let cluster: runner.MongoCluster;
12-
13-
beforeAll(async () => {
14-
cluster = await runMongoDB();
15-
}, 60_000);
16-
17-
beforeEach(async () => {
18-
({ client, teardown: serverClientTeardown } = await setupIntegrationTest());
19-
});
20-
21-
afterEach(async () => {
22-
await serverClientTeardown?.();
23-
});
24-
25-
afterAll(async () => {
26-
await cluster.close();
27-
});
6+
const client = jestTestMCPClient();
7+
const cluster = jestTestCluster();
288

299
it("should have correct metadata", async () => {
30-
const { tools } = await client.listTools();
10+
const { tools } = await client().listTools();
3111
const connectTool = tools.find((tool) => tool.name === "connect")!;
3212
expect(connectTool).toBeDefined();
3313
expect(connectTool.description).toBe("Connect to a MongoDB instance");
@@ -50,7 +30,7 @@ describe("Connect tool", () => {
5030
describe("with default config", () => {
5131
describe("without connection string", () => {
5232
it("prompts for connection string", async () => {
53-
const response = await client.callTool({ name: "connect", arguments: {} });
33+
const response = await client().callTool({ name: "connect", arguments: {} });
5434
const content = getResponseContent(response.content);
5535
expect(content).toContain("No connection details provided");
5636
expect(content).toContain("mongodb://localhost:27017");
@@ -59,19 +39,19 @@ describe("Connect tool", () => {
5939

6040
describe("with connection string", () => {
6141
it("connects to the database", async () => {
62-
const response = await client.callTool({
42+
const response = await client().callTool({
6343
name: "connect",
64-
arguments: { connectionStringOrClusterName: cluster.connectionString },
44+
arguments: { connectionStringOrClusterName: cluster().connectionString },
6545
});
6646
const content = getResponseContent(response.content);
6747
expect(content).toContain("Successfully connected");
68-
expect(content).toContain(cluster.connectionString);
48+
expect(content).toContain(cluster().connectionString);
6949
});
7050
});
7151

7252
describe("with invalid connection string", () => {
7353
it("returns error message", async () => {
74-
const response = await client.callTool({
54+
const response = await client().callTool({
7555
name: "connect",
7656
arguments: { connectionStringOrClusterName: "mongodb://localhost:12345" },
7757
});
@@ -83,19 +63,19 @@ describe("Connect tool", () => {
8363

8464
describe("with connection string in config", () => {
8565
beforeEach(async () => {
86-
config.connectionString = cluster.connectionString;
66+
config.connectionString = cluster().connectionString;
8767
});
8868

8969
it("uses the connection string from config", async () => {
90-
const response = await client.callTool({ name: "connect", arguments: {} });
70+
const response = await client().callTool({ name: "connect", arguments: {} });
9171
const content = getResponseContent(response.content);
9272
expect(content).toContain("Successfully connected");
93-
expect(content).toContain(cluster.connectionString);
73+
expect(content).toContain(cluster().connectionString);
9474
});
9575

9676
it("prefers connection string from arguments", async () => {
97-
const newConnectionString = `${cluster.connectionString}?appName=foo-bar`;
98-
const response = await client.callTool({
77+
const newConnectionString = `${cluster().connectionString}?appName=foo-bar`;
78+
const response = await client().callTool({
9979
name: "connect",
10080
arguments: { connectionStringOrClusterName: newConnectionString },
10181
});

tests/integration/tools/mongodb/metadata/listDatabases.test.ts

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,13 @@
1-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { runMongoDB, setupIntegrationTest, getResponseElements, connect } from "../../../helpers.js";
3-
import runner from "mongodb-runner";
1+
import { getResponseElements, connect, jestTestCluster, jestTestMCPClient } from "../../../helpers.js";
42
import { MongoClient } from "mongodb";
53
import { toIncludeSameMembers } from "jest-extended";
64

75
describe("listDatabases tool", () => {
8-
let client: Client;
9-
let serverClientTeardown: () => Promise<void>;
10-
11-
let cluster: runner.MongoCluster;
12-
13-
beforeAll(async () => {
14-
cluster = await runMongoDB();
15-
}, 60_000);
16-
17-
beforeEach(async () => {
18-
({ client, teardown: serverClientTeardown } = await setupIntegrationTest());
19-
});
20-
21-
afterEach(async () => {
22-
await serverClientTeardown?.();
23-
});
24-
25-
afterAll(async () => {
26-
await cluster.close();
27-
});
6+
const client = jestTestMCPClient();
7+
const cluster = jestTestCluster();
288

299
it("should have correct metadata", async () => {
30-
const { tools } = await client.listTools();
10+
const { tools } = await client().listTools();
3111
const listDatabases = tools.find((tool) => tool.name === "list-databases")!;
3212
expect(listDatabases).toBeDefined();
3313
expect(listDatabases.description).toBe("List all databases for a MongoDB connection");
@@ -40,8 +20,8 @@ describe("listDatabases tool", () => {
4020

4121
describe("with no preexisting databases", () => {
4222
it("returns only the system databases", async () => {
43-
await connect(client, cluster);
44-
const response = await client.callTool({ name: "list-databases", arguments: {} });
23+
await connect(client(), cluster());
24+
const response = await client().callTool({ name: "list-databases", arguments: {} });
4525
const dbNames = getDbNames(response.content);
4626

4727
expect(dbNames).toIncludeSameMembers(["admin", "config", "local"]);
@@ -50,14 +30,14 @@ describe("listDatabases tool", () => {
5030

5131
describe("with preexisting databases", () => {
5232
it("returns their names and sizes", async () => {
53-
const mongoClient = new MongoClient(cluster.connectionString);
33+
const mongoClient = new MongoClient(cluster().connectionString);
5434
await mongoClient.db("foo").collection("bar").insertOne({ test: "test" });
5535
await mongoClient.db("baz").collection("qux").insertOne({ test: "test" });
5636
await mongoClient.close();
5737

58-
await connect(client, cluster);
38+
await connect(client(), cluster());
5939

60-
const response = await client.callTool({ name: "list-databases", arguments: {} });
40+
const response = await client().callTool({ name: "list-databases", arguments: {} });
6141
const dbNames = getDbNames(response.content);
6242
expect(dbNames).toIncludeSameMembers(["admin", "config", "local", "foo", "baz"]);
6343
});

0 commit comments

Comments
 (0)