diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts index 716d6f75399..1e614b709e2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts @@ -1,334 +1,104 @@ -import { allEndUsersCall } from "@/components/networking"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import React, { ReactNode } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Customer, CustomersResponse } from "./useCustomers"; -import { useCustomers } from "./useCustomers"; +import { useCustomers, type EndUser } from "./useCustomers"; -// Mock the networking function -vi.mock("@/components/networking", () => ({ - allEndUsersCall: vi.fn(), +const mockGet = vi.fn(); +vi.mock("@/lib/http/api", () => ({ + fetchClient: { GET: (...args: unknown[]) => mockGet(...args) }, })); -// Mock useAuthorized hook - we can override this in individual tests const mockUseAuthorized = vi.fn(); vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized(), })); -// Import actual roles instead of mocking them - -// Mock data -const mockCustomers: Customer[] = [ - { - user_id: "customer-1", - alias: "Test Customer 1", - spend: 150.5, - blocked: false, - allowed_model_region: "us-east-1", - default_model: "gpt-3.5-turbo", - budget_id: "budget-1", - litellm_budget_table: { - budget_id: "budget-1", - max_budget: 1000, - soft_budget: 800, - max_parallel_requests: 10, - tpm_limit: 1000, - rpm_limit: 100, - model_max_budget: { "gpt-4": 500 }, - budget_duration: "monthly", - budget_reset_at: "2024-02-01T00:00:00Z", - created_at: "2024-01-01T00:00:00Z", - created_by: "admin-1", - updated_at: "2024-01-01T00:00:00Z", - updated_by: "admin-1", - }, - }, - { - user_id: "customer-2", - alias: null, - spend: 0, - blocked: true, - allowed_model_region: null, - default_model: null, - budget_id: null, - litellm_budget_table: null, - }, +const mockCustomers: EndUser[] = [ + { user_id: "customer-1", alias: "Test Customer 1", spend: 150.5, blocked: false }, + { user_id: "customer-2", alias: null, spend: 0, blocked: true }, ]; -const mockCustomersResponse: CustomersResponse = mockCustomers; +const authorized = { + accessToken: "test-access-token", + userRole: "Admin", + userId: "test-user-id", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, +}; describe("useCustomers", () => { let queryClient: QueryClient; beforeEach(() => { - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - - // Reset all mocks + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); vi.clearAllMocks(); - - // Set default mock for useAuthorized (enabled state) - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userRole: "Admin", - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); + mockUseAuthorized.mockReturnValue(authorized); }); const wrapper = ({ children }: { children: ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); - it("should return customers data when query is successful", async () => { - // Mock successful API call - (allEndUsersCall as any).mockResolvedValue(mockCustomersResponse); + it("fetches /customer/list and returns the typed list on success", async () => { + mockGet.mockResolvedValue({ data: mockCustomers }); const { result } = renderHook(() => useCustomers(), { 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(mockCustomersResponse); - expect(result.current.error).toBeNull(); - expect(allEndUsersCall).toHaveBeenCalledWith("test-access-token"); - expect(allEndUsersCall).toHaveBeenCalledTimes(1); + expect(result.current.data).toEqual(mockCustomers); + expect(mockGet).toHaveBeenCalledWith("/customer/list"); + expect(mockGet).toHaveBeenCalledTimes(1); }); - it("should handle error when allEndUsersCall fails", async () => { - const errorMessage = "Failed to fetch customers"; - const testError = new Error(errorMessage); - - // Mock failed API call - (allEndUsersCall as any).mockRejectedValue(testError); + it("surfaces an error when the request rejects", async () => { + const testError = new Error("Failed to fetch customers"); + mockGet.mockRejectedValue(testError); const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).toHaveBeenCalledWith("test-access-token"); - expect(allEndUsersCall).toHaveBeenCalledTimes(1); }); - it("should not execute query when accessToken is missing", async () => { - // Mock missing accessToken - mockUseAuthorized.mockReturnValue({ - accessToken: null, - userRole: "Admin", - userId: "test-user-id", - token: null, - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); + it("falls back to an empty list when the response has no body", async () => { + mockGet.mockResolvedValue({ data: undefined }); const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).not.toHaveBeenCalled(); - }); - - it("should not execute query when userRole is not an admin role", async () => { - // Mock non-admin userRole - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userRole: "member", // Not in all_admin_roles - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).not.toHaveBeenCalled(); - }); - - it("should not execute query when userRole is null", async () => { - // Mock null userRole - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userRole: null, - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).not.toHaveBeenCalled(); - }); - - it("should not execute query when userRole is empty string", async () => { - // Mock empty string userRole - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userRole: "", - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).not.toHaveBeenCalled(); - }); - - it("should not execute query when both accessToken and userRole are missing", async () => { - // Mock both auth values missing - mockUseAuthorized.mockReturnValue({ - accessToken: null, - userRole: null, - userId: "test-user-id", - token: null, - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCustomers(), { 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(allEndUsersCall).not.toHaveBeenCalled(); - }); - - it("should execute query when accessToken is present and userRole is Admin", async () => { - // Mock successful API call - (allEndUsersCall as any).mockResolvedValue(mockCustomersResponse); - - // Ensure auth values are set (already done in beforeEach) - const { result } = renderHook(() => useCustomers(), { wrapper }); - - // Wait for query to execute await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(allEndUsersCall).toHaveBeenCalledWith("test-access-token"); - expect(allEndUsersCall).toHaveBeenCalledTimes(1); - }); - - it("should execute query when accessToken is present and userRole is proxy_admin", async () => { - // Mock successful API call - (allEndUsersCall as any).mockResolvedValue(mockCustomersResponse); - - // Mock proxy_admin role - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userRole: "proxy_admin", - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - // Wait for query to execute - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - expect(allEndUsersCall).toHaveBeenCalledWith("test-access-token"); - expect(allEndUsersCall).toHaveBeenCalledTimes(1); - }); - - it("should return empty customers array when API returns empty data", async () => { - // Mock API returning empty customers array - (allEndUsersCall as any).mockResolvedValue([]); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - // Wait for success - await waitFor(() => { - expect(result.current.isLoading).toBe(false); expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); - expect(allEndUsersCall).toHaveBeenCalledWith("test-access-token"); }); - it("should handle network timeout error", async () => { - const timeoutError = new Error("Network timeout"); - - // Mock network timeout - (allEndUsersCall as any).mockRejectedValue(timeoutError); + it("does not fetch when the access token is missing", () => { + mockUseAuthorized.mockReturnValue({ ...authorized, accessToken: null, token: null }); const { result } = renderHook(() => useCustomers(), { wrapper }); - // Wait for error - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); + expect(result.current.isFetched).toBe(false); + expect(mockGet).not.toHaveBeenCalled(); + }); - expect(result.current.error).toEqual(timeoutError); - expect(result.current.data).toBeUndefined(); + it("does not fetch when the user is not an admin", () => { + mockUseAuthorized.mockReturnValue({ ...authorized, userRole: "member" }); + + const { result } = renderHook(() => useCustomers(), { wrapper }); + + expect(result.current.isFetched).toBe(false); + expect(mockGet).not.toHaveBeenCalled(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts index d9f3e7cbb36..c31720bf199 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts @@ -1,42 +1,19 @@ -import { allEndUsersCall } from "@/components/networking"; import { useQuery } from "@tanstack/react-query"; import { createQueryKeys } from "../common/queryKeysFactory"; +import { fetchClient } from "@/lib/http/api"; import { all_admin_roles } from "@/utils/roles"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import type { components } from "@/lib/http/schema"; + +export type EndUser = components["schemas"]["LiteLLM_EndUserTable"]; + const customersKeys = createQueryKeys("customers"); -export interface Customer { - user_id: string; - alias?: string | null; - spend: number; - blocked: boolean; - allowed_model_region?: string | null; - default_model?: string | null; - budget_id?: string | null; - litellm_budget_table?: { - budget_id: string; - max_budget?: number | null; - soft_budget?: number | null; - max_parallel_requests?: number | null; - tpm_limit?: number | null; - rpm_limit?: number | null; - model_max_budget?: Record | null; - budget_duration?: string | null; - budget_reset_at?: string | null; - created_at: string; - created_by: string; - updated_at: string; - updated_by: string; - } | null; -} - -export type CustomersResponse = Customer[]; - export const useCustomers = () => { const { accessToken, userRole } = useAuthorized(); - return useQuery({ + return useQuery({ queryKey: customersKeys.list({}), - queryFn: async () => await allEndUsersCall(accessToken!), + queryFn: async () => (await fetchClient.GET("/customer/list")).data ?? [], enabled: Boolean(accessToken) && all_admin_roles.includes(userRole!), }); };