1066 lines
42 KiB
TypeScript
Vendored
1066 lines
42 KiB
TypeScript
Vendored
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createPinia, setActivePinia } from "pinia";
|
|
import { nextTick, shallowRef } from "vue";
|
|
import type { TreeNode } from "@/types/database";
|
|
import {
|
|
cloneMongoCollectionError,
|
|
cloneMongoCollectionLoading,
|
|
cloneMongoCollectionName,
|
|
mongoCreateIndexError,
|
|
mongoCreateIndexForm,
|
|
mongoEditIndexOriginalName,
|
|
mongoIndexManagerError,
|
|
mongoIndexManagerLoading,
|
|
mongoIndexManagerMode,
|
|
mongoIndexManagerRows,
|
|
mongoIndexManagerSelectedName,
|
|
resetMongoCreateIndexForm,
|
|
resetMongoIndexManager,
|
|
sidebarDangerTarget,
|
|
sidebarFormTarget,
|
|
showCloneMongoCollectionDialog,
|
|
showCreateMongoIndexDialog,
|
|
showDropAllMongoIndexesConfirm,
|
|
showDropMongoCollectionConfirm,
|
|
showMongoIndexManagerDialog,
|
|
} from "@/components/sidebar/sidebarTreeDialogState";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
toast: vi.fn(),
|
|
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
|
loadIndexes: vi.fn().mockResolvedValue(undefined),
|
|
mongoListIndexSpecs: vi.fn().mockResolvedValue([]),
|
|
listMongoCompletionFields: vi.fn().mockResolvedValue([{ name: "email", type: "string" }]),
|
|
loadMongoCollections: vi.fn().mockResolvedValue(undefined),
|
|
loadMongoDatabases: vi.fn().mockResolvedValue(undefined),
|
|
removeTreeNode: vi.fn(),
|
|
mongoCreateIndex: vi.fn().mockResolvedValue({ name: "email_1" }),
|
|
mongoCloneCollection: vi.fn().mockResolvedValue({ documents_copied: 2, indexes_copied: 1 }),
|
|
mongoDropCollection: vi.fn().mockResolvedValue(undefined),
|
|
mongoDropDatabase: vi.fn().mockResolvedValue(undefined),
|
|
mongoDropIndexes: vi.fn().mockResolvedValue({ dropped_names: ["email_1"], affected_rows: 1 }),
|
|
getConfig: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("vue-i18n", () => ({
|
|
useI18n: () => ({
|
|
t: (key: string, params?: Record<string, unknown>) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/composables/useToast", () => ({
|
|
useToast: () => ({ toast: mocks.toast }),
|
|
}));
|
|
|
|
vi.mock("@/stores/connectionStore", () => ({
|
|
useConnectionStore: () => ({}),
|
|
}));
|
|
|
|
vi.mock("@/lib/backend/api", () => ({
|
|
mongoListIndexSpecs: (...args: unknown[]) => mocks.mongoListIndexSpecs(...args),
|
|
mongoCreateIndex: (...args: unknown[]) => mocks.mongoCreateIndex(...args),
|
|
mongoCloneCollection: (...args: unknown[]) => mocks.mongoCloneCollection(...args),
|
|
mongoDropCollection: (...args: unknown[]) => mocks.mongoDropCollection(...args),
|
|
mongoDropDatabase: (...args: unknown[]) => mocks.mongoDropDatabase(...args),
|
|
mongoDropIndexes: (...args: unknown[]) => mocks.mongoDropIndexes(...args),
|
|
mongoRenameCollection: vi.fn(),
|
|
nacosCreateNamespace: vi.fn(),
|
|
nacosUpdateNamespace: vi.fn(),
|
|
redisFlushDb: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/sidebar/sidebarActionTarget", () => ({
|
|
findSidebarActionTarget: () => null,
|
|
}));
|
|
|
|
import { useSidebarDatabaseSpecificMutationRuntime } from "@/composables/useSidebarDatabaseSpecificMutationRuntime";
|
|
|
|
function mongoConfig(driverProfile?: string, production = false) {
|
|
return {
|
|
id: "conn-1",
|
|
name: "Mongo",
|
|
db_type: "mongodb" as const,
|
|
driver_profile: driverProfile,
|
|
host: "localhost",
|
|
port: 27017,
|
|
username: "op",
|
|
password: "",
|
|
is_production: production,
|
|
};
|
|
}
|
|
|
|
function milvusConfig(database?: string) {
|
|
return {
|
|
id: "conn-1",
|
|
name: "Milvus",
|
|
db_type: "milvus" as const,
|
|
host: "localhost",
|
|
port: 19530,
|
|
username: "",
|
|
password: "",
|
|
database,
|
|
};
|
|
}
|
|
|
|
function milvusDatabaseNode(database: string): TreeNode {
|
|
return {
|
|
id: `conn-1:${database}`,
|
|
label: database,
|
|
type: "vector-database",
|
|
connectionId: "conn-1",
|
|
database,
|
|
isExpanded: false,
|
|
children: [],
|
|
};
|
|
}
|
|
|
|
function mongoDatabaseNode(): TreeNode {
|
|
return {
|
|
id: "conn-1:app",
|
|
label: "app",
|
|
type: "mongo-db",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
isExpanded: false,
|
|
};
|
|
}
|
|
|
|
function mongoCollectionNode(kind: "collection" | "view" | "timeseries" = "collection"): TreeNode {
|
|
return {
|
|
id: "conn-1:app:users",
|
|
label: "users",
|
|
type: "mongo-collection",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
meta: { collectionKind: kind },
|
|
isExpanded: false,
|
|
};
|
|
}
|
|
|
|
function mongoIndexesGroupNode(kind: "collection" | "view" | "timeseries" = "collection"): TreeNode {
|
|
return {
|
|
id: "conn-1:app:users:__indexes",
|
|
label: "tree.indexes",
|
|
type: "group-indexes",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
tableName: "users",
|
|
meta: { collectionKind: kind },
|
|
isExpanded: false,
|
|
children: [],
|
|
};
|
|
}
|
|
|
|
function mongoIndexNode(name: string, kind: "collection" | "view" | "timeseries" = "collection", isPrimary = name === "_id_"): TreeNode {
|
|
return {
|
|
id: `conn-1:app:users:__indexes:${name}`,
|
|
label: `${name} (email)`,
|
|
type: "index",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
tableName: "users",
|
|
meta: { name, columns: ["email"], is_primary: isPrimary, is_unique: false, collectionKind: kind },
|
|
isExpanded: false,
|
|
};
|
|
}
|
|
|
|
/** Let the panel's fire-and-forget index load settle before asserting. */
|
|
async function flush(): Promise<void> {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await nextTick();
|
|
}
|
|
|
|
function runtime(activeNode: TreeNode) {
|
|
const indexesGroup = activeNode.type === "group-indexes" ? activeNode : mongoIndexesGroupNode();
|
|
return useSidebarDatabaseSpecificMutationRuntime({
|
|
activeNode: shallowRef(activeNode),
|
|
connectionStore: {
|
|
getConfig: mocks.getConfig,
|
|
ensureConnected: mocks.ensureConnected,
|
|
loadIndexes: mocks.loadIndexes,
|
|
listMongoCompletionFields: mocks.listMongoCompletionFields,
|
|
loadMongoCollections: mocks.loadMongoCollections,
|
|
loadMongoDatabases: mocks.loadMongoDatabases,
|
|
removeTreeNode: mocks.removeTreeNode,
|
|
treeNodes: [indexesGroup],
|
|
} as any,
|
|
});
|
|
}
|
|
|
|
describe("MongoDB sidebar mutation runtime", () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
vi.clearAllMocks();
|
|
mocks.getConfig.mockReturnValue(mongoConfig());
|
|
mocks.ensureConnected.mockResolvedValue(undefined);
|
|
mocks.loadIndexes.mockResolvedValue(undefined);
|
|
mocks.mongoListIndexSpecs.mockResolvedValue([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
{
|
|
name: "email_1",
|
|
keys: [{ field: "email", direction: "1" }],
|
|
is_unique: false,
|
|
is_primary: false,
|
|
is_sparse: true,
|
|
expire_after_seconds: 3600,
|
|
partial_filter_expression: '{"archived":false}',
|
|
background: false,
|
|
bucket_size: null,
|
|
hidden: false,
|
|
properties_complete: true,
|
|
extra_options: null,
|
|
},
|
|
]);
|
|
mocks.listMongoCompletionFields.mockResolvedValue([{ name: "email", type: "string" }]);
|
|
mocks.loadMongoCollections.mockResolvedValue(undefined);
|
|
mocks.loadMongoDatabases.mockResolvedValue(undefined);
|
|
mocks.mongoCreateIndex.mockResolvedValue({ name: "email_1" });
|
|
mocks.mongoCloneCollection.mockResolvedValue({ documents_copied: 2, indexes_copied: 1 });
|
|
mocks.mongoDropCollection.mockResolvedValue(undefined);
|
|
mocks.mongoDropDatabase.mockResolvedValue(undefined);
|
|
mocks.mongoDropIndexes.mockResolvedValue({ dropped_names: ["email_1"], affected_rows: 1 });
|
|
sidebarDangerTarget.value = null;
|
|
sidebarFormTarget.value = null;
|
|
showCreateMongoIndexDialog.value = false;
|
|
showCloneMongoCollectionDialog.value = false;
|
|
showDropAllMongoIndexesConfirm.value = false;
|
|
showDropMongoCollectionConfirm.value = false;
|
|
showMongoIndexManagerDialog.value = false;
|
|
resetMongoCreateIndexForm();
|
|
resetMongoIndexManager();
|
|
cloneMongoCollectionName.value = "";
|
|
cloneMongoCollectionError.value = "";
|
|
cloneMongoCollectionLoading.value = false;
|
|
});
|
|
|
|
it("protects the built-in and configured Milvus databases from deletion", () => {
|
|
mocks.getConfig.mockReturnValue(milvusConfig("analytics"));
|
|
const activeNode = shallowRef(milvusDatabaseNode("default"));
|
|
const feature = useSidebarDatabaseSpecificMutationRuntime({
|
|
activeNode,
|
|
connectionStore: {
|
|
getConfig: mocks.getConfig,
|
|
} as any,
|
|
});
|
|
|
|
expect(feature.canDropMilvusDatabase.value).toBe(false);
|
|
|
|
activeNode.value = milvusDatabaseNode("analytics");
|
|
expect(feature.canDropMilvusDatabase.value).toBe(false);
|
|
|
|
activeNode.value = milvusDatabaseNode("archive");
|
|
expect(feature.canDropMilvusDatabase.value).toBe(true);
|
|
|
|
mocks.getConfig.mockReturnValue(mongoConfig());
|
|
activeNode.value = milvusDatabaseNode("archive-2");
|
|
expect(feature.canDropMilvusDatabase.value).toBe(false);
|
|
});
|
|
|
|
it("keeps Legacy MongoDB mutations available while limiting index actions to the Indexes group", () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
|
const activeNode = shallowRef(mongoDatabaseNode());
|
|
const feature = useSidebarDatabaseSpecificMutationRuntime({
|
|
activeNode,
|
|
connectionStore: {
|
|
getConfig: mocks.getConfig,
|
|
ensureConnected: mocks.ensureConnected,
|
|
loadIndexes: mocks.loadIndexes,
|
|
listMongoCompletionFields: mocks.listMongoCompletionFields,
|
|
loadMongoCollections: mocks.loadMongoCollections,
|
|
loadMongoDatabases: mocks.loadMongoDatabases,
|
|
treeNodes: [],
|
|
} as any,
|
|
});
|
|
|
|
expect(feature.canDropMongoDatabase.value).toBe(true);
|
|
activeNode.value = mongoCollectionNode();
|
|
expect(feature.canDropMongoCollection.value).toBe(true);
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
|
expect(feature.canRenameMongoCollection.value).toBe(false);
|
|
expect(feature.canCloneMongoCollection.value).toBe(true);
|
|
expect(feature.canCreateMongoIndex.value).toBe(false);
|
|
activeNode.value = mongoIndexesGroupNode();
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(true);
|
|
expect(feature.canCreateMongoIndex.value).toBe(true);
|
|
activeNode.value = mongoIndexesGroupNode("timeseries");
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(true);
|
|
expect(feature.canCreateMongoIndex.value).toBe(true);
|
|
activeNode.value = mongoIndexNode("email_1");
|
|
expect(feature.canDropMongoIndex.value).toBe(true);
|
|
activeNode.value = mongoIndexNode("_id_");
|
|
expect(feature.canDropMongoIndex.value).toBe(false);
|
|
});
|
|
|
|
it("keeps collection deletion available for views without exposing unsupported index actions", () => {
|
|
const activeNode = shallowRef(mongoCollectionNode("view"));
|
|
const feature = useSidebarDatabaseSpecificMutationRuntime({
|
|
activeNode,
|
|
connectionStore: {
|
|
getConfig: mocks.getConfig,
|
|
ensureConnected: mocks.ensureConnected,
|
|
loadIndexes: mocks.loadIndexes,
|
|
listMongoCompletionFields: mocks.listMongoCompletionFields,
|
|
loadMongoCollections: mocks.loadMongoCollections,
|
|
loadMongoDatabases: mocks.loadMongoDatabases,
|
|
treeNodes: [],
|
|
} as any,
|
|
});
|
|
|
|
expect(feature.canDropMongoCollection.value).toBe(true);
|
|
expect(feature.canCloneMongoCollection.value).toBe(false);
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
|
activeNode.value = mongoIndexesGroupNode("view");
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
|
expect(feature.canCreateMongoIndex.value).toBe(false);
|
|
activeNode.value = mongoIndexNode("email_1", "view");
|
|
expect(feature.canDropMongoIndex.value).toBe(false);
|
|
});
|
|
|
|
it("does not expose Indexes group mutations for read-only MongoDB connections", () => {
|
|
mocks.getConfig.mockReturnValue({ ...mongoConfig(), read_only: true });
|
|
const feature = runtime(mongoIndexesGroupNode());
|
|
|
|
expect(feature.canCreateMongoIndex.value).toBe(false);
|
|
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
|
});
|
|
|
|
it("clones a regular MongoDB collection through the Legacy Agent and refreshes the sidebar", async () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
expect(feature.canCloneMongoCollection.value).toBe(true);
|
|
feature.prepareCloneMongoCollectionDialog();
|
|
expect(cloneMongoCollectionName.value).toBe("users_copy");
|
|
|
|
await feature.confirmCloneMongoCollection();
|
|
|
|
expect(mocks.ensureConnected).toHaveBeenCalledWith("conn-1");
|
|
expect(mocks.mongoCloneCollection).toHaveBeenCalledWith("conn-1", "app", "users", "users_copy");
|
|
expect(mocks.loadMongoCollections).toHaveBeenCalledWith("conn-1", "app");
|
|
expect(showCloneMongoCollectionDialog.value).toBe(false);
|
|
expect(mocks.toast).toHaveBeenCalledWith('contextMenu.cloneCollectionSuccess:{"name":"users_copy","documents":2,"indexes":1}', 3000);
|
|
});
|
|
|
|
it("refreshes the collection list when cloning fails after a target may have been created", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
mocks.mongoCloneCollection.mockRejectedValue(new Error("index build failed"));
|
|
|
|
feature.prepareCloneMongoCollectionDialog();
|
|
await feature.confirmCloneMongoCollection();
|
|
|
|
expect(mocks.loadMongoCollections).toHaveBeenCalledWith("conn-1", "app");
|
|
expect(showCloneMongoCollectionDialog.value).toBe(true);
|
|
expect(cloneMongoCollectionError.value).toContain("index build failed");
|
|
});
|
|
|
|
it("creates an index from the shared sidebar dialog state", async () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareCreateMongoIndexDialog();
|
|
mongoCreateIndexForm.value = {
|
|
name: "email_created_at",
|
|
fields: [
|
|
{ id: 1, path: "email", type: "1" },
|
|
{ id: 2, path: "createdAt", type: "-1" },
|
|
],
|
|
unique: true,
|
|
sparse: false,
|
|
};
|
|
await feature.confirmCreateMongoIndex();
|
|
|
|
expect(showCreateMongoIndexDialog.value).toBe(false);
|
|
expect(mocks.ensureConnected).toHaveBeenCalledWith("conn-1");
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledWith("conn-1", "app", "users", '{"email":1,"createdAt":-1}', '{"name":"email_created_at","unique":true}');
|
|
expect(mocks.loadIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, "conn-1:app:users:__indexes", undefined);
|
|
expect(mocks.toast).toHaveBeenCalledWith('contextMenu.createMongoIndexSuccess:{"name":"email_1","collection":"users"}', 3000);
|
|
});
|
|
|
|
it("starts every create-index dialog with safe defaults", () => {
|
|
const feature = runtime(mongoIndexesGroupNode());
|
|
mongoCreateIndexForm.value = {
|
|
name: "stale_index",
|
|
fields: [{ id: 7, path: "location", type: "2dsphere" }],
|
|
unique: true,
|
|
sparse: true,
|
|
expireAfterSeconds: "600",
|
|
partialFilterExpression: '{"active":true}',
|
|
background: true,
|
|
bucketSize: "32",
|
|
};
|
|
mongoCreateIndexError.value = "previous failure";
|
|
|
|
feature.prepareCreateMongoIndexDialog();
|
|
|
|
expect(mongoCreateIndexForm.value).toEqual({
|
|
name: "",
|
|
fields: [{ id: 1, path: "", type: "1" }],
|
|
unique: false,
|
|
sparse: false,
|
|
expireAfterSeconds: "",
|
|
partialFilterExpression: "",
|
|
background: false,
|
|
bucketSize: "",
|
|
hidden: false,
|
|
});
|
|
expect(mongoCreateIndexError.value).toBe("");
|
|
expect(showCreateMongoIndexDialog.value).toBe(true);
|
|
});
|
|
|
|
it("does not expose index creation through a collection node", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareCreateMongoIndexDialog();
|
|
expect(showCreateMongoIndexDialog.value).toBe(false);
|
|
await feature.confirmCreateMongoIndex();
|
|
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("opens the index manager panel from a collection node and lists its indexes", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
expect(feature.canManageMongoIndexes.value).toBe(true);
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
expect(showMongoIndexManagerDialog.value).toBe(true);
|
|
expect(mocks.mongoListIndexSpecs).toHaveBeenCalledWith("conn-1", "app", "users");
|
|
expect(mongoIndexManagerRows.value).toEqual([
|
|
{
|
|
name: "_id_",
|
|
keys: "_id ASC",
|
|
isUnique: true,
|
|
isProtected: true,
|
|
isSparse: false,
|
|
expireAfterSeconds: undefined,
|
|
partialFilterExpression: undefined,
|
|
background: false,
|
|
bucketSize: undefined,
|
|
hidden: false,
|
|
propertiesComplete: true,
|
|
extraOptions: undefined,
|
|
},
|
|
{
|
|
name: "email_1",
|
|
keys: "email ASC",
|
|
isUnique: false,
|
|
isProtected: false,
|
|
isSparse: true,
|
|
expireAfterSeconds: 3600,
|
|
partialFilterExpression: '{"archived":false}',
|
|
background: false,
|
|
bucketSize: undefined,
|
|
hidden: false,
|
|
propertiesComplete: true,
|
|
extraOptions: undefined,
|
|
},
|
|
]);
|
|
// The first row is auto-selected so the property pane is never blank.
|
|
expect(mongoIndexManagerSelectedName.value).toBe("_id_");
|
|
expect(feature.canDropSelectedMongoIndexRow.value).toBe(false);
|
|
});
|
|
|
|
it("creates an index from the panel opened on a collection node and returns to view mode", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.startCreateMongoIndexDraft();
|
|
expect(mongoIndexManagerMode.value).toBe("create");
|
|
mongoCreateIndexForm.value = { ...mongoCreateIndexForm.value, fields: [{ id: 1, path: "email", type: "1" }], expireAfterSeconds: "3600" };
|
|
await feature.confirmCreateMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledWith("conn-1", "app", "users", '{"email":1}', '{"expireAfterSeconds":3600}');
|
|
// The panel stays open, drops back to read-only, and selects the new index.
|
|
expect(showMongoIndexManagerDialog.value).toBe(true);
|
|
expect(mongoIndexManagerMode.value).toBe("view");
|
|
expect(mongoIndexManagerSelectedName.value).toBe("email_1");
|
|
});
|
|
|
|
it("drops the selected index from the panel while protecting the default index", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
expect(feature.canDropSelectedMongoIndexRow.value).toBe(true);
|
|
await feature.dropSelectedMongoIndexRow();
|
|
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", '"email_1"', true);
|
|
|
|
feature.selectMongoIndexRow("_id_");
|
|
mocks.mongoDropIndexes.mockClear();
|
|
await feature.dropSelectedMongoIndexRow();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("edits the selected index by dropping then creating and returns to view mode", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
expect(feature.canEditSelectedMongoIndexRow.value).toBe(true);
|
|
|
|
feature.startEditMongoIndexDraft();
|
|
expect(mongoIndexManagerMode.value).toBe("edit");
|
|
expect(mongoEditIndexOriginalName.value).toBe("email_1");
|
|
// The form is prefilled from the selected row.
|
|
expect(mongoCreateIndexForm.value.name).toBe("email_1");
|
|
expect(mongoCreateIndexForm.value.fields).toEqual([{ id: 1, path: "email", type: "1" }]);
|
|
expect(mongoCreateIndexForm.value.sparse).toBe(true);
|
|
expect(mongoCreateIndexForm.value.expireAfterSeconds).toBe("3600");
|
|
|
|
// Rename: create new first, then drop old.
|
|
mongoCreateIndexForm.value.name = "email_renamed";
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
// Re-read happens before any mutation; then create(new) before drop(old).
|
|
expect(mocks.mongoListIndexSpecs).toHaveBeenCalledWith("conn-1", "app", "users");
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledWith("conn-1", "app", "users", '{"email":1}', '{"name":"email_renamed","sparse":true,"expireAfterSeconds":3600,"partialFilterExpression":{"archived":false}}');
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", '"email_1"', true);
|
|
expect(mongoIndexManagerMode.value).toBe("view");
|
|
expect(mongoEditIndexOriginalName.value).toBe("");
|
|
expect(mongoIndexManagerSelectedName.value).toBe("email_1");
|
|
});
|
|
|
|
it("does not edit the protected _id index", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("_id_");
|
|
expect(feature.canEditSelectedMongoIndexRow.value).toBe(false);
|
|
|
|
feature.startEditMongoIndexDraft();
|
|
expect(mongoIndexManagerMode.value).toBe("view");
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not edit or rename an index without a complete server specification", async () => {
|
|
mocks.mongoListIndexSpecs.mockResolvedValueOnce([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
{
|
|
name: "email_1",
|
|
keys: [{ field: "email", direction: "1" }],
|
|
is_unique: false,
|
|
is_primary: false,
|
|
is_sparse: false,
|
|
expire_after_seconds: null,
|
|
partial_filter_expression: null,
|
|
background: false,
|
|
bucket_size: null,
|
|
hidden: false,
|
|
properties_complete: false,
|
|
extra_options: null,
|
|
},
|
|
]);
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
feature.selectMongoIndexRow("email_1");
|
|
|
|
expect(feature.canEditSelectedMongoIndexRow.value).toBe(false);
|
|
feature.startEditMongoIndexDraft();
|
|
expect(mongoIndexManagerMode.value).toBe("view");
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("creates the new index first when renaming and does not drop if create fails", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.name = "email_v2";
|
|
|
|
mocks.mongoCreateIndex.mockRejectedValueOnce(new Error("duplicate key"));
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
// Create was attempted; drop was never called because create failed.
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalled();
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
// The error surfaces in the panel.
|
|
expect(mongoCreateIndexError.value).toContain("duplicate key");
|
|
});
|
|
|
|
it("restores the complete original specification when a same-name rebuild fails", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.unique = true;
|
|
|
|
mocks.mongoCreateIndex.mockRejectedValueOnce(new Error("index build failed")).mockResolvedValueOnce({ name: "email_1" });
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", '"email_1"', true);
|
|
expect(mocks.mongoCreateIndex).toHaveBeenNthCalledWith(1, "conn-1", "app", "users", '{"email":1}', '{"name":"email_1","unique":true,"sparse":true,"expireAfterSeconds":3600,"partialFilterExpression":{"archived":false}}');
|
|
expect(mocks.mongoCreateIndex).toHaveBeenNthCalledWith(2, "conn-1", "app", "users", '{"email":1}', '{"name":"email_1","sparse":true,"expireAfterSeconds":3600,"partialFilterExpression":{"archived":false}}');
|
|
expect(mongoCreateIndexError.value).toContain("email_1");
|
|
expect(mongoCreateIndexError.value).toContain("index build failed");
|
|
expect(mongoCreateIndexError.value).toContain("mongoEditIndexCreateFailedRolledBack");
|
|
});
|
|
|
|
it("reports both replacement and rollback errors when restoring a same-name index fails", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.unique = true;
|
|
|
|
mocks.mongoCreateIndex.mockRejectedValueOnce(new Error("replacement failed")).mockRejectedValueOnce(new Error("rollback failed"));
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledTimes(2);
|
|
expect(mongoCreateIndexError.value).toContain("replacement failed");
|
|
expect(mongoCreateIndexError.value).toContain("rollback failed");
|
|
});
|
|
|
|
it("aborts the edit when the index no longer exists on the server", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
|
|
// Simulate the index being deleted by another session before confirm.
|
|
mocks.mongoListIndexSpecs.mockResolvedValueOnce([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
]);
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(mongoCreateIndexError.value).toContain("email_1");
|
|
});
|
|
|
|
it("aborts the edit when the index keys changed since the dialog was opened", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
|
|
// Simulate the index being modified by another session (direction changed).
|
|
mocks.mongoListIndexSpecs.mockResolvedValueOnce([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
{ name: "email_1", keys: [{ field: "email", direction: "-1" }], is_unique: false, is_primary: false, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
]);
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(mongoCreateIndexError.value).toContain("email_1");
|
|
});
|
|
|
|
it("aborts the edit when the keys match but the complete server options changed", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
|
|
mocks.mongoListIndexSpecs.mockResolvedValueOnce([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
{
|
|
name: "email_1",
|
|
keys: [{ field: "email", direction: "1" }],
|
|
is_unique: true,
|
|
is_primary: false,
|
|
is_sparse: true,
|
|
expire_after_seconds: 3600,
|
|
partial_filter_expression: '{"archived":false}',
|
|
background: false,
|
|
bucket_size: null,
|
|
hidden: true,
|
|
properties_complete: true,
|
|
extra_options: '{"collation":{"locale":"fr"}}',
|
|
},
|
|
]);
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(mongoCreateIndexError.value).toContain("mongoEditIndexStale");
|
|
});
|
|
|
|
it("does not report a rename as successful when drop returns structured failures", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.name = "email_v2";
|
|
mocks.mongoCreateIndex.mockResolvedValueOnce({ name: "email_v2" });
|
|
mocks.mongoDropIndexes.mockResolvedValueOnce({
|
|
dropped_names: [],
|
|
affected_rows: 0,
|
|
failures: [{ name: "email_1", message: "not authorized" }],
|
|
});
|
|
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledOnce();
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledOnce();
|
|
expect(mongoIndexManagerMode.value).toBe("edit");
|
|
expect(mongoCreateIndexError.value).toContain("not authorized");
|
|
expect(mongoCreateIndexError.value).toContain("mongoEditIndexDropFailedAfterCreate");
|
|
expect(mocks.toast).not.toHaveBeenCalledWith(expect.stringContaining("mongoEditIndexSuccess"), 3000);
|
|
});
|
|
|
|
it("does not create or report rollback when a same-name drop returns structured failures", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
feature.selectMongoIndexRow("email_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.unique = true;
|
|
mocks.mongoDropIndexes.mockResolvedValueOnce({
|
|
dropped_names: [],
|
|
affected_rows: 0,
|
|
failures: [{ name: "email_1", message: "index is busy" }],
|
|
});
|
|
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(mongoIndexManagerMode.value).toBe("edit");
|
|
expect(mongoCreateIndexError.value).toContain("index is busy");
|
|
expect(mongoCreateIndexError.value).toContain("mongoEditIndexDropFailed");
|
|
expect(mongoCreateIndexError.value).not.toContain("mongoEditIndexCreateFailedRolledBack");
|
|
});
|
|
|
|
it("preserves extra options (collation, wildcardProjection) through the edit", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
|
|
// Seed the list with an index that has extraOptions.
|
|
mocks.mongoListIndexSpecs.mockReturnValue([
|
|
{ name: "_id_", keys: [{ field: "_id", direction: "1" }], is_unique: true, is_primary: true, is_sparse: false, expire_after_seconds: null, partial_filter_expression: null, background: false, bucket_size: null, hidden: false, properties_complete: true, extra_options: null },
|
|
{
|
|
name: "wild_1",
|
|
keys: [{ field: "x", direction: "1" }],
|
|
is_unique: false,
|
|
is_primary: false,
|
|
is_sparse: false,
|
|
expire_after_seconds: null,
|
|
partial_filter_expression: null,
|
|
background: false,
|
|
bucket_size: null,
|
|
hidden: false,
|
|
properties_complete: true,
|
|
extra_options: '{"wildcardProjection":{"x":1}}',
|
|
},
|
|
]);
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
feature.selectMongoIndexRow("wild_1");
|
|
feature.startEditMongoIndexDraft();
|
|
mongoCreateIndexForm.value.name = "wild_v2";
|
|
await feature.confirmEditMongoIndex();
|
|
await flush();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalled();
|
|
const createArgs = mocks.mongoCreateIndex.mock.calls[0];
|
|
const optionsJson = createArgs[4];
|
|
expect(optionsJson).toContain("wildcardProjection");
|
|
});
|
|
|
|
it("surfaces an index-list failure in the panel instead of showing a stale list", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
mocks.mongoListIndexSpecs.mockRejectedValueOnce(new Error("listIndexes failed"));
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
await flush();
|
|
|
|
expect(mongoIndexManagerRows.value).toEqual([]);
|
|
expect(mongoIndexManagerSelectedName.value).toBe("");
|
|
expect(mongoIndexManagerError.value).toContain("listIndexes failed");
|
|
expect(mongoIndexManagerLoading.value).toBe(false);
|
|
});
|
|
|
|
it("does not open the index manager panel for read-only MongoDB connections", () => {
|
|
mocks.getConfig.mockReturnValue({ ...mongoConfig(), read_only: true });
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
|
|
feature.prepareMongoIndexManagerDialog();
|
|
|
|
expect(showMongoIndexManagerDialog.value).toBe(false);
|
|
expect(mocks.mongoListIndexSpecs).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not clear indexes through a collection node", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
|
|
await feature.confirmDropAllMongoIndexes();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the sidebar form target when the active node changes", async () => {
|
|
const originalTarget = mongoIndexesGroupNode();
|
|
const activeNode = shallowRef(originalTarget);
|
|
const feature = useSidebarDatabaseSpecificMutationRuntime({
|
|
activeNode,
|
|
connectionStore: {
|
|
getConfig: mocks.getConfig,
|
|
ensureConnected: mocks.ensureConnected,
|
|
loadIndexes: mocks.loadIndexes,
|
|
listMongoCompletionFields: mocks.listMongoCompletionFields,
|
|
loadMongoCollections: mocks.loadMongoCollections,
|
|
loadMongoDatabases: mocks.loadMongoDatabases,
|
|
treeNodes: [originalTarget],
|
|
} as any,
|
|
});
|
|
|
|
sidebarFormTarget.value = originalTarget;
|
|
feature.prepareCreateMongoIndexDialog();
|
|
mongoCreateIndexForm.value.fields[0]!.path = "email";
|
|
activeNode.value = {
|
|
...mongoIndexesGroupNode(),
|
|
id: "conn-1:app:orders:__indexes",
|
|
tableName: "orders",
|
|
};
|
|
await feature.confirmCreateMongoIndex();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledWith("conn-1", "app", "users", '{"email":1}', undefined);
|
|
expect(mocks.loadIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, "conn-1:app:users:__indexes", undefined);
|
|
});
|
|
|
|
it("drops every removable index through the shared mutation and refreshes metadata", async () => {
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
showDropAllMongoIndexesConfirm.value = true;
|
|
mocks.mongoDropIndexes.mockResolvedValueOnce({ dropped_names: ["email_1", "created_at_-1"], affected_rows: 2 });
|
|
|
|
expect(feature.mongoDropAllIndexesPreview(node)).toBe('db.getSiblingDB("app").getCollection("users").dropIndexes()');
|
|
await feature.confirmDropAllMongoIndexes();
|
|
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, false);
|
|
expect(mocks.loadIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, "conn-1:app:users:__indexes", undefined);
|
|
expect(showDropAllMongoIndexesConfirm.value).toBe(false);
|
|
expect(mocks.toast).toHaveBeenCalledWith('contextMenu.dropAllIndexesSuccess:{"count":2,"name":"users"}', 3000);
|
|
});
|
|
|
|
it("refreshes index metadata after a failed delete request", async () => {
|
|
const node = mongoIndexNode("email_1");
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
mocks.mongoDropIndexes.mockRejectedValueOnce(new Error("connection lost"));
|
|
|
|
await feature.confirmDropMongoIndex();
|
|
|
|
expect(mocks.loadIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, "conn-1:app:users:__indexes", undefined);
|
|
expect(mocks.toast).toHaveBeenCalledWith(expect.stringContaining("contextMenu.tableOperationFailed"), 5000);
|
|
});
|
|
|
|
it("reports partial index deletion after forcing a metadata refresh", async () => {
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
mocks.mongoDropIndexes.mockResolvedValueOnce({
|
|
dropped_names: ["email_1"],
|
|
affected_rows: 1,
|
|
failures: [{ name: "missing_1", message: "index not found" }],
|
|
});
|
|
|
|
await feature.confirmDropAllMongoIndexes();
|
|
|
|
expect(mocks.loadIndexes).toHaveBeenCalledOnce();
|
|
expect(mocks.toast).toHaveBeenCalledWith('contextMenu.dropIndexesPartialFailure:{"success":1,"failed":1}', 5000);
|
|
});
|
|
|
|
it("executes Legacy delete operations and refreshes their MongoDB metadata", async () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
|
|
|
const databaseNode = mongoDatabaseNode();
|
|
const databaseFeature = runtime(databaseNode);
|
|
sidebarDangerTarget.value = databaseNode;
|
|
await databaseFeature.confirmDropMongoDatabase();
|
|
|
|
expect(mocks.mongoDropDatabase).toHaveBeenCalledWith("conn-1", "app");
|
|
expect(mocks.loadMongoDatabases).toHaveBeenCalledWith("conn-1");
|
|
|
|
const collectionNode = mongoCollectionNode();
|
|
const collectionFeature = runtime(collectionNode);
|
|
sidebarDangerTarget.value = collectionNode;
|
|
await collectionFeature.confirmDropMongoCollection();
|
|
|
|
expect(mocks.mongoDropCollection).toHaveBeenCalledWith("conn-1", "app", "users");
|
|
expect(mocks.loadMongoDatabases).toHaveBeenCalledWith("conn-1");
|
|
expect(mocks.loadMongoCollections).toHaveBeenCalledWith("conn-1", "app");
|
|
|
|
const indexNode = mongoIndexNode("email_1");
|
|
const indexFeature = runtime(indexNode);
|
|
sidebarDangerTarget.value = indexNode;
|
|
await indexFeature.confirmDropMongoIndex();
|
|
|
|
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", '"email_1"', true);
|
|
expect(mocks.loadIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, "conn-1:app:users:__indexes", undefined);
|
|
});
|
|
|
|
it("keeps a completed collection drop successful when metadata refresh fails", async () => {
|
|
const node = mongoCollectionNode();
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
showDropMongoCollectionConfirm.value = true;
|
|
mocks.loadMongoDatabases.mockRejectedValueOnce(new Error("metadata unavailable"));
|
|
|
|
await feature.confirmDropMongoCollection();
|
|
|
|
expect(mocks.mongoDropCollection).toHaveBeenCalledWith("conn-1", "app", "users");
|
|
expect(showDropMongoCollectionConfirm.value).toBe(false);
|
|
expect(mocks.removeTreeNode).toHaveBeenCalledWith(node.id);
|
|
expect(mocks.toast).toHaveBeenCalledWith(expect.stringContaining("contextMenu.dropCollectionSuccess"), 3000);
|
|
expect(mocks.toast).toHaveBeenCalledWith(expect.stringContaining("contextMenu.objectDropRefreshFailed"), 5000);
|
|
expect(mocks.toast).not.toHaveBeenCalledWith(expect.stringContaining("contextMenu.tableOperationFailed"), 5000);
|
|
});
|
|
|
|
it("does not send a default _id_ index deletion request", async () => {
|
|
const node = mongoIndexNode("_id_");
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
|
|
await feature.confirmDropMongoIndex();
|
|
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("also hides indexes marked primary when metadata has an unexpected name", () => {
|
|
const feature = runtime(mongoIndexNode("unexpected_primary_name", "collection", true));
|
|
|
|
expect(feature.canDropMongoIndex.value).toBe(false);
|
|
});
|
|
|
|
it("reports an index-list refresh problem without misreporting a completed create as failed", async () => {
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
mocks.loadIndexes.mockRejectedValue(new Error("metadata unavailable"));
|
|
|
|
feature.prepareCreateMongoIndexDialog();
|
|
mongoCreateIndexForm.value.fields[0]!.path = "email";
|
|
await feature.confirmCreateMongoIndex();
|
|
|
|
expect(mocks.mongoCreateIndex).toHaveBeenCalledOnce();
|
|
expect(mocks.toast).toHaveBeenCalledWith(expect.stringContaining("contextMenu.mongoIndexRefreshFailed"), 5000);
|
|
});
|
|
|
|
it("does not issue a create request when production confirmation is cancelled", async () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig(undefined, true));
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarFormTarget.value = node;
|
|
feature.prepareCreateMongoIndexDialog();
|
|
mongoCreateIndexForm.value.fields[0]!.path = "email";
|
|
const pending = feature.confirmCreateMongoIndex();
|
|
await Promise.resolve();
|
|
|
|
const { useProductionSafetyStore } = await import("@/stores/productionSafetyStore");
|
|
useProductionSafetyStore().cancel();
|
|
await pending;
|
|
|
|
expect(mocks.ensureConnected).not.toHaveBeenCalled();
|
|
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
|
expect(showCreateMongoIndexDialog.value).toBe(true);
|
|
});
|
|
|
|
it("does not clear indexes when production confirmation is cancelled", async () => {
|
|
mocks.getConfig.mockReturnValue(mongoConfig(undefined, true));
|
|
const node = mongoIndexesGroupNode();
|
|
const feature = runtime(node);
|
|
sidebarDangerTarget.value = node;
|
|
showDropAllMongoIndexesConfirm.value = true;
|
|
const pending = feature.confirmDropAllMongoIndexes();
|
|
await Promise.resolve();
|
|
|
|
const { useProductionSafetyStore } = await import("@/stores/productionSafetyStore");
|
|
useProductionSafetyStore().cancel();
|
|
await pending;
|
|
|
|
expect(mocks.ensureConnected).not.toHaveBeenCalled();
|
|
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
|
expect(showDropAllMongoIndexesConfirm.value).toBe(true);
|
|
});
|
|
});
|