diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx index e1b3b358300..3c5101fc2dc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.test.tsx @@ -131,7 +131,7 @@ describe("ModelsAndEndpointsView", () => { , ); expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument(); - }, 15000); + }); it("should show Missing provider banner by default", async () => { localStorageMock.clear(); @@ -149,7 +149,7 @@ describe("ModelsAndEndpointsView", () => { , ); expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument(); - }, 15000); + }); it("should hide Missing provider banner when dismiss button is clicked and persist to localStorage", async () => { localStorageMock.clear(); @@ -180,7 +180,7 @@ describe("ModelsAndEndpointsView", () => { // LocalStorage should be updated expect(localStorageMock.getItem("hideMissingProviderBanner")).toBe("true"); - }, 15000); + }); it("should show compact Request Provider button when banner is dismissed", async () => { // Set localStorage to hide banner @@ -209,7 +209,7 @@ describe("ModelsAndEndpointsView", () => { const requestProviderLinks = document.querySelectorAll('a[href="https://models.litellm.ai/?request=true"]'); // There should be a compact button when banner is hidden expect(requestProviderLinks.length).toBeGreaterThan(0); - }, 15000); + }); it("should pass model IDs (not model names) to HealthCheckComponent as all_models_on_proxy", async () => { mockHealthCheckComponent.mockClear(); diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx index 9a4659da9d3..03d982ca7c2 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx @@ -51,7 +51,7 @@ function renderWithProviders(ui: React.ReactElement) { return render({ui}); } -describe("CreateUserButton", { timeout: 20000 }, () => { +describe("CreateUserButton", () => { beforeEach(() => { vi.clearAllMocks(); mockGetProxyUISettings.mockResolvedValue({ @@ -62,288 +62,296 @@ describe("CreateUserButton", { timeout: 20000 }, () => { }); }); - it("should render the create user form when embedded", () => { - renderWithProviders( - , - ); - expect(screen.getByRole("button", { name: /create user/i })).toBeInTheDocument(); - }); + describe("rendering and visibility", () => { + it("should render the create user form when embedded", () => { + renderWithProviders( + , + ); + expect(screen.getByRole("button", { name: /create user/i })).toBeInTheDocument(); + }); - it("should render the invite user button when not embedded", async () => { - renderWithProviders(); - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + it("should render the invite user button when not embedded", async () => { + renderWithProviders(); + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + }); + + it("should open the invite modal when invite user button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + expect(dialog).toBeInTheDocument(); + expect(within(dialog).getByRole("button", { name: /invite user/i })).toBeInTheDocument(); + }); + + it("should display email invitations info message in embedded mode", () => { + renderWithProviders(); + expect(screen.getByText("Email invitations")).toBeInTheDocument(); + }); + + it("should display user role options when possibleUIRoles is provided", async () => { + const possibleUIRoles = { + proxy_admin: { ui_label: "Admin", description: "Full access" }, + proxy_user: { ui_label: "User", description: "Limited access" }, + }; + renderWithProviders( + , + ); + await userEvent.click(screen.getByRole("combobox", { name: /user role/i })); + expect(screen.getByText("Admin")).toBeInTheDocument(); + expect(screen.getByText("User")).toBeInTheDocument(); + }); + + it("should close modal when cancel is clicked in standalone mode", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + expect(screen.getByRole("dialog", { name: /invite user/i })).toBeInTheDocument(); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.click(within(dialog).getByRole("button", { name: /close/i })); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); }); - it("should open the invite modal when invite user button is clicked", async () => { - const user = userEvent.setup(); - renderWithProviders(); - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + describe("embedded mode submission", () => { + it("should call userCreateCall when form is submitted in embedded mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-123" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-1", + user_id: "new-user-123", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await user.type(screen.getByLabelText(/user email/i), "test@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("button", { name: /create user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + user_email: "test@example.com", + user_role: "proxy_user", + })); + }); }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - expect(dialog).toBeInTheDocument(); - expect(within(dialog).getByRole("button", { name: /invite user/i })).toBeInTheDocument(); - }); - it("should display email invitations info message in embedded mode", () => { - renderWithProviders(); - expect(screen.getByText("Email invitations")).toBeInTheDocument(); - }); + it("should call onUserCreated callback when user is created in embedded mode", async () => { + const user = userEvent.setup(); + const onUserCreated = vi.fn(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-456" } }); - it("should display user role options when possibleUIRoles is provided", async () => { - const possibleUIRoles = { - proxy_admin: { ui_label: "Admin", description: "Full access" }, - proxy_user: { ui_label: "User", description: "Limited access" }, - }; - renderWithProviders( - , - ); - await userEvent.click(screen.getByRole("combobox", { name: /user role/i })); - expect(screen.getByText("Admin")).toBeInTheDocument(); - expect(screen.getByText("User")).toBeInTheDocument(); - }); + renderWithProviders( + , + ); - it("should call userCreateCall when form is submitted in embedded mode", async () => { - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-123" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-1", - user_id: "new-user-123", - has_user_setup_sso: false, - } as any); + await user.type(screen.getByLabelText(/user email/i), "embedded@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("button", { name: /create user/i })); - renderWithProviders( - , - ); + await waitFor(() => { + expect(onUserCreated).toHaveBeenCalledWith("new-user-456"); + }); + }); - await user.type(screen.getByLabelText(/user email/i), "test@example.com"); - await user.click(screen.getByRole("combobox", { name: /user role/i })); - await user.click(screen.getByText("User")); - await user.click(screen.getByRole("button", { name: /create user/i })); + it("should show error notification when user creation fails", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockRejectedValue({ response: { data: { detail: "Email already exists" } } }); - await waitFor(() => { - expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ - user_email: "test@example.com", - user_role: "proxy_user", - })); + renderWithProviders( + , + ); + + await user.type(screen.getByLabelText(/user email/i), "duplicate@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("button", { name: /create user/i })); + + await waitFor(() => { + expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Email already exists"); + }); + }); + + it("should show info notification when making API call", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-3", + user_id: "new-user", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await user.type(screen.getByLabelText(/user email/i), "info@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("button", { name: /create user/i })); + + await waitFor(() => { + expect(mockNotificationsManager.info).toHaveBeenCalledWith("Making API Call"); + }); }); }); - it("should call onUserCreated callback when user is created in embedded mode", async () => { - const user = userEvent.setup(); - const onUserCreated = vi.fn(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-456" } }); + describe("standalone mode submission", () => { + it("should show success notification when user is created successfully in standalone mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-789" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-2", + user_id: "new-user-789", + has_user_setup_sso: false, + } as any); - renderWithProviders( - , - ); + renderWithProviders( + , + ); - await user.type(screen.getByLabelText(/user email/i), "embedded@example.com"); - await user.click(screen.getByRole("combobox", { name: /user role/i })); - await user.click(screen.getByText("User")); - await user.click(screen.getByRole("button", { name: /create user/i })); + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - await waitFor(() => { - expect(onUserCreated).toHaveBeenCalledWith("new-user-456"); + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "standalone@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockNotificationsManager.success).toHaveBeenCalledWith("API user Created"); + }); + }); + + it("should show onboarding modal when user is created and SSO is disabled", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "sso-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-sso", + user_id: "sso-user", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "sso@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockInvitationCreateCall).toHaveBeenCalledWith("token", "sso-user"); + }); + await waitFor(() => { + expect(mockNotificationsManager.success).toHaveBeenCalledWith("API user Created"); + }); }); }); - it("should show success notification when user is created successfully in standalone mode", async () => { - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user-789" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-2", - user_id: "new-user-789", - has_user_setup_sso: false, - } as any); + describe("organizations", () => { + it("should send organizations list in POST body when organizations are selected", async () => { + const { useOrganizations } = await import("@/app/(dashboard)/hooks/organizations/useOrganizations"); + vi.mocked(useOrganizations).mockReturnValue({ + data: [{ organization_id: "org-1", organization_alias: "My Org" }], + isLoading: false, + } as any); - renderWithProviders( - , - ); + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "org-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-org", + user_id: "org-user", + has_user_setup_sso: false, + } as any); - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "org@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + + // Select org from the dropdown + const orgSelect = within(dialog).getByRole("combobox", { name: /organization/i }); + await user.click(orgSelect); + await user.click(screen.getByText("My Org (org-1)")); + + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + organizations: ["org-1"], + })); + }); }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - await user.type(within(dialog).getByLabelText(/user email/i), "standalone@example.com"); - await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); - await user.click(screen.getByText("User")); - await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + it("should not call organizationMemberAddCall after user creation", async () => { + const { useOrganizations } = await import("@/app/(dashboard)/hooks/organizations/useOrganizations"); + vi.mocked(useOrganizations).mockReturnValue({ + data: [{ organization_id: "org-1", organization_alias: "My Org" }], + isLoading: false, + } as any); - await waitFor(() => { - expect(mockNotificationsManager.success).toHaveBeenCalledWith("API user Created"); + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "no-member-add-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-nma", + user_id: "no-member-add-user", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "nomemberadd@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalled(); + }); + expect(mockOrganizationMemberAddCall).not.toHaveBeenCalled(); }); }); - - it("should show error notification when user creation fails", async () => { - const user = userEvent.setup(); - mockUserCreateCall.mockRejectedValue({ response: { data: { detail: "Email already exists" } } }); - - renderWithProviders( - , - ); - - await user.type(screen.getByLabelText(/user email/i), "duplicate@example.com"); - await user.click(screen.getByRole("combobox", { name: /user role/i })); - await user.click(screen.getByText("User")); - await user.click(screen.getByRole("button", { name: /create user/i })); - - await waitFor(() => { - expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Email already exists"); - }); - }); - - it("should show info notification when making API call", async () => { - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "new-user" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-3", - user_id: "new-user", - has_user_setup_sso: false, - } as any); - - renderWithProviders( - , - ); - - await user.type(screen.getByLabelText(/user email/i), "info@example.com"); - await user.click(screen.getByRole("combobox", { name: /user role/i })); - await user.click(screen.getByText("User")); - await user.click(screen.getByRole("button", { name: /create user/i })); - - await waitFor(() => { - expect(mockNotificationsManager.info).toHaveBeenCalledWith("Making API Call"); - }); - }); - - it("should close modal when cancel is clicked in standalone mode", async () => { - const user = userEvent.setup(); - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); - }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - expect(screen.getByRole("dialog", { name: /invite user/i })).toBeInTheDocument(); - - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - await user.click(within(dialog).getByRole("button", { name: /close/i })); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - - it("should show onboarding modal when user is created and SSO is disabled", async () => { - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "sso-user" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-sso", - user_id: "sso-user", - has_user_setup_sso: false, - } as any); - - renderWithProviders( - , - ); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); - }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - await user.type(within(dialog).getByLabelText(/user email/i), "sso@example.com"); - await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); - await user.click(screen.getByText("User")); - await user.click(within(dialog).getByRole("button", { name: /invite user/i })); - - await waitFor(() => { - expect(mockInvitationCreateCall).toHaveBeenCalledWith("token", "sso-user"); - }); - await waitFor(() => { - expect(mockNotificationsManager.success).toHaveBeenCalledWith("API user Created"); - }); - }); - - it("should send organizations list in POST body when organizations are selected", async () => { - const { useOrganizations } = await import("@/app/(dashboard)/hooks/organizations/useOrganizations"); - vi.mocked(useOrganizations).mockReturnValue({ - data: [{ organization_id: "org-1", organization_alias: "My Org" }], - isLoading: false, - } as any); - - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "org-user" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-org", - user_id: "org-user", - has_user_setup_sso: false, - } as any); - - renderWithProviders( - , - ); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); - }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - await user.type(within(dialog).getByLabelText(/user email/i), "org@example.com"); - await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); - await user.click(screen.getByText("User")); - - // Select org from the dropdown - const orgSelect = within(dialog).getByRole("combobox", { name: /organization/i }); - await user.click(orgSelect); - await user.click(screen.getByText("My Org (org-1)")); - - await user.click(within(dialog).getByRole("button", { name: /invite user/i })); - - await waitFor(() => { - expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ - organizations: ["org-1"], - })); - }); - }); - - it("should not call organizationMemberAddCall after user creation", async () => { - const { useOrganizations } = await import("@/app/(dashboard)/hooks/organizations/useOrganizations"); - vi.mocked(useOrganizations).mockReturnValue({ - data: [{ organization_id: "org-1", organization_alias: "My Org" }], - isLoading: false, - } as any); - - const user = userEvent.setup(); - mockUserCreateCall.mockResolvedValue({ data: { user_id: "no-member-add-user" } }); - mockInvitationCreateCall.mockResolvedValue({ - id: "inv-nma", - user_id: "no-member-add-user", - has_user_setup_sso: false, - } as any); - - renderWithProviders( - , - ); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); - }); - await user.click(screen.getByRole("button", { name: /\+ invite user/i })); - - const dialog = screen.getByRole("dialog", { name: /invite user/i }); - await user.type(within(dialog).getByLabelText(/user email/i), "nomemberadd@example.com"); - await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); - await user.click(screen.getByText("User")); - await user.click(within(dialog).getByRole("button", { name: /invite user/i })); - - await waitFor(() => { - expect(mockUserCreateCall).toHaveBeenCalled(); - }); - expect(mockOrganizationMemberAddCall).not.toHaveBeenCalled(); - }); }); diff --git a/ui/litellm-dashboard/src/components/OldTeams.test.tsx b/ui/litellm-dashboard/src/components/OldTeams.test.tsx index 651d1495c61..4b89820bad6 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.test.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.test.tsx @@ -843,7 +843,7 @@ describe("OldTeams - access_group_ids in team create", () => { }), ); }); - }, { timeout: 30000 }); + }); }); describe("OldTeams - models dropdown options", () => { diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_tab.test.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_tab.test.tsx index 197bcd6569f..0a8d2d9124e 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_model_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_model_tab.test.tsx @@ -175,7 +175,7 @@ describe("Add Model Tab", () => { ); expect(await screen.findByRole("tab", { name: "Add Model" })).toBeInTheDocument(); - }, 10000); // This test is flaky, adding a timeout until we find a better solution + }); it("should display both Add Model and Add Auto Router tabs", async () => { const props = createTestProps(); @@ -269,7 +269,7 @@ describe("Add Model Tab", () => { }, { timeout: 10000 }, ); - }, 15000); // 15 second timeout to allow waitFor to complete + }); it("should show team selection when team-only switch is enabled", async () => { const props = createTestProps(); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx index c92956b430f..b4251267137 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx @@ -150,151 +150,139 @@ describe("CreateMCPServer", () => { }); }); - it( - "should not require auth value when creating a server with API Key auth type", - { timeout: 15000 }, - async () => { - await selectHttpTransport(); + it("should not require auth value when creating a server with API Key auth type", async () => { + await selectHttpTransport(); - const user = userEvent.setup({ delay: null }); + const user = userEvent.setup({ delay: null }); - // Fill in server name (use id to avoid duplicate placeholder) - const nameInput = getServerNameInput(); - await user.type(nameInput, "Test_Server"); + // Fill in server name (use id to avoid duplicate placeholder) + const nameInput = getServerNameInput(); + await user.type(nameInput, "Test_Server"); - // Fill in URL - const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); - await user.type(urlInput, "https://example.com/mcp"); + // Fill in URL + const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); + await user.type(urlInput, "https://example.com/mcp"); - // Select API Key auth type - await selectAntOption("Authentication", "API Key"); + // Select API Key auth type + await selectAntOption("Authentication", "API Key"); - await waitFor(() => { - expect(screen.getByText("Authentication Value")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Authentication Value")).toBeInTheDocument(); + }); - // Leave auth value empty and submit - vi.mocked(networking.createMCPServer).mockResolvedValue({ - server_id: "new-server-1", - server_name: "Test_Server", - alias: "Test_Server", - url: "https://example.com/mcp", - transport: "http", - auth_type: "api_key", - created_at: "2024-01-01T00:00:00Z", - created_by: "user-1", - updated_at: "2024-01-01T00:00:00Z", - updated_by: "user-1", - }); + // Leave auth value empty and submit + vi.mocked(networking.createMCPServer).mockResolvedValue({ + server_id: "new-server-1", + server_name: "Test_Server", + alias: "Test_Server", + url: "https://example.com/mcp", + transport: "http", + auth_type: "api_key", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + }); - const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); - await act(async () => { - fireEvent.click(submitButton); - }); + const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); + await act(async () => { + fireEvent.click(submitButton); + }); - // The form should submit without validation error on auth_value - await waitFor(() => { - expect(networking.createMCPServer).toHaveBeenCalledTimes(1); - }); - }, - ); + // The form should submit without validation error on auth_value + await waitFor(() => { + expect(networking.createMCPServer).toHaveBeenCalledTimes(1); + }); + }); - it( - "should not require auth value when creating a server with Bearer Token auth type", - { timeout: 15000 }, - async () => { - await selectHttpTransport(); + it("should not require auth value when creating a server with Bearer Token auth type", async () => { + await selectHttpTransport(); - const user = userEvent.setup({ delay: null }); + const user = userEvent.setup({ delay: null }); - const nameInput = getServerNameInput(); - await user.type(nameInput, "Test_Server"); + const nameInput = getServerNameInput(); + await user.type(nameInput, "Test_Server"); - const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); - await user.type(urlInput, "https://example.com/mcp"); + const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); + await user.type(urlInput, "https://example.com/mcp"); - await selectAntOption("Authentication", "Bearer Token"); + await selectAntOption("Authentication", "Bearer Token"); - await waitFor(() => { - expect(screen.getByText("Authentication Value")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Authentication Value")).toBeInTheDocument(); + }); - // Leave auth value empty and submit - vi.mocked(networking.createMCPServer).mockResolvedValue({ - server_id: "new-server-1", - server_name: "Test_Server", - alias: "Test_Server", - url: "https://example.com/mcp", - transport: "http", - auth_type: "bearer_token", - created_at: "2024-01-01T00:00:00Z", - created_by: "user-1", - updated_at: "2024-01-01T00:00:00Z", - updated_by: "user-1", - }); + // Leave auth value empty and submit + vi.mocked(networking.createMCPServer).mockResolvedValue({ + server_id: "new-server-1", + server_name: "Test_Server", + alias: "Test_Server", + url: "https://example.com/mcp", + transport: "http", + auth_type: "bearer_token", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + }); - const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); - await act(async () => { - fireEvent.click(submitButton); - }); + const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); + await act(async () => { + fireEvent.click(submitButton); + }); - await waitFor(() => { - expect(networking.createMCPServer).toHaveBeenCalledTimes(1); - }); - }, - ); + await waitFor(() => { + expect(networking.createMCPServer).toHaveBeenCalledTimes(1); + }); + }); - it( - "should successfully create a server when auth value is provided", - { timeout: 15000 }, - async () => { - await selectHttpTransport(); + it("should successfully create a server when auth value is provided", async () => { + await selectHttpTransport(); - const user = userEvent.setup({ delay: null }); + const user = userEvent.setup({ delay: null }); - const nameInput = getServerNameInput(); - await user.type(nameInput, "My_Server"); + const nameInput = getServerNameInput(); + await user.type(nameInput, "My_Server"); - const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); - await user.type(urlInput, "https://example.com/mcp"); + const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); + await user.type(urlInput, "https://example.com/mcp"); - await selectAntOption("Authentication", "API Key"); + await selectAntOption("Authentication", "API Key"); - await waitFor(() => { - expect(screen.getByText("Authentication Value")).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText("Authentication Value")).toBeInTheDocument(); + }); - // Fill in auth value - const authInput = screen.getByPlaceholderText("Enter token or secret"); - await user.type(authInput, "my-secret-key"); + // Fill in auth value + const authInput = screen.getByPlaceholderText("Enter token or secret"); + await user.type(authInput, "my-secret-key"); - vi.mocked(networking.createMCPServer).mockResolvedValue({ - server_id: "new-server-1", - server_name: "My_Server", - alias: "My_Server", - url: "https://example.com/mcp", - transport: "http", - auth_type: "api_key", - created_at: "2024-01-01T00:00:00Z", - created_by: "user-1", - updated_at: "2024-01-01T00:00:00Z", - updated_by: "user-1", - }); + vi.mocked(networking.createMCPServer).mockResolvedValue({ + server_id: "new-server-1", + server_name: "My_Server", + alias: "My_Server", + url: "https://example.com/mcp", + transport: "http", + auth_type: "api_key", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + }); - const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); - await act(async () => { - fireEvent.click(submitButton); - }); + const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); + await act(async () => { + fireEvent.click(submitButton); + }); - await waitFor(() => { - expect(networking.createMCPServer).toHaveBeenCalledTimes(1); - }); + await waitFor(() => { + expect(networking.createMCPServer).toHaveBeenCalledTimes(1); + }); - const [token, payload] = vi.mocked(networking.createMCPServer).mock.calls[0]; - expect(token).toBe("test-token"); - expect(payload.credentials).toEqual({ auth_value: "my-secret-key" }); - }, - ); + const [token, payload] = vi.mocked(networking.createMCPServer).mock.calls[0]; + expect(token).toBe("test-token"); + expect(payload.credentials).toEqual({ auth_value: "my-secret-key" }); + }); it("should not show auth value field when None auth type is selected", async () => { await selectHttpTransport(); @@ -307,50 +295,46 @@ describe("CreateMCPServer", () => { }); }); - it( - "should successfully create a server with no auth", - { timeout: 15000 }, - async () => { - await selectHttpTransport(); + it("should successfully create a server with no auth", async () => { + await selectHttpTransport(); - const user = userEvent.setup({ delay: null }); + const user = userEvent.setup({ delay: null }); - const nameInput = getServerNameInput(); - await user.type(nameInput, "No_Auth_Server"); + const nameInput = getServerNameInput(); + await user.type(nameInput, "No_Auth_Server"); - const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); - await user.type(urlInput, "https://example.com/mcp"); + const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com"); + await user.type(urlInput, "https://example.com/mcp"); - await selectAntOption("Authentication", "None"); + await selectAntOption("Authentication", "None"); - vi.mocked(networking.createMCPServer).mockResolvedValue({ - server_id: "new-server-1", - server_name: "No_Auth_Server", - alias: "No_Auth_Server", - url: "https://example.com/mcp", - transport: "http", - auth_type: "none", - created_at: "2024-01-01T00:00:00Z", - created_by: "user-1", - updated_at: "2024-01-01T00:00:00Z", - updated_by: "user-1", - }); + vi.mocked(networking.createMCPServer).mockResolvedValue({ + server_id: "new-server-1", + server_name: "No_Auth_Server", + alias: "No_Auth_Server", + url: "https://example.com/mcp", + transport: "http", + auth_type: "none", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + }); - const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); - await act(async () => { - fireEvent.click(submitButton); - }); + const submitButton = screen.getByRole("button", { name: "Add MCP Server" }); + await act(async () => { + fireEvent.click(submitButton); + }); - await waitFor(() => { - expect(networking.createMCPServer).toHaveBeenCalledTimes(1); - }); + await waitFor(() => { + expect(networking.createMCPServer).toHaveBeenCalledTimes(1); + }); - const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0]; - expect(payload.auth_type).toBe("none"); - // No credentials should be sent for "none" auth - expect(payload.credentials).toBeUndefined(); - }, - ); + const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0]; + expect(payload.auth_type).toBe("none"); + // No credentials should be sent for "none" auth + expect(payload.credentials).toBeUndefined(); + }); }); describe("when OAuth interactive auth is selected", () => { diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx index fb149458f61..f24b4e77e16 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx @@ -222,634 +222,640 @@ describe("TeamInfoView", () => { vi.clearAllMocks(); }); - it("should render", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + describe("display and rendering", () => { + it("should render", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - renderWithProviders(); + renderWithProviders(); - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - }); - - it("should display loading state while fetching team data", () => { - vi.mocked(networking.teamInfoCall).mockImplementation(() => new Promise(() => { })); - - renderWithProviders(); - - expect(screen.getByText("Loading...")).toBeInTheDocument(); - }); - - it("should display error message when team is not found", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue({ - team_id: "123", - team_info: null as any, - keys: [], - team_memberships: [], + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); }); - renderWithProviders(); + it("should display loading state while fetching team data", () => { + vi.mocked(networking.teamInfoCall).mockImplementation(() => new Promise(() => {})); - await waitFor(() => { - expect(screen.getByText("Team not found")).toBeInTheDocument(); - }); - }); + renderWithProviders(); - it("should display budget information in overview", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - max_budget: 1000, - spend: 250.5, - budget_duration: "30d", - }) - ); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByText("Budget Status")).toBeInTheDocument(); - }); - }); - - it("should display guardrails in overview when present", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - guardrails: ["guardrail1", "guardrail2"], - }) - ); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByText("Guardrails")).toBeInTheDocument(); - }); - }); - - it("should display policies in overview when present", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - policies: ["policy1"], - }) - ); - vi.mocked(networking.getPolicyInfoWithGuardrails).mockResolvedValue({ - resolved_guardrails: ["guardrail1"], + expect(screen.getByText("Loading...")).toBeInTheDocument(); }); - renderWithProviders(); + it("should display error message when team is not found", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue({ + team_id: "123", + team_info: null as any, + keys: [], + team_memberships: [], + }); - await waitFor(() => { - expect(screen.getByText("Policies")).toBeInTheDocument(); - }); - }); + renderWithProviders(); - it("should show members tab when user can edit team", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByRole("tab", { name: "Members" })).toBeInTheDocument(); - }); - }); - - it("should not show members tab when user cannot edit team", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); + await waitFor(() => { + expect(screen.getByText("Team not found")).toBeInTheDocument(); + }); }); - expect(screen.queryByRole("tab", { name: "Members" })).not.toBeInTheDocument(); - }); - - it("should show settings tab when user can edit team", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByRole("tab", { name: "Settings" })).toBeInTheDocument(); - }); - }); - - it("should navigate to settings tab when clicked", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByText("Team Settings")).toBeInTheDocument(); - }); - }); - - it("should open edit mode when edit button is clicked", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); - }); - - const editButton = screen.getByRole("button", { name: /edit settings/i }); - await user.click(editButton); - - await waitFor(() => { - expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); - }); - }); - - it("should close edit mode when cancel button is clicked", { timeout: 15000 }, async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); - }); - - const editButton = screen.getByRole("button", { name: /edit settings/i }); - await user.click(editButton); - - await waitFor(() => { - expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); - }); - - const cancelButton = screen.getByRole("button", { name: /cancel/i }); - await user.click(cancelButton); - - await waitFor(() => { - expect(screen.queryByLabelText("Team Name")).not.toBeInTheDocument(); - }); - }); - - it("should call onClose when back button is clicked", async () => { - const user = userEvent.setup({ delay: null }); - const onClose = vi.fn(); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const backButton = screen.getByRole("button", { name: /back to teams/i }); - await user.click(backButton); - - expect(onClose).toHaveBeenCalled(); - }); - - it("should copy team ID to clipboard when copy button is clicked", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const copyButtons = screen.getAllByRole("button"); - const copyButton = copyButtons.find((btn) => btn.querySelector("svg")); - expect(copyButton).toBeTruthy(); - - if (copyButton) { - await user.click(copyButton); - } - }); - - it("should disable secret manager settings for non-premium users", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - metadata: { - secret_manager_settings: { provider: "aws", secret_id: "abc" }, - }, - }) - ); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); - }); - - const editButton = screen.getByRole("button", { name: /edit settings/i }); - await user.click(editButton); - - const secretField = await screen.findByPlaceholderText( - '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' - ); - expect(secretField).toBeDisabled(); - }); - - it("should allow premium users to edit secret manager settings", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - metadata: { - secret_manager_settings: { provider: "aws", secret_id: "abc" }, - }, - }) - ); - vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); - }); - - const editButton = screen.getByRole("button", { name: /edit settings/i }); - await user.click(editButton); - - const secretField = await screen.findByPlaceholderText( - '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' - ); - expect(secretField).not.toBeDisabled(); - }); - - it("should add team member when form is submitted", async () => { - const user = userEvent.setup({ delay: null }); - const onUpdate = vi.fn(); - const teamData = createMockTeamData(); - vi.mocked(networking.teamInfoCall).mockResolvedValue(teamData); - vi.mocked(networking.teamMemberAddCall).mockResolvedValue({} as any); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const membersTab = screen.getByRole("tab", { name: "Members" }); - await user.click(membersTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /add member/i })).toBeInTheDocument(); - }); - - const addButton = screen.getByRole("button", { name: /add member/i }); - await user.click(addButton); - - await waitFor(() => { - expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument(); - }); - - const submitButton = screen.getByRole("button", { name: "Submit" }); - await user.click(submitButton); - - await waitFor(() => { - expect(networking.teamMemberAddCall).toHaveBeenCalled(); - }); - }); - - it("should display team member budget information when present", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - team_member_budget_table: { - max_budget: 500, + it("should display budget information in overview", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + max_budget: 1000, + spend: 250.5, budget_duration: "30d", - tpm_limit: 5000, - rpm_limit: 50, - }, - }) - ); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByText("Budget Status")).toBeInTheDocument(); - }); - }); - - it("should display virtual keys information", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue({ - ...createMockTeamData(), - keys: [ - { user_id: "user1", token: "key1" }, - { token: "key2" }, - ], - }); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); - }); - }); - - it("should show Virtual Keys tab when user cannot edit team", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); - }); - }); - - it("should display X Members in Virtual Keys tab when navigated to", async () => { - const user = userEvent.setup(); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - const fiveKeys = Array.from({ length: 5 }, (_, i) => ({ - token: `sk-${i}`, - token_id: `key-${i}`, - key_alias: `key_${i}`, - key_name: `sk-...${i}`, - user_id: `user-${i}`, - organization_id: null, - user: { user_id: `user-${i}`, user_email: `user${i}@test.com` }, - created_at: "2024-01-01T00:00:00Z", - team_id: "123", - spend: 0, - max_budget: 100, - models: ["gpt-4"], - })); - mockUseKeys.mockReturnValue({ - data: { keys: fiveKeys, total_count: 5, current_page: 1, total_pages: 1 }, - isPending: false, - isFetching: false, - refetch: vi.fn(), - } as any); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); - await user.click(virtualKeysTab); - - await waitFor(() => { - expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); - }); - }); - - it("should show Filters and pagination controls in Virtual Keys tab", async () => { - const user = userEvent.setup(); - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - mockUseKeys.mockReturnValue({ - data: { - keys: [ - { - token: "sk-1", - token_id: "key-1", - key_alias: "key1", - key_name: "sk-...1", - user_id: "user-1", - organization_id: null, - user: { user_id: "user-1", user_email: "user1@test.com" }, - created_at: "2024-01-01T00:00:00Z", - team_id: "123", - spend: 0, - max_budget: 100, - models: ["gpt-4"], - }, - ], - total_count: 1, - current_page: 1, - total_pages: 1, - }, - isPending: false, - isFetching: false, - refetch: vi.fn(), - } as any); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); - await user.click(virtualKeysTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: "Filters" })).toBeInTheDocument(); - }); - expect(screen.getByRole("button", { name: "Reset Filters" })).toBeInTheDocument(); - expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Previous" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Next" })).toBeInTheDocument(); - }); - - it("should display object permissions when present", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - object_permission: { - object_permission_id: "perm-1", - mcp_servers: ["server1"], - vector_stores: ["store1"], - }, - }) - ); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - }); - - it("should display soft budget in settings view when present", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - soft_budget: 500.75, - max_budget: 1000, - }) - ); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByText("Team Settings")).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(screen.getByText(/Soft Budget:/)).toBeInTheDocument(); - expect(screen.getByText(/\$500\.75/)).toBeInTheDocument(); - }); - }); - - it("should open Settings tab by default when editTeam is true and user can edit", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - expect(screen.getByText("Team Settings")).toBeInTheDocument(); - }); - - it("should open Overview tab by default when editTeam is false", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - expect(screen.getByText("Budget Status")).toBeInTheDocument(); - }); - - it("should open Overview tab by default when editTeam is true but user cannot edit", async () => { - vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); - - renderWithProviders( - - ); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - expect(screen.getByText("Budget Status")).toBeInTheDocument(); - }); - - it("should display soft budget alerting emails in settings view when present", async () => { - const user = userEvent.setup({ delay: null }); - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - metadata: { - soft_budget_alerting_emails: ["alert1@test.com", "alert2@test.com"], - }, - }) - ); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByText("Team Settings")).toBeInTheDocument(); - }); - - await waitFor(() => { - expect(screen.getByText(/Soft Budget Alerting Emails:/)).toBeInTheDocument(); - expect(screen.getByText(/alert1@test\.com, alert2@test\.com/)).toBeInTheDocument(); - }); - }); - - it("should pass access_group_ids to teamUpdateCall when saving team settings", async () => { - const user = userEvent.setup({ delay: null }); - const accessGroupIds = ["ag-1", "ag-2"]; - vi.mocked(networking.teamInfoCall).mockResolvedValue( - createMockTeamData({ - access_group_ids: accessGroupIds, - models: ["gpt-4"], - }) - ); - vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any); - - renderWithProviders(); - - await waitFor(() => { - const teamNameElements = screen.queryAllByText("Test Team"); - expect(teamNameElements.length).toBeGreaterThan(0); - }); - - const settingsTab = screen.getByRole("tab", { name: "Settings" }); - await user.click(settingsTab); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); - }); - - const editButton = screen.getByRole("button", { name: /edit settings/i }); - await user.click(editButton); - - await waitFor(() => { - expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); - }); - - const saveButton = screen.getByRole("button", { name: /save changes/i }); - await user.click(saveButton); - - await waitFor(() => { - expect(networking.teamUpdateCall).toHaveBeenCalledWith( - "test-token", - expect.objectContaining({ - access_group_ids: accessGroupIds, - team_id: "123", }) ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + }); + + it("should display guardrails in overview when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + guardrails: ["guardrail1", "guardrail2"], + }) + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Guardrails")).toBeInTheDocument(); + }); + }); + + it("should display policies in overview when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + policies: ["policy1"], + }) + ); + vi.mocked(networking.getPolicyInfoWithGuardrails).mockResolvedValue({ + resolved_guardrails: ["guardrail1"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Policies")).toBeInTheDocument(); + }); + }); + + it("should display team member budget information when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + team_member_budget_table: { + max_budget: 500, + budget_duration: "30d", + tpm_limit: 5000, + rpm_limit: 50, + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + }); + + it("should display virtual keys information", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue({ + ...createMockTeamData(), + keys: [ + { user_id: "user1", token: "key1" }, + { token: "key2" }, + ], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); + }); + }); + + it("should display object permissions when present", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + object_permission: { + object_permission_id: "perm-1", + mcp_servers: ["server1"], + vector_stores: ["store1"], + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + }); + + it("should open Settings tab by default when editTeam is true and user can edit", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + it("should open Overview tab by default when editTeam is false", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + + it("should open Overview tab by default when editTeam is true but user cannot edit", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders( + + ); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + expect(screen.getByText("Budget Status")).toBeInTheDocument(); + }); + }); + + describe("tabs and navigation", () => { + it("should show members tab when user can edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Members" })).toBeInTheDocument(); + }); + }); + + it("should not show members tab when user cannot edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + expect(screen.queryByRole("tab", { name: "Members" })).not.toBeInTheDocument(); + }); + + it("should show settings tab when user can edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Settings" })).toBeInTheDocument(); + }); + }); + + it("should navigate to settings tab when clicked", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + }); + + it("should call onClose when back button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + const onClose = vi.fn(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const backButton = screen.getByRole("button", { name: /back to teams/i }); + await user.click(backButton); + + expect(onClose).toHaveBeenCalled(); + }); + + it("should copy team ID to clipboard when copy button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((btn) => btn.querySelector("svg")); + expect(copyButton).toBeTruthy(); + + if (copyButton) { + await user.click(copyButton); + } + }); + + it("should show Virtual Keys tab when user cannot edit team", async () => { + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: "Virtual Keys" })).toBeInTheDocument(); + }); + }); + + it("should display X Members in Virtual Keys tab when navigated to", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + const fiveKeys = Array.from({ length: 5 }, (_, i) => ({ + token: `sk-${i}`, + token_id: `key-${i}`, + key_alias: `key_${i}`, + key_name: `sk-...${i}`, + user_id: `user-${i}`, + organization_id: null, + user: { user_id: `user-${i}`, user_email: `user${i}@test.com` }, + created_at: "2024-01-01T00:00:00Z", + team_id: "123", + spend: 0, + max_budget: 100, + models: ["gpt-4"], + })); + mockUseKeys.mockReturnValue({ + data: { keys: fiveKeys, total_count: 5, current_page: 1, total_pages: 1 }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); + await user.click(virtualKeysTab); + + await waitFor(() => { + expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); + }); + }); + + it("should show Filters and pagination controls in Virtual Keys tab", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + mockUseKeys.mockReturnValue({ + data: { + keys: [ + { + token: "sk-1", + token_id: "key-1", + key_alias: "key1", + key_name: "sk-...1", + user_id: "user-1", + organization_id: null, + user: { user_id: "user-1", user_email: "user1@test.com" }, + created_at: "2024-01-01T00:00:00Z", + team_id: "123", + spend: 0, + max_budget: 100, + models: ["gpt-4"], + }, + ], + total_count: 1, + current_page: 1, + total_pages: 1, + }, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const virtualKeysTab = screen.getByRole("tab", { name: "Virtual Keys" }); + await user.click(virtualKeysTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "Filters" })).toBeInTheDocument(); + }); + expect(screen.getByRole("button", { name: "Reset Filters" })).toBeInTheDocument(); + expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Previous" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Next" })).toBeInTheDocument(); + }); + }); + + describe("settings and editing", () => { + it("should open edit mode when edit button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: /edit settings/i }); + await user.click(editButton); + + await waitFor(() => { + expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); + }); + }); + + it("should close edit mode when cancel button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData()); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: /edit settings/i }); + await user.click(editButton); + + await waitFor(() => { + expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); + }); + + const cancelButton = screen.getByRole("button", { name: /cancel/i }); + await user.click(cancelButton); + + await waitFor(() => { + expect(screen.queryByLabelText("Team Name")).not.toBeInTheDocument(); + }); + }); + + it("should disable secret manager settings for non-premium users", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + metadata: { + secret_manager_settings: { provider: "aws", secret_id: "abc" }, + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: /edit settings/i }); + await user.click(editButton); + + const secretField = await screen.findByPlaceholderText( + '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' + ); + expect(secretField).toBeDisabled(); + }); + + it("should allow premium users to edit secret manager settings", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + metadata: { + secret_manager_settings: { provider: "aws", secret_id: "abc" }, + }, + }) + ); + vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: /edit settings/i }); + await user.click(editButton); + + const secretField = await screen.findByPlaceholderText( + '{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}' + ); + expect(secretField).not.toBeDisabled(); + }); + + it("should add team member when form is submitted", async () => { + const user = userEvent.setup({ delay: null }); + const onUpdate = vi.fn(); + const teamData = createMockTeamData(); + vi.mocked(networking.teamInfoCall).mockResolvedValue(teamData); + vi.mocked(networking.teamMemberAddCall).mockResolvedValue({} as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const membersTab = screen.getByRole("tab", { name: "Members" }); + await user.click(membersTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /add member/i })).toBeInTheDocument(); + }); + + const addButton = screen.getByRole("button", { name: /add member/i }); + await user.click(addButton); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument(); + }); + + const submitButton = screen.getByRole("button", { name: "Submit" }); + await user.click(submitButton); + + await waitFor(() => { + expect(networking.teamMemberAddCall).toHaveBeenCalled(); + }); + }); + + it("should display soft budget in settings view when present", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + soft_budget: 500.75, + max_budget: 1000, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText(/Soft Budget:/)).toBeInTheDocument(); + expect(screen.getByText(/\$500\.75/)).toBeInTheDocument(); + }); + }); + + it("should display soft budget alerting emails in settings view when present", async () => { + const user = userEvent.setup({ delay: null }); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + metadata: { + soft_budget_alerting_emails: ["alert1@test.com", "alert2@test.com"], + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText(/Soft Budget Alerting Emails:/)).toBeInTheDocument(); + expect(screen.getByText(/alert1@test\.com, alert2@test\.com/)).toBeInTheDocument(); + }); + }); + + it("should pass access_group_ids to teamUpdateCall when saving team settings", async () => { + const user = userEvent.setup({ delay: null }); + const accessGroupIds = ["ag-1", "ag-2"]; + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + access_group_ids: accessGroupIds, + models: ["gpt-4"], + }) + ); + vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + + const editButton = screen.getByRole("button", { name: /edit settings/i }); + await user.click(editButton); + + await waitFor(() => { + expect(screen.getByLabelText("Team Name")).toBeInTheDocument(); + }); + + const saveButton = screen.getByRole("button", { name: /save changes/i }); + await user.click(saveButton); + + await waitFor(() => { + expect(networking.teamUpdateCall).toHaveBeenCalledWith( + "test-token", + expect.objectContaining({ + access_group_ids: accessGroupIds, + team_id: "123", + }) + ); + }); }); }); }); diff --git a/ui/litellm-dashboard/vitest.config.ts b/ui/litellm-dashboard/vitest.config.ts index 7c52b88d3b6..d2b6e7b43bf 100644 --- a/ui/litellm-dashboard/vitest.config.ts +++ b/ui/litellm-dashboard/vitest.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ setupFiles: ["tests/setupTests.ts"], globals: true, css: true, // lets you import CSS/modules without extra mocks - testTimeout: 10000, + testTimeout: 30000, coverage: { provider: "v8", reporter: ["text", "lcov"],