From 81c78931d8b5b8790412fe7d167e58fd588b968a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 31 Dec 2025 12:24:01 -0800 Subject: [PATCH] Testing coverage with v8 --- .gitignore | 3 +- ui/litellm-dashboard/package.json | 1 + .../hooks/guardrails/useGuardrails.test.ts | 284 +++++++++++++++++ .../organizations/useOrganizations.test.ts | 293 +++++++++++++++++ .../hooks/providers/useProviderFields.test.ts | 193 ++++++++++++ .../(dashboard)/hooks/tags/useTags.test.ts | 294 ++++++++++++++++++ .../hooks/uiConfig/useUIConfig.test.ts | 169 ++++++++++ ui/litellm-dashboard/vitest.config.ts | 25 +- 8 files changed, 1259 insertions(+), 3 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/providers/useProviderFields.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.test.ts diff --git a/.gitignore b/.gitignore index 9a4f666bf47..fafacd874a0 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ scripts/test_vertex_ai_search.py LAZY_LOADING_IMPROVEMENTS.md **/test-results **/playwright-report -**/*.storageState.json \ No newline at end of file +**/*.storageState.json +**/coverage \ No newline at end of file diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index a018a4e7a58..0ecba62140f 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -9,6 +9,7 @@ "lint": "next lint", "test": "vitest", "test:watch": "vitest -w", + "test:coverage": "vitest run --coverage", "format": "prettier --write .", "format:check": "prettier --check .", "e2e": "playwright test --config e2e_tests/playwright.config.ts", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts new file mode 100644 index 00000000000..48344a2167f --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts @@ -0,0 +1,284 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useGuardrails } from "./useGuardrails"; +import { getGuardrailsList } from "@/components/networking"; + +// Mock the networking function +vi.mock("@/components/networking", () => ({ + getGuardrailsList: vi.fn(), +})); + +// Mock the queryKeysFactory - we'll mock the specific return value +vi.mock("../common/queryKeysFactory", () => ({ + createQueryKeys: vi.fn((resource: string) => ({ + all: [resource], + lists: () => [resource, "list"], + list: (params?: any) => [resource, "list", { params }], + details: () => [resource, "detail"], + detail: (uid: string) => [resource, "detail", uid], + })), +})); + +// Mock useAuthorized hook - we can override this in individual tests +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +// Mock data +const mockGuardrailsResponse = { + guardrails: [ + { guardrail_name: "content-safety" }, + { guardrail_name: "toxicity-filter" }, + { guardrail_name: "pii-detection" }, + ], +}; + +const expectedGuardrailNames = ["content-safety", "toxicity-filter", "pii-detection"]; + +describe("useGuardrails", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + // Reset all mocks + vi.clearAllMocks(); + + // Set default mock for useAuthorized (enabled state) + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("should return guardrail names when query is successful", async () => { + // Mock successful API call + (getGuardrailsList as any).mockResolvedValue(mockGuardrailsResponse); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(expectedGuardrailNames); + expect(result.current.error).toBeNull(); + expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token"); + expect(getGuardrailsList).toHaveBeenCalledTimes(1); + }); + + it("should handle error when getGuardrailsList fails", async () => { + const errorMessage = "Failed to fetch guardrails"; + const testError = new Error(errorMessage); + + // Mock failed API call + (getGuardrailsList as any).mockRejectedValue(testError); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + + // Wait for error + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(testError); + expect(result.current.data).toBeUndefined(); + expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token"); + expect(getGuardrailsList).toHaveBeenCalledTimes(1); + }); + + it("should not execute query when accessToken is missing", async () => { + // Mock missing accessToken + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: "test-user-id", + userRole: "Admin", + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(getGuardrailsList).not.toHaveBeenCalled(); + }); + + it("should not execute query when userId is missing", async () => { + // Mock missing userId + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: null, + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(getGuardrailsList).not.toHaveBeenCalled(); + }); + + it("should not execute query when userRole is missing", async () => { + // Mock missing userRole + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: null, + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(getGuardrailsList).not.toHaveBeenCalled(); + }); + + it("should not execute query when all auth values are missing", async () => { + // Mock all auth values missing + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: null, + userRole: null, + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(getGuardrailsList).not.toHaveBeenCalled(); + }); + + it("should execute query when all auth values are present", async () => { + // Mock successful API call + (getGuardrailsList as any).mockResolvedValue(mockGuardrailsResponse); + + // Ensure all auth values are present (already set in beforeEach) + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Wait for query to execute + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token"); + expect(getGuardrailsList).toHaveBeenCalledTimes(1); + }); + + it("should return empty array when API returns empty guardrails", async () => { + // Mock API returning empty guardrails array + (getGuardrailsList as any).mockResolvedValue({ guardrails: [] }); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual([]); + expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token"); + }); + + it("should handle network timeout error", async () => { + const timeoutError = new Error("Network timeout"); + + // Mock network timeout + (getGuardrailsList as any).mockRejectedValue(timeoutError); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(timeoutError); + expect(result.current.data).toBeUndefined(); + }); + + it("should correctly transform guardrail objects to names array", async () => { + const customGuardrailsResponse = { + guardrails: [{ guardrail_name: "custom-guardrail-1" }, { guardrail_name: "custom-guardrail-2" }], + }; + const expectedNames = ["custom-guardrail-1", "custom-guardrail-2"]; + + // Mock API call with custom data + (getGuardrailsList as any).mockResolvedValue(customGuardrailsResponse); + + const { result } = renderHook(() => useGuardrails(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(expectedNames); + expect(result.current.data).toHaveLength(2); + expect(result.current.data).toContain("custom-guardrail-1"); + expect(result.current.data).toContain("custom-guardrail-2"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.test.ts new file mode 100644 index 00000000000..2f13a68bab4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.test.ts @@ -0,0 +1,293 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useOrganizations } from "./useOrganizations"; +import { organizationListCall } from "@/components/networking"; +import type { Organization } from "@/components/networking"; + +// Mock the networking function +vi.mock("@/components/networking", () => ({ + organizationListCall: vi.fn(), +})); + +// Mock the queryKeysFactory - we'll mock the specific return value +vi.mock("../common/queryKeysFactory", () => ({ + createQueryKeys: vi.fn((resource: string) => ({ + all: [resource], + lists: () => [resource, "list"], + list: (params?: any) => [resource, "list", { params }], + details: () => [resource, "detail"], + detail: (uid: string) => [resource, "detail", uid], + })), +})); + +// Mock useAuthorized hook - we can override this in individual tests +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +// Mock data +const mockOrganizations: Organization[] = [ + { + organization_id: "org-1", + organization_alias: "Test Organization 1", + budget_id: "budget-1", + metadata: {}, + models: ["gpt-3.5-turbo", "gpt-4"], + spend: 100.5, + model_spend: { "gpt-3.5-turbo": 50.25, "gpt-4": 50.25 }, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + litellm_budget_table: null, + teams: null, + users: null, + members: [ + { user_id: "user-1", user_role: "admin" }, + { user_id: "user-2", user_role: "member" }, + ], + }, + { + organization_id: "org-2", + organization_alias: "Test Organization 2", + budget_id: "budget-2", + metadata: {}, + models: ["claude-3"], + spend: 250.75, + model_spend: { "claude-3": 250.75 }, + created_at: "2024-01-01T00:00:00Z", + created_by: "user-3", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-3", + litellm_budget_table: null, + teams: null, + users: null, + members: [{ user_id: "user-3", user_role: "admin" }], + }, +]; + +describe("useOrganizations", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + // Reset all mocks + vi.clearAllMocks(); + + // Set default mock for useAuthorized (enabled state) + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("should return organizations data when query is successful", async () => { + // Mock successful API call + (organizationListCall as any).mockResolvedValue(mockOrganizations); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(mockOrganizations); + expect(result.current.error).toBeNull(); + expect(organizationListCall).toHaveBeenCalledWith("test-access-token"); + expect(organizationListCall).toHaveBeenCalledTimes(1); + }); + + it("should handle error when organizationListCall fails", async () => { + const errorMessage = "Failed to fetch organizations"; + const testError = new Error(errorMessage); + + // Mock failed API call + (organizationListCall as any).mockRejectedValue(testError); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + + // Wait for error + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(testError); + expect(result.current.data).toBeUndefined(); + expect(organizationListCall).toHaveBeenCalledWith("test-access-token"); + expect(organizationListCall).toHaveBeenCalledTimes(1); + }); + + it("should not execute query when accessToken is missing", async () => { + // Mock missing accessToken + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: "test-user-id", + userRole: "Admin", + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(organizationListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when userId is missing", async () => { + // Mock missing userId + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: null, + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(organizationListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when userRole is missing", async () => { + // Mock missing userRole + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: null, + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(organizationListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when all auth values are missing", async () => { + // Mock all auth values missing + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: null, + userRole: null, + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(organizationListCall).not.toHaveBeenCalled(); + }); + + it("should execute query when all auth values are present", async () => { + // Mock successful API call + (organizationListCall as any).mockResolvedValue(mockOrganizations); + + // Ensure all auth values are present (already set in beforeEach) + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Wait for query to execute + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(organizationListCall).toHaveBeenCalledWith("test-access-token"); + expect(organizationListCall).toHaveBeenCalledTimes(1); + }); + + it("should return empty array when API returns empty data", async () => { + // Mock API returning empty array + (organizationListCall as any).mockResolvedValue([]); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual([]); + expect(organizationListCall).toHaveBeenCalledWith("test-access-token"); + }); + + it("should handle network timeout error", async () => { + const timeoutError = new Error("Network timeout"); + + // Mock network timeout + (organizationListCall as any).mockRejectedValue(timeoutError); + + const { result } = renderHook(() => useOrganizations(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(timeoutError); + expect(result.current.data).toBeUndefined(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/providers/useProviderFields.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/providers/useProviderFields.test.ts new file mode 100644 index 00000000000..952b52bb409 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/providers/useProviderFields.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useProviderFields } from "./useProviderFields"; +import { getProviderCreateMetadata } from "@/components/networking"; +import type { ProviderCreateInfo } from "@/components/networking"; + +// Mock the networking function +vi.mock("@/components/networking", () => ({ + getProviderCreateMetadata: vi.fn(), +})); + +// Mock the queryKeysFactory - we'll mock the specific return value +vi.mock("../common/queryKeysFactory", () => ({ + createQueryKeys: vi.fn((resource: string) => ({ + all: [resource], + lists: () => [resource, "list"], + list: (params?: any) => [resource, "list", { params }], + details: () => [resource, "detail"], + detail: (uid: string) => [resource, "detail", uid], + })), +})); + +// Mock data +const mockProviderFields: ProviderCreateInfo[] = [ + { + provider: "OpenAI", + provider_display_name: "OpenAI", + litellm_provider: "openai", + default_model_placeholder: "gpt-3.5-turbo", + credential_fields: [], + }, + { + provider: "Anthropic", + provider_display_name: "Anthropic", + litellm_provider: "anthropic", + default_model_placeholder: "claude-3-sonnet-20240229", + credential_fields: [], + }, + { + provider: "Azure", + provider_display_name: "Azure OpenAI", + litellm_provider: "azure", + default_model_placeholder: "gpt-35-turbo", + credential_fields: [], + }, +]; + +describe("useProviderFields", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + // Reset all mocks + vi.clearAllMocks(); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("should return provider fields data when query is successful", async () => { + // Mock successful API call + (getProviderCreateMetadata as any).mockResolvedValue(mockProviderFields); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(mockProviderFields); + expect(result.current.error).toBeNull(); + expect(getProviderCreateMetadata).toHaveBeenCalledTimes(1); + }); + + it("should handle error when getProviderCreateMetadata fails", async () => { + const errorMessage = "Failed to fetch provider fields"; + const testError = new Error(errorMessage); + + // Mock failed API call + (getProviderCreateMetadata as any).mockRejectedValue(testError); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + + // Wait for error + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(testError); + expect(result.current.data).toBeUndefined(); + expect(getProviderCreateMetadata).toHaveBeenCalledTimes(1); + }); + + it("should return empty array when API returns empty data", async () => { + // Mock API returning empty array + (getProviderCreateMetadata as any).mockResolvedValue([]); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual([]); + expect(getProviderCreateMetadata).toHaveBeenCalledTimes(1); + }); + + it("should handle network timeout error", async () => { + const timeoutError = new Error("Network timeout"); + + // Mock network timeout + (getProviderCreateMetadata as any).mockRejectedValue(timeoutError); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(timeoutError); + expect(result.current.data).toBeUndefined(); + }); + + it("should have correct query configuration", async () => { + // Mock successful API call + (getProviderCreateMetadata as any).mockResolvedValue(mockProviderFields); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Wait for query to complete + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Verify the query was called + expect(getProviderCreateMetadata).toHaveBeenCalledTimes(1); + + // The hook should have the expected properties from useQuery + expect(result.current).toHaveProperty("data"); + expect(result.current).toHaveProperty("isLoading"); + expect(result.current).toHaveProperty("isError"); + expect(result.current).toHaveProperty("isSuccess"); + expect(result.current).toHaveProperty("error"); + }); + + it("should return provider fields with populated credential fields", async () => { + const mockFieldsWithCredentials: ProviderCreateInfo[] = [ + { + provider: "TestProvider", + provider_display_name: "Test Provider", + litellm_provider: "test", + default_model_placeholder: "test-model", + credential_fields: [], // Keeping empty as per existing test patterns + }, + ]; + + // Mock successful API call with provider that has credential fields + (getProviderCreateMetadata as any).mockResolvedValue(mockFieldsWithCredentials); + + const { result } = renderHook(() => useProviderFields(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(mockFieldsWithCredentials); + expect(result.current.data?.[0].provider).toBe("TestProvider"); + expect(result.current.data?.[0].litellm_provider).toBe("test"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.test.ts new file mode 100644 index 00000000000..9ae96623597 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useTags } from "./useTags"; +import { tagListCall } from "@/components/networking"; +import type { TagListResponse } from "@/components/tag_management/types"; + +// Mock the networking function +vi.mock("@/components/networking", () => ({ + tagListCall: vi.fn(), +})); + +// Mock the queryKeysFactory - we'll mock the specific return value +vi.mock("../common/queryKeysFactory", () => ({ + createQueryKeys: vi.fn((resource: string) => ({ + all: [resource], + lists: () => [resource, "list"], + list: (params?: any) => [resource, "list", { params }], + details: () => [resource, "detail"], + detail: (uid: string) => [resource, "detail", uid], + })), +})); + +// Mock useAuthorized hook - we can override this in individual tests +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorized(), +})); + +// Mock data +const mockTags: TagListResponse = { + "tag-1": { + name: "tag-1", + description: "Test tag 1 description", + models: ["gpt-3.5-turbo", "gpt-4"], + model_info: { "gpt-3.5-turbo": "GPT-3.5 Turbo", "gpt-4": "GPT-4" }, + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_by: "user-1", + litellm_budget_table: { + max_budget: 1000, + soft_budget: 800, + tpm_limit: 100000, + rpm_limit: 1000, + max_parallel_requests: 10, + budget_duration: "monthly", + model_max_budget: { "gpt-3.5-turbo": 500, "gpt-4": 500 }, + }, + }, + "tag-2": { + name: "tag-2", + description: "Test tag 2 description", + models: ["claude-3"], + model_info: { "claude-3": "Claude 3" }, + created_at: "2024-01-02T00:00:00Z", + updated_at: "2024-01-02T00:00:00Z", + created_by: "user-2", + updated_by: "user-2", + litellm_budget_table: { + max_budget: 2000, + soft_budget: 1500, + tpm_limit: 200000, + rpm_limit: 2000, + max_parallel_requests: 20, + budget_duration: "monthly", + model_max_budget: { "claude-3": 2000 }, + }, + }, +}; + +describe("useTags", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + // Reset all mocks + vi.clearAllMocks(); + + // Set default mock for useAuthorized (enabled state) + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("should return tags data when query is successful", async () => { + // Mock successful API call + (tagListCall as any).mockResolvedValue(mockTags); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(mockTags); + expect(result.current.error).toBeNull(); + expect(tagListCall).toHaveBeenCalledWith("test-access-token"); + expect(tagListCall).toHaveBeenCalledTimes(1); + }); + + it("should handle error when tagListCall fails", async () => { + const errorMessage = "Failed to fetch tags"; + const testError = new Error(errorMessage); + + // Mock failed API call + (tagListCall as any).mockRejectedValue(testError); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + + // Wait for error + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(testError); + expect(result.current.data).toBeUndefined(); + expect(tagListCall).toHaveBeenCalledWith("test-access-token"); + expect(tagListCall).toHaveBeenCalledTimes(1); + }); + + it("should not execute query when accessToken is missing", async () => { + // Mock missing accessToken + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: "test-user-id", + userRole: "Admin", + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(tagListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when userId is missing", async () => { + // Mock missing userId + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: null, + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(tagListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when userRole is missing", async () => { + // Mock missing userRole + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: null, + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(tagListCall).not.toHaveBeenCalled(); + }); + + it("should not execute query when all auth values are missing", async () => { + // Mock all auth values missing + mockUseAuthorized.mockReturnValue({ + accessToken: null, + userId: null, + userRole: null, + token: null, + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Query should not execute + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); + + // API should not be called + expect(tagListCall).not.toHaveBeenCalled(); + }); + + it("should execute query when all auth values are present", async () => { + // Mock successful API call + (tagListCall as any).mockResolvedValue(mockTags); + + // Ensure all auth values are present (already set in beforeEach) + const { result } = renderHook(() => useTags(), { wrapper }); + + // Wait for query to execute + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(tagListCall).toHaveBeenCalledWith("test-access-token"); + expect(tagListCall).toHaveBeenCalledTimes(1); + }); + + it("should return empty object when API returns empty data", async () => { + // Mock API returning empty object + (tagListCall as any).mockResolvedValue({}); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual({}); + expect(tagListCall).toHaveBeenCalledWith("test-access-token"); + }); + + it("should handle network timeout error", async () => { + const timeoutError = new Error("Network timeout"); + + // Mock network timeout + (tagListCall as any).mockRejectedValue(timeoutError); + + const { result } = renderHook(() => useTags(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(timeoutError); + expect(result.current.data).toBeUndefined(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.test.ts new file mode 100644 index 00000000000..6429aeafb5a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useUIConfig } from "./useUIConfig"; +import { getUiConfig, LiteLLMWellKnownUiConfig } from "@/components/networking"; + +// Mock the networking function +vi.mock("@/components/networking", () => ({ + getUiConfig: vi.fn(), +})); + +// Mock the queryKeysFactory - we'll mock the specific return value +vi.mock("../common/queryKeysFactory", () => ({ + createQueryKeys: vi.fn((resource: string) => ({ + all: [resource], + lists: () => [resource, "list"], + list: (params?: any) => [resource, "list", { params }], + details: () => [resource, "detail"], + detail: (uid: string) => [resource, "detail", uid], + })), +})); + +// Mock data +const mockUIConfig: LiteLLMWellKnownUiConfig = { + server_root_path: "/api", + proxy_base_url: "https://proxy.example.com", + auto_redirect_to_sso: true, + admin_ui_disabled: false, +}; + +describe("useUIConfig", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + // Reset all mocks + vi.clearAllMocks(); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("should return UI config data when query is successful", async () => { + // Mock successful API call + (getUiConfig as any).mockResolvedValue(mockUIConfig); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(mockUIConfig); + expect(result.current.error).toBeNull(); + expect(getUiConfig).toHaveBeenCalledWith(); + expect(getUiConfig).toHaveBeenCalledTimes(1); + }); + + it("should handle error when getUiConfig fails", async () => { + const errorMessage = "Failed to fetch UI config"; + const testError = new Error(errorMessage); + + // Mock failed API call + (getUiConfig as any).mockRejectedValue(testError); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Initially loading + expect(result.current.isLoading).toBe(true); + + // Wait for error + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(testError); + expect(result.current.data).toBeUndefined(); + expect(getUiConfig).toHaveBeenCalledWith(); + expect(getUiConfig).toHaveBeenCalledTimes(1); + }); + + it("should return different UI config data correctly", async () => { + const alternativeUIConfig: LiteLLMWellKnownUiConfig = { + server_root_path: "/v1", + proxy_base_url: null, + auto_redirect_to_sso: false, + admin_ui_disabled: true, + }; + + // Mock successful API call with different data + (getUiConfig as any).mockResolvedValue(alternativeUIConfig); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Wait for success + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual(alternativeUIConfig); + expect(result.current.error).toBeNull(); + }); + + it("should handle network timeout error", async () => { + const timeoutError = new Error("Network timeout"); + + // Mock network timeout + (getUiConfig as any).mockRejectedValue(timeoutError); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(timeoutError); + expect(result.current.data).toBeUndefined(); + }); + + it("should handle malformed response error", async () => { + const malformedError = new Error("Invalid JSON response"); + + // Mock malformed response + (getUiConfig as any).mockRejectedValue(malformedError); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Wait for error + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expect(result.current.error).toEqual(malformedError); + expect(result.current.data).toBeUndefined(); + }); + + it("should use correct query key structure", async () => { + // Mock successful API call + (getUiConfig as any).mockResolvedValue(mockUIConfig); + + const { result } = renderHook(() => useUIConfig(), { wrapper }); + + // Wait for query to execute + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + // The query key should be generated by createQueryKeys("uiConfig").list({}) + // Based on our mock, this should be ["uiConfig", "list", {}] + expect(getUiConfig).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/litellm-dashboard/vitest.config.ts b/ui/litellm-dashboard/vitest.config.ts index 51e3df7a280..108f0715704 100644 --- a/ui/litellm-dashboard/vitest.config.ts +++ b/ui/litellm-dashboard/vitest.config.ts @@ -7,8 +7,29 @@ export default defineConfig({ setupFiles: ["tests/setupTests.ts"], globals: true, css: true, // lets you import CSS/modules without extra mocks - coverage: { reporter: ["text", "lcov"] }, - exclude: ["e2e_tests/**,", "node_modules/**"], + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "**/*.d.ts", + "**/*.test.*", + "**/*.spec.*", + + "tests/**", + "e2e_tests/**", + + "node_modules/**", + ".next/**", + "out/**", + + "**/*.config.*", + "postcss.config.*", + "tailwind.config.*", + "next.config.*", + ], + }, + exclude: ["e2e_tests/**", "node_modules/**"], include: ["src/**/*.test.ts", "src/**/*.test.tsx", "tests/**/*.test.ts", "tests/**/*.test.tsx"], }, resolve: {