dropIndex.test.ts•25.7 kB
import { describe, beforeEach, it, afterEach, expect, vi, type MockInstance } from "vitest";
import type { Collection } from "mongodb";
import {
databaseCollectionInvalidArgs,
databaseCollectionParameters,
defaultTestConfig,
getDataFromUntrustedContent,
getResponseContent,
validateThrowsForInvalidArguments,
validateToolMetadata,
} from "../../../helpers.js";
import {
describeWithMongoDB,
waitUntilSearchIndexIsListed,
waitUntilSearchIsReady,
type MongoDBIntegrationTestCase,
} from "../mongodbHelpers.js";
import { createMockElicitInput } from "../../../../utils/elicitationMocks.js";
import { Elicitation } from "../../../../../src/elicitation.js";
function setupForClassicIndexes(integration: MongoDBIntegrationTestCase): {
getMoviesCollection: () => Collection;
getIndexName: () => string;
} {
let moviesCollection: Collection;
let indexName: string;
beforeEach(async () => {
await integration.connectMcpClient();
const client = integration.mongoClient();
moviesCollection = client.db("mflix").collection("movies");
await moviesCollection.insertMany([
{
name: "Movie1",
year: 1994,
},
{
name: "Movie2",
year: 2001,
},
]);
indexName = await moviesCollection.createIndex({ year: 1 });
});
afterEach(async () => {
await moviesCollection.drop();
});
return {
getMoviesCollection: () => moviesCollection,
getIndexName: () => indexName,
};
}
function setupForVectorSearchIndexes(integration: MongoDBIntegrationTestCase): {
getMoviesCollection: () => Collection;
getIndexName: () => string;
} {
let moviesCollection: Collection;
const indexName = "searchIdx";
beforeEach(async () => {
await integration.connectMcpClient();
const mongoClient = integration.mongoClient();
moviesCollection = mongoClient.db("mflix").collection("movies");
await moviesCollection.insertMany([
{
name: "Movie1",
plot: "This is a horrible movie about a database called BongoDB and how it tried to copy the OG MangoDB.",
},
]);
await waitUntilSearchIsReady(mongoClient);
await moviesCollection.createSearchIndex({
name: indexName,
definition: { mappings: { dynamic: true } },
});
await waitUntilSearchIndexIsListed(moviesCollection, indexName);
});
afterEach(async () => {
// dropping collection also drops the associated search indexes
await moviesCollection.drop();
});
return {
getMoviesCollection: () => moviesCollection,
getIndexName: () => indexName,
};
}
describe.each([{ vectorSearchEnabled: false }, { vectorSearchEnabled: true }])(
"drop-index tool",
({ vectorSearchEnabled }) => {
describe(`when vector search feature flag is ${vectorSearchEnabled ? "enabled" : "disabled"}`, () => {
describeWithMongoDB(
"tool metadata and parameters",
(integration) => {
validateToolMetadata(
integration,
"drop-index",
"Drop an index for the provided database and collection.",
[
...databaseCollectionParameters,
{
name: "indexName",
type: "string",
description: "The name of the index to be dropped.",
required: true,
},
vectorSearchEnabled
? {
name: "type",
type: "string",
description:
"The type of index to be deleted. Use 'classic' for standard indexes and 'search' for atlas search and vector search indexes.",
required: true,
}
: {
name: "type",
type: "string",
description: "The type of index to be deleted. Is always set to 'classic'.",
required: false,
},
]
);
const invalidArgsTestCases = vectorSearchEnabled
? [
...databaseCollectionInvalidArgs,
{ database: "test", collection: "testColl", indexName: null, type: "classic" },
{ database: "test", collection: "testColl", indexName: undefined, type: "classic" },
{ database: "test", collection: "testColl", indexName: [], type: "classic" },
{ database: "test", collection: "testColl", indexName: true, type: "classic" },
{ database: "test", collection: "testColl", indexName: false, type: "search" },
{ database: "test", collection: "testColl", indexName: 0, type: "search" },
{ database: "test", collection: "testColl", indexName: 12, type: "search" },
{ database: "test", collection: "testColl", indexName: "", type: "search" },
// When feature flag is enabled anything other than search and
// classic are invalid
{ database: "test", collection: "testColl", indexName: "goodIndex", type: "anything" },
]
: [
...databaseCollectionInvalidArgs,
{ database: "test", collection: "testColl", indexName: null },
{ database: "test", collection: "testColl", indexName: undefined },
{ database: "test", collection: "testColl", indexName: [] },
{ database: "test", collection: "testColl", indexName: true },
{ database: "test", collection: "testColl", indexName: false },
{ database: "test", collection: "testColl", indexName: 0 },
{ database: "test", collection: "testColl", indexName: 12 },
{ database: "test", collection: "testColl", indexName: "" },
// When feature flag is disabled even "search" is an invalid
// argument
{ database: "test", collection: "testColl", indexName: "", type: "search" },
];
validateThrowsForInvalidArguments(integration, "drop-index", invalidArgsTestCases);
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
}
);
describeWithMongoDB(
"dropping classic indexes",
(integration) => {
const { getIndexName } = setupForClassicIndexes(integration);
describe.each([
{
database: "mflix",
collection: "non-existent",
},
{
database: "non-db",
collection: "non-coll",
},
])(
"when attempting to delete an index from non-existent namespace - $database $collection",
({ database, collection }) => {
it("should fail with error", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: vectorSearchEnabled
? { database, collection, indexName: "non-existent", type: "classic" }
: { database, collection, indexName: "non-existent" },
});
expect(response.isError).toBe(true);
const content = getResponseContent(response.content);
expect(content).toEqual(
`Error running drop-index: ns not found ${database}.${collection}`
);
});
}
);
describe("when attempting to delete an index that does not exist", () => {
it("should fail with error", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: vectorSearchEnabled
? {
database: "mflix",
collection: "movies",
indexName: "non-existent",
type: "classic",
}
: { database: "mflix", collection: "movies", indexName: "non-existent" },
});
expect(response.isError).toBe(true);
const content = getResponseContent(response.content);
expect(content).toEqual(
`Error running drop-index: index not found with name [non-existent]`
);
});
});
describe("when attempting to delete an index that exists", () => {
it("should succeed", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
// The index is created in beforeEach
arguments: vectorSearchEnabled
? {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "classic",
}
: { database: "mflix", collection: "movies", indexName: getIndexName() },
});
expect(response.isError).toBe(undefined);
const content = getResponseContent(response.content);
expect(content).toContain(`Successfully dropped the index from the provided namespace.`);
const data = getDataFromUntrustedContent(content);
expect(JSON.parse(data)).toMatchObject({
indexName: getIndexName(),
namespace: "mflix.movies",
});
});
});
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
}
);
const mockElicitInput = createMockElicitInput();
describeWithMongoDB(
"dropping classic indexes through an elicitation enabled client",
(integration) => {
const { getMoviesCollection, getIndexName } = setupForClassicIndexes(integration);
afterEach(() => {
mockElicitInput.clear();
});
it("should ask for confirmation before proceeding with tool call", async () => {
expect(await getMoviesCollection().listIndexes().toArray()).toHaveLength(2);
mockElicitInput.confirmYes();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: vectorSearchEnabled
? {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "classic",
}
: { database: "mflix", collection: "movies", indexName: getIndexName() },
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the index named `year_1` from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(await getMoviesCollection().listIndexes().toArray()).toHaveLength(1);
});
it("should not drop the index if the confirmation was not provided", async () => {
expect(await getMoviesCollection().listIndexes().toArray()).toHaveLength(2);
mockElicitInput.confirmNo();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: vectorSearchEnabled
? {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "classic",
}
: { database: "mflix", collection: "movies", indexName: getIndexName() },
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the index named `year_1` from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(await getMoviesCollection().listIndexes().toArray()).toHaveLength(2);
});
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: vectorSearchEnabled ? ["vectorSearch"] : [],
}),
getMockElicitationInput: () => mockElicitInput,
}
);
describe.skipIf(!vectorSearchEnabled)("dropping vector search indexes", () => {
describeWithMongoDB(
"when connected to MongoDB without search support",
(integration) => {
it("should fail with appropriate error when invoked", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database: "any", collection: "foo", indexName: "default", type: "search" },
});
const content = getResponseContent(response.content);
expect(response.isError).toBe(true);
expect(content).toContain(
"The connected MongoDB deployment does not support vector search indexes"
);
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }),
}
);
describeWithMongoDB(
"when connected to MongoDB with search support",
(integration) => {
const { getIndexName } = setupForVectorSearchIndexes(integration);
describe.each([
{
title: "an index from non-existent database",
database: "non-existent-db",
collection: "non-existent-coll",
indexName: "non-existent-index",
},
{
title: "an index from non-existent collection",
database: "mflix",
collection: "non-existent-coll",
indexName: "non-existent-index",
},
{
title: "a non-existent index",
database: "mflix",
collection: "movies",
indexName: "non-existent-index",
},
])(
"and attempting to delete $title (namespace - $database $collection)",
({ database, collection, indexName }) => {
it("should fail with appropriate error", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: { database, collection, indexName, type: "search" },
});
expect(response.isError).toBe(true);
const content = getResponseContent(response.content);
expect(content).toContain("Index does not exist in the provided namespace.");
const data = getDataFromUntrustedContent(content);
expect(JSON.parse(data)).toMatchObject({
indexName,
namespace: `${database}.${collection}`,
});
});
}
);
describe("and attempting to delete an existing index", () => {
it("should succeed in deleting the index", async () => {
const response = await integration.mcpClient().callTool({
name: "drop-index",
arguments: {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "search",
},
});
const content = getResponseContent(response.content);
expect(content).toContain(
"Successfully dropped the index from the provided namespace."
);
const data = getDataFromUntrustedContent(content);
expect(JSON.parse(data)).toMatchObject({
indexName: getIndexName(),
namespace: "mflix.movies",
});
});
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }),
downloadOptions: { search: true },
}
);
const mockElicitInput = createMockElicitInput();
describeWithMongoDB(
"when invoked via an elicitation enabled client",
(integration) => {
const { getIndexName } = setupForVectorSearchIndexes(integration);
let dropSearchIndexSpy: MockInstance;
beforeEach(() => {
// Note: Unlike drop-index tool test, we don't test the final state of
// indexes because of possible longer wait periods for changes to
// reflect, at-times taking >30 seconds.
dropSearchIndexSpy = vi.spyOn(
integration.mcpServer().session.serviceProvider,
"dropSearchIndex"
);
});
afterEach(() => {
mockElicitInput.clear();
});
it("should ask for confirmation before proceeding with tool call", async () => {
mockElicitInput.confirmYes();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "search",
},
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the search index named `searchIdx` from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(dropSearchIndexSpy).toHaveBeenCalledExactlyOnceWith(
"mflix",
"movies",
getIndexName()
);
});
it("should not drop the index if the confirmation was not provided", async () => {
mockElicitInput.confirmNo();
await integration.mcpClient().callTool({
name: "drop-index",
arguments: {
database: "mflix",
collection: "movies",
indexName: getIndexName(),
type: "search",
},
});
expect(mockElicitInput.mock).toHaveBeenCalledTimes(1);
expect(mockElicitInput.mock).toHaveBeenCalledWith({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining(
"You are about to drop the search index named `searchIdx` from the `mflix.movies` namespace"
),
requestedSchema: Elicitation.CONFIRMATION_SCHEMA,
});
expect(dropSearchIndexSpy).not.toHaveBeenCalled();
});
},
{
getUserConfig: () => ({ ...defaultTestConfig, previewFeatures: ["vectorSearch"] }),
downloadOptions: { search: true },
getMockElicitationInput: () => mockElicitInput,
}
);
});
});
}
);