diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.test.tsx new file mode 100644 index 00000000000..4214d5fda7c --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.test.tsx @@ -0,0 +1,29 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder"; + +describe("HashicorpVaultEmptyPlaceholder", () => { + it("should render the empty state message and configure button", () => { + render(); + expect(screen.getByText("No Vault Configuration Found")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /configure vault/i })).toBeInTheDocument(); + }); + + it("should call onAdd when the configure button is clicked", async () => { + const onAdd = vi.fn(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: /configure vault/i })); + + expect(onAdd).toHaveBeenCalledOnce(); + }); + + it("should display the description text about Vault purpose", () => { + render(); + expect( + screen.getByText(/Configure Hashicorp Vault to securely manage provider API keys/), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.test.tsx new file mode 100644 index 00000000000..798572b2037 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.test.tsx @@ -0,0 +1,77 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import PageVisibilitySettings from "./PageVisibilitySettings"; + +vi.mock("@/components/page_utils", () => ({ + getAvailablePages: () => [ + { page: "usage", label: "Usage", description: "View usage stats", group: "Analytics" }, + { page: "models", label: "Models", description: "Manage models", group: "Analytics" }, + { page: "keys", label: "API Keys", description: "Manage API keys", group: "Access" }, + ], +})); + +describe("PageVisibilitySettings", () => { + it("should render the not-set tag when enabledPagesInternalUsers is null", () => { + render( + , + ); + expect(screen.getByText("Not set (all pages visible)")).toBeInTheDocument(); + }); + + it("should show the selected page count tag when pages are configured", () => { + render( + , + ); + expect(screen.getByText("2 pages selected")).toBeInTheDocument(); + }); + + it("should show singular 'page' when exactly one page is selected", () => { + render( + , + ); + expect(screen.getByText("1 page selected")).toBeInTheDocument(); + }); + + it("should call onUpdate with null when reset button is clicked", async () => { + const onUpdate = vi.fn(); + const user = userEvent.setup(); + render( + , + ); + + // Expand the collapse panel first to reveal the reset button + await user.click(screen.getByRole("button", { name: /configure page visibility/i })); + await user.click(await screen.findByRole("button", { name: /reset to default/i })); + + expect(onUpdate).toHaveBeenCalledWith({ enabled_ui_pages_internal_users: null }); + }); + + it("should display the property description when provided", () => { + render( + , + ); + expect(screen.getByText("Controls which pages are visible")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/ui/ui-loading-spinner.test.tsx b/ui/litellm-dashboard/src/components/ui/ui-loading-spinner.test.tsx new file mode 100644 index 00000000000..73b2de54c4c --- /dev/null +++ b/ui/litellm-dashboard/src/components/ui/ui-loading-spinner.test.tsx @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { UiLoadingSpinner } from "./ui-loading-spinner"; + +describe("UiLoadingSpinner", () => { + it("should render an SVG element", () => { + render(); + expect(screen.getByTestId("spinner")).toBeInTheDocument(); + }); + + it("should apply custom className alongside default classes", () => { + render(); + const svg = screen.getByTestId("spinner"); + expect(svg).toHaveClass("text-red-500"); + expect(svg).toHaveClass("animate-spin"); + }); + + it("should spread additional SVG props onto the element", () => { + render(); + expect(screen.getByLabelText("Loading")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/errorUtils.test.ts b/ui/litellm-dashboard/src/utils/errorUtils.test.ts new file mode 100644 index 00000000000..ccd65717f40 --- /dev/null +++ b/ui/litellm-dashboard/src/utils/errorUtils.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { extractErrorMessage } from "./errorUtils"; + +describe("extractErrorMessage", () => { + it("should return the message from an Error instance", () => { + expect(extractErrorMessage(new Error("Something broke"))).toBe("Something broke"); + }); + + it("should return detail when it is a string", () => { + expect(extractErrorMessage({ detail: "Not found" })).toBe("Not found"); + }); + + it("should join msg fields from a FastAPI 422 detail array", () => { + const err = { + detail: [ + { msg: "field required", loc: ["body", "name"], type: "value_error" }, + { msg: "invalid type", loc: ["body", "age"], type: "type_error" }, + ], + }; + expect(extractErrorMessage(err)).toBe("field required; invalid type"); + }); + + it("should extract error from nested detail object", () => { + expect(extractErrorMessage({ detail: { error: "bad request" } })).toBe("bad request"); + }); + + it("should fall back to message property on plain objects", () => { + expect(extractErrorMessage({ message: "fallback msg" })).toBe("fallback msg"); + }); + + it("should JSON.stringify unknown object shapes", () => { + expect(extractErrorMessage({ foo: "bar" })).toBe('{"foo":"bar"}'); + }); + + it("should stringify primitive non-object values", () => { + expect(extractErrorMessage(42)).toBe("42"); + expect(extractErrorMessage(null)).toBe("null"); + expect(extractErrorMessage(undefined)).toBe("undefined"); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.test.ts b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.test.ts new file mode 100644 index 00000000000..0ae8ae70bec --- /dev/null +++ b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { classifyToolOp, groupToolsByCrud } from "./mcpToolCrudClassification"; + +describe("classifyToolOp", () => { + it("should classify read operations by name", () => { + expect(classifyToolOp("get-users")).toBe("read"); + expect(classifyToolOp("list-items")).toBe("read"); + expect(classifyToolOp("search documents")).toBe("read"); + }); + + it("should classify delete operations by name", () => { + expect(classifyToolOp("delete-user")).toBe("delete"); + expect(classifyToolOp("remove-item")).toBe("delete"); + expect(classifyToolOp("purge-cache")).toBe("delete"); + }); + + it("should classify create operations by name", () => { + expect(classifyToolOp("create-user")).toBe("create"); + expect(classifyToolOp("add-item")).toBe("create"); + expect(classifyToolOp("upload-file")).toBe("create"); + }); + + it("should classify update operations by name", () => { + expect(classifyToolOp("update-settings")).toBe("update"); + expect(classifyToolOp("edit-profile")).toBe("update"); + expect(classifyToolOp("rename-file")).toBe("update"); + }); + + it("should prioritize read over delete for names like get-removed-entries", () => { + expect(classifyToolOp("get-removed-entries")).toBe("read"); + expect(classifyToolOp("list-deleted-items")).toBe("read"); + }); + + it("should fall back to description when name is unrecognised", () => { + expect(classifyToolOp("mytool", "This will delete the record")).toBe("delete"); + expect(classifyToolOp("mytool", "fetch data from the API")).toBe("read"); + }); + + it("should return unknown when neither name nor description match", () => { + expect(classifyToolOp("my_tool")).toBe("unknown"); + expect(classifyToolOp("my_tool", "does something")).toBe("unknown"); + }); +}); + +describe("groupToolsByCrud", () => { + it("should group tools into their CRUD categories", () => { + const tools = [ + { name: "get-user", description: "" }, + { name: "create-item", description: "" }, + { name: "delete-record", description: "" }, + { name: "update-settings", description: "" }, + { name: "mysteryop", description: "" }, + ]; + + const groups = groupToolsByCrud(tools); + + expect(groups.read).toHaveLength(1); + expect(groups.create).toHaveLength(1); + expect(groups.delete).toHaveLength(1); + expect(groups.update).toHaveLength(1); + expect(groups.unknown).toHaveLength(1); + }); +});