Skip to content

chore: add tests for db-stats #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/tools/mongodb/metadata/dbStats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs, OperationType } from "../../tool.js";
import { EJSON } from "bson";

export class DbStatsTool extends MongoDBToolBase {
protected name = "db-stats";
Expand All @@ -21,7 +22,11 @@ export class DbStatsTool extends MongoDBToolBase {
return {
content: [
{
text: `Statistics for database ${database}: ${JSON.stringify(result)}`,
text: `Statistics for database ${database}`,
type: "text",
},
{
text: EJSON.stringify(result),
type: "text",
},
],
Expand Down
19 changes: 17 additions & 2 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,27 @@ export function getParameters(tool: ToolInfo): ParameterInfo[] {
});
}

export const dbOperationParameters: ParameterInfo[] = [
export const databaseParameters: ParameterInfo[] = [
{ name: "database", type: "string", description: "Database name", required: true },
];

export const databaseCollectionParameters: ParameterInfo[] = [
...databaseParameters,
{ name: "collection", type: "string", description: "Collection name", required: true },
];

export const dbOperationInvalidArgTests = [{}, { database: 123 }, { foo: "bar", database: "test" }, { database: [] }];
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 function validateToolMetadata(
integration: IntegrationTest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
dbOperationInvalidArgTests,
databaseCollectionInvalidArgs,
} from "../../../helpers.js";

describeWithMongoDB("createCollection tool", (integration) => {
validateToolMetadata(
integration,
"create-collection",
"Creates a new collection in a database. If the database doesn't exist, it will be created automatically.",
dbOperationParameters
databaseCollectionParameters
);

validateThrowsForInvalidArguments(integration, "create-collection", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "create-collection", databaseCollectionInvalidArgs);

describe("with non-existent database", () => {
it("creates a new collection", async () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/tools/mongodb/create/createIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
} from "../../../helpers.js";
import { IndexDirection } from "mongodb";

describeWithMongoDB("createIndex tool", (integration) => {
validateToolMetadata(integration, "create-index", "Create an index for a collection", [
...dbOperationParameters,
...databaseCollectionParameters,
{
name: "keys",
type: "object",
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/tools/mongodb/create/insertMany.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
} from "../../../helpers.js";

describeWithMongoDB("insertMany tool", (integration) => {
validateToolMetadata(integration, "insert-many", "Insert an array of documents into a MongoDB collection", [
...dbOperationParameters,
...databaseCollectionParameters,
{
name: "documents",
type: "array",
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/tools/mongodb/delete/deleteMany.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
} from "../../../helpers.js";
Expand All @@ -13,7 +13,7 @@ describeWithMongoDB("deleteMany tool", (integration) => {
"delete-many",
"Removes all documents that match the filter from a MongoDB collection",
[
...dbOperationParameters,
...databaseCollectionParameters,
{
name: "filter",
type: "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
dbOperationInvalidArgTests,
databaseCollectionInvalidArgs,
} from "../../../helpers.js";

describeWithMongoDB("dropCollection tool", (integration) => {
validateToolMetadata(
integration,
"drop-collection",
"Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection.",
dbOperationParameters
databaseCollectionParameters
);

validateThrowsForInvalidArguments(integration, "drop-collection", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "drop-collection", databaseCollectionInvalidArgs);

it("can drop non-existing collection", async () => {
await integration.connectMcpClient();
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/tools/mongodb/delete/dropDatabase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
dbOperationInvalidArgTests,
databaseParameters,
databaseInvalidArgs,
} from "../../../helpers.js";

describeWithMongoDB("dropDatabase tool", (integration) => {
validateToolMetadata(
integration,
"drop-database",
"Removes the specified database, deleting the associated data files",
[dbOperationParameters.find((d) => d.name === "database")!]
databaseParameters
);

validateThrowsForInvalidArguments(integration, "drop-database", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "drop-database", databaseInvalidArgs);

it("can drop non-existing database", async () => {
let { databases } = await integration.mongoClient().db("").admin().listDatabases();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp
import {
getResponseElements,
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
dbOperationInvalidArgTests,
databaseCollectionInvalidArgs,
} from "../../../helpers.js";
import { Document } from "bson";
import { OptionalId } from "mongodb";
Expand All @@ -17,10 +17,10 @@ describeWithMongoDB("collectionSchema tool", (integration) => {
integration,
"collection-schema",
"Describe the schema for a collection",
dbOperationParameters
databaseCollectionParameters
);

validateThrowsForInvalidArguments(integration, "collection-schema", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "collection-schema", databaseCollectionInvalidArgs);

describe("with non-existent database", () => {
it("returns empty schema", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
databaseCollectionInvalidArgs,
validateToolMetadata,
dbOperationInvalidArgTests,
validateThrowsForInvalidArguments,
} from "../../../helpers.js";
import * as crypto from "crypto";
Expand All @@ -14,13 +14,13 @@ describeWithMongoDB("collectionStorageSize tool", (integration) => {
integration,
"collection-storage-size",
"Gets the size of the collection",
dbOperationParameters
databaseCollectionParameters
);

validateThrowsForInvalidArguments(integration, "collection-storage-size", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "collection-storage-size", databaseCollectionInvalidArgs);

describe("with non-existent database", () => {
it("returns 0 MB", async () => {
it("returns an error", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "collection-storage-size",
Expand Down
96 changes: 96 additions & 0 deletions tests/integration/tools/mongodb/metadata/dbStats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ObjectId } from "bson";
import {
databaseParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
databaseInvalidArgs,
getResponseElements,
} from "../../../helpers.js";
import * as crypto from "crypto";
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";

describeWithMongoDB("dbStats tool", (integration) => {
validateToolMetadata(
integration,
"db-stats",
"Returns statistics that reflect the use state of a single database",
databaseParameters
);

validateThrowsForInvalidArguments(integration, "db-stats", databaseInvalidArgs);

describe("with non-existent database", () => {
it("returns an error", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "db-stats",
arguments: { database: integration.randomDbName() },
});
const elements = getResponseElements(response.content);
expect(elements).toHaveLength(2);
expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`);

const stats = JSON.parse(elements[1].text);
expect(stats.db).toBe(integration.randomDbName());
expect(stats.collections).toBe(0);
expect(stats.storageSize).toBe(0);
});
});

describe("with existing database", () => {
const testCases = [
{
collections: {
foos: 3,
},
name: "single collection",
},
{
collections: {
foos: 2,
bars: 5,
},
name: "multiple collections",
},
];
for (const test of testCases) {
it(`returns correct stats for ${test.name}`, async () => {
for (const [name, count] of Object.entries(test.collections)) {
const objects = Array(count)
.fill(0)
.map(() => {
return { data: crypto.randomBytes(1024), _id: new ObjectId() };
});
await integration.mongoClient().db(integration.randomDbName()).collection(name).insertMany(objects);
}

await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "db-stats",
arguments: { database: integration.randomDbName() },
});
const elements = getResponseElements(response.content);
expect(elements).toHaveLength(2);
expect(elements[0].text).toBe(`Statistics for database ${integration.randomDbName()}`);

const stats = JSON.parse(elements[1].text);
expect(stats.db).toBe(integration.randomDbName());
expect(stats.collections).toBe(Object.entries(test.collections).length);
expect(stats.storageSize).toBeGreaterThan(1024);
expect(stats.objects).toBe(Object.values(test.collections).reduce((a, b) => a + b, 0));
});
}
});

describe("when not connected", () => {
validateAutoConnectBehavior(integration, "db-stats", () => {
return {
args: {
database: integration.randomDbName(),
collection: "foo",
},
expectedResponse: `Statistics for database ${integration.randomDbName()}`,
};
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import {
getResponseContent,
validateToolMetadata,
validateThrowsForInvalidArguments,
dbOperationInvalidArgTests,
databaseInvalidArgs,
databaseParameters,
} from "../../../helpers.js";

describeWithMongoDB("listCollections tool", (integration) => {
validateToolMetadata(integration, "list-collections", "List all collections for a given database", [
{ name: "database", description: "Database name", type: "string", required: true },
]);
validateToolMetadata(
integration,
"list-collections",
"List all collections for a given database",
databaseParameters
);

validateThrowsForInvalidArguments(integration, "list-collections", dbOperationInvalidArgTests);
validateThrowsForInvalidArguments(integration, "list-collections", databaseInvalidArgs);

describe("with non-existent database", () => {
it("returns no collections", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";

import { getResponseElements, getParameters } from "../../../helpers.js";

describeWithMongoDB("listDatabases tool", (integration) => {
Expand All @@ -21,7 +20,7 @@ describeWithMongoDB("listDatabases tool", (integration) => {
const response = await integration.mcpClient().callTool({ name: "list-databases", arguments: {} });
const dbNames = getDbNames(response.content);

expect(defaultDatabases).toIncludeAllMembers(defaultDatabases);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫣

expect(defaultDatabases).toIncludeAllMembers(dbNames);
});
});

Expand Down
4 changes: 2 additions & 2 deletions tests/integration/tools/mongodb/read/count.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelp

import {
getResponseContent,
dbOperationParameters,
databaseCollectionParameters,
validateToolMetadata,
validateThrowsForInvalidArguments,
} from "../../../helpers.js";
Expand All @@ -16,7 +16,7 @@ describeWithMongoDB("count tool", (integration) => {
type: "object",
required: false,
},
...dbOperationParameters,
...databaseCollectionParameters,
]);

validateThrowsForInvalidArguments(integration, "count", [
Expand Down
Loading