Skip to content

Commit ed14781

Browse files
committed
chore: add tests for updateMany and renameCollection
1 parent 09b74ec commit ed14781

File tree

4 files changed

+478
-11
lines changed

4 files changed

+478
-11
lines changed

src/tools/mongodb/update/renameCollection.ts

+33-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { MongoDBToolBase } from "../mongodbTool.js";
3+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55

66
export class RenameCollectionTool extends MongoDBToolBase {
77
protected name = "rename-collection";
88
protected description = "Renames a collection in a MongoDB database";
99
protected argsShape = {
10-
collection: z.string().describe("Collection name"),
11-
database: z.string().describe("Database name"),
10+
...DbOperationArgs,
1211
newName: z.string().describe("The new name for the collection"),
1312
dropTarget: z.boolean().optional().default(false).describe("If true, drops the target collection if it exists"),
1413
};
@@ -28,10 +27,40 @@ export class RenameCollectionTool extends MongoDBToolBase {
2827
return {
2928
content: [
3029
{
31-
text: `Collection \`${collection}\` renamed to \`${result.collectionName}\` in database \`${database}\`.`,
30+
text: `Collection "${collection}" renamed to "${result.collectionName}" in database "${database}".`,
3231
type: "text",
3332
},
3433
],
3534
};
3635
}
36+
37+
protected handleError(
38+
error: unknown,
39+
args: ToolArgs<typeof this.argsShape>
40+
): Promise<CallToolResult> | CallToolResult {
41+
if (error instanceof Error && "codeName" in error) {
42+
switch (error.codeName) {
43+
case "NamespaceNotFound":
44+
return {
45+
content: [
46+
{
47+
text: `Cannot rename "${args.database}.${args.collection}" because it doesn't exist.`,
48+
type: "text",
49+
},
50+
],
51+
};
52+
case "NamespaceExists":
53+
return {
54+
content: [
55+
{
56+
text: `Cannot rename "${args.database}.${args.collection}" to "${args.newName}" because the target collection already exists. If you want to overwrite it, set the "dropTarget" argument to true.`,
57+
type: "text",
58+
},
59+
],
60+
};
61+
}
62+
}
63+
64+
return super.handleError(error, args);
65+
}
3766
}

src/tools/mongodb/update/updateMany.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { MongoDBToolBase } from "../mongodbTool.js";
3+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55

66
export class UpdateManyTool extends MongoDBToolBase {
77
protected name = "update-many";
88
protected description = "Updates all documents that match the specified filter for a collection";
99
protected argsShape = {
10-
collection: z.string().describe("Collection name"),
11-
database: z.string().describe("Database name"),
10+
...DbOperationArgs,
1211
filter: z
1312
.object({})
1413
.passthrough()
@@ -19,7 +18,6 @@ export class UpdateManyTool extends MongoDBToolBase {
1918
update: z
2019
.object({})
2120
.passthrough()
22-
.optional()
2321
.describe("An update document describing the modifications to apply using update operator expressions"),
2422
upsert: z
2523
.boolean()
@@ -41,15 +39,15 @@ export class UpdateManyTool extends MongoDBToolBase {
4139
});
4240

4341
let message = "";
44-
if (result.matchedCount === 0) {
45-
message = `No documents matched the filter.`;
42+
if (result.matchedCount === 0 && result.modifiedCount === 0 && result.upsertedCount === 0) {
43+
message = "No documents matched the filter.";
4644
} else {
4745
message = `Matched ${result.matchedCount} document(s).`;
4846
if (result.modifiedCount > 0) {
4947
message += ` Modified ${result.modifiedCount} document(s).`;
5048
}
5149
if (result.upsertedCount > 0) {
52-
message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId?.toString()}).`;
50+
message += ` Upserted ${result.upsertedCount} document with id: ${result.upsertedId?.toString()}.`;
5351
}
5452
}
5553

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import {
2+
getResponseContent,
3+
databaseCollectionParameters,
4+
setupIntegrationTest,
5+
validateToolMetadata,
6+
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
8+
} from "../../../helpers.js";
9+
10+
describe("renameCollection tool", () => {
11+
const integration = setupIntegrationTest();
12+
13+
validateToolMetadata(integration, "rename-collection", "Renames a collection in a MongoDB database", [
14+
...databaseCollectionParameters,
15+
16+
{
17+
name: "newName",
18+
description: "The new name for the collection",
19+
type: "string",
20+
required: true,
21+
},
22+
{
23+
name: "dropTarget",
24+
description: "If true, drops the target collection if it exists",
25+
type: "boolean",
26+
required: false,
27+
},
28+
]);
29+
30+
validateThrowsForInvalidArguments(integration, "rename-collection", [
31+
{},
32+
{ database: 123, collection: "bar" },
33+
{ database: "test", collection: "bar", newName: "foo", extra: "extra" },
34+
{ database: "test", collection: [], newName: "foo" },
35+
{ database: "test", collection: "bar", newName: 10 },
36+
{ database: "test", collection: "bar", newName: "foo", dropTarget: "true" },
37+
{ database: "test", collection: "bar", newName: "foo", dropTarget: 1 },
38+
]);
39+
40+
describe("with non-existing database", () => {
41+
it("returns an error", async () => {
42+
await integration.connectMcpClient();
43+
const response = await integration.mcpClient().callTool({
44+
name: "rename-collection",
45+
arguments: { database: "non-existent", collection: "foos", newName: "bar" },
46+
});
47+
const content = getResponseContent(response.content);
48+
expect(content).toEqual(`Cannot rename "non-existent.foos" because it doesn't exist.`);
49+
});
50+
});
51+
52+
describe("with non-existing collection", () => {
53+
it("returns an error", async () => {
54+
await integration.mongoClient().db(integration.randomDbName()).collection("bar").insertOne({});
55+
56+
await integration.connectMcpClient();
57+
const response = await integration.mcpClient().callTool({
58+
name: "rename-collection",
59+
arguments: { database: integration.randomDbName(), collection: "non-existent", newName: "foo" },
60+
});
61+
const content = getResponseContent(response.content);
62+
expect(content).toEqual(
63+
`Cannot rename "${integration.randomDbName()}.non-existent" because it doesn't exist.`
64+
);
65+
});
66+
});
67+
68+
describe("with existing collection", () => {
69+
it("renames to non-existing collection", async () => {
70+
await integration
71+
.mongoClient()
72+
.db(integration.randomDbName())
73+
.collection("before")
74+
.insertOne({ value: 42 });
75+
76+
await integration.connectMcpClient();
77+
const response = await integration.mcpClient().callTool({
78+
name: "rename-collection",
79+
arguments: { database: integration.randomDbName(), collection: "before", newName: "after" },
80+
});
81+
const content = getResponseContent(response.content);
82+
expect(content).toEqual(
83+
`Collection "before" renamed to "after" in database "${integration.randomDbName()}".`
84+
);
85+
86+
const docsInBefore = await integration
87+
.mongoClient()
88+
.db(integration.randomDbName())
89+
.collection("before")
90+
.find({})
91+
.toArray();
92+
expect(docsInBefore).toHaveLength(0);
93+
94+
const docsInAfter = await integration
95+
.mongoClient()
96+
.db(integration.randomDbName())
97+
.collection("after")
98+
.find({})
99+
.toArray();
100+
expect(docsInAfter).toHaveLength(1);
101+
expect(docsInAfter[0].value).toEqual(42);
102+
});
103+
104+
it("returns an error when renaming to an existing collection", async () => {
105+
await integration
106+
.mongoClient()
107+
.db(integration.randomDbName())
108+
.collection("before")
109+
.insertOne({ value: 42 });
110+
await integration.mongoClient().db(integration.randomDbName()).collection("after").insertOne({ value: 84 });
111+
112+
await integration.connectMcpClient();
113+
const response = await integration.mcpClient().callTool({
114+
name: "rename-collection",
115+
arguments: { database: integration.randomDbName(), collection: "before", newName: "after" },
116+
});
117+
const content = getResponseContent(response.content);
118+
expect(content).toEqual(
119+
`Cannot rename "${integration.randomDbName()}.before" to "after" because the target collection already exists. If you want to overwrite it, set the "dropTarget" argument to true.`
120+
);
121+
122+
// Ensure no data was lost
123+
const docsInBefore = await integration
124+
.mongoClient()
125+
.db(integration.randomDbName())
126+
.collection("before")
127+
.find({})
128+
.toArray();
129+
expect(docsInBefore).toHaveLength(1);
130+
expect(docsInBefore[0].value).toEqual(42);
131+
132+
const docsInAfter = await integration
133+
.mongoClient()
134+
.db(integration.randomDbName())
135+
.collection("after")
136+
.find({})
137+
.toArray();
138+
expect(docsInAfter).toHaveLength(1);
139+
expect(docsInAfter[0].value).toEqual(84);
140+
});
141+
142+
it("renames to existing collection with dropTarget", async () => {
143+
await integration
144+
.mongoClient()
145+
.db(integration.randomDbName())
146+
.collection("before")
147+
.insertOne({ value: 42 });
148+
await integration.mongoClient().db(integration.randomDbName()).collection("after").insertOne({ value: 84 });
149+
150+
await integration.connectMcpClient();
151+
const response = await integration.mcpClient().callTool({
152+
name: "rename-collection",
153+
arguments: {
154+
database: integration.randomDbName(),
155+
collection: "before",
156+
newName: "after",
157+
dropTarget: true,
158+
},
159+
});
160+
const content = getResponseContent(response.content);
161+
expect(content).toEqual(
162+
`Collection "before" renamed to "after" in database "${integration.randomDbName()}".`
163+
);
164+
165+
// Ensure the data was moved
166+
const docsInBefore = await integration
167+
.mongoClient()
168+
.db(integration.randomDbName())
169+
.collection("before")
170+
.find({})
171+
.toArray();
172+
expect(docsInBefore).toHaveLength(0);
173+
174+
const docsInAfter = await integration
175+
.mongoClient()
176+
.db(integration.randomDbName())
177+
.collection("after")
178+
.find({})
179+
.toArray();
180+
expect(docsInAfter).toHaveLength(1);
181+
expect(docsInAfter[0].value).toEqual(42);
182+
});
183+
});
184+
185+
validateAutoConnectBehavior(
186+
integration,
187+
"rename-collection",
188+
() => {
189+
return {
190+
args: { database: integration.randomDbName(), collection: "coll1", newName: "coll2" },
191+
expectedResponse: `Collection "coll1" renamed to "coll2" in database "${integration.randomDbName()}".`,
192+
};
193+
},
194+
async () => {
195+
await integration.mongoClient().db(integration.randomDbName()).createCollection("coll1");
196+
}
197+
);
198+
});

0 commit comments

Comments
 (0)