From 6fd7b1cf5669df663bd2ba3feeca7932af145073 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 6 Jan 2026 22:46:06 -0800 Subject: [PATCH] Adding unit tests for coverage --- .../Modals/BaseSSOSettingsForm.test.tsx | 185 ++++++++++++++++++ .../components/team/available_teams.test.tsx | 138 +++++++++++++ .../team/member_permissions.test.tsx | 160 +++++++++++++++ .../team/permission_definitions.test.tsx | 59 ++++++ 4 files changed, 542 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx create mode 100644 ui/litellm-dashboard/src/components/team/available_teams.test.tsx create mode 100644 ui/litellm-dashboard/src/components/team/member_permissions.test.tsx create mode 100644 ui/litellm-dashboard/src/components/team/permission_definitions.test.tsx diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx new file mode 100644 index 0000000000..a885bffa71 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx @@ -0,0 +1,185 @@ +import { Form } from "antd"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { renderWithProviders } from "../../../../../../tests/test-utils"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import BaseSSOSettingsForm, { renderProviderFields } from "./BaseSSOSettingsForm"; + +describe("BaseSSOSettingsForm", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should render", () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + expect(screen.getByText("SSO Provider")).toBeInTheDocument(); + expect(screen.getByText("Proxy Admin Email")).toBeInTheDocument(); + expect(screen.getByText("Proxy Base URL")).toBeInTheDocument(); + }); + + it("should render provider fields when provider is selected", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + const providerSelect = screen.getByLabelText("SSO Provider"); + await act(async () => { + fireEvent.mouseDown(providerSelect); + }); + + await waitFor(() => { + const googleOption = screen.getByText(/google sso/i); + fireEvent.click(googleOption); + }); + + await waitFor(() => { + expect(screen.getByText("Google Client ID")).toBeInTheDocument(); + expect(screen.getByText("Google Client Secret")).toBeInTheDocument(); + }); + }); + + it("should show role mappings fields for okta provider", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + const providerSelect = screen.getByLabelText("SSO Provider"); + await act(async () => { + fireEvent.mouseDown(providerSelect); + }); + + await waitFor(() => { + const oktaOption = screen.getByText(/okta/i); + fireEvent.click(oktaOption); + }); + + await waitFor(() => { + expect(screen.getByText("Use Role Mappings")).toBeInTheDocument(); + }); + }); + + it("should validate proxy base url format", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + const urlInput = screen.getByPlaceholderText("https://example.com"); + await act(async () => { + fireEvent.change(urlInput, { target: { value: "invalid-url" } }); + fireEvent.blur(urlInput); + }); + + await waitFor(() => { + expect(screen.getByText(/URL must start with http:\/\/ or https:\/\//i)).toBeInTheDocument(); + }); + }); + + it("should validate proxy base url trailing slash", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + const urlInput = screen.getByPlaceholderText("https://example.com"); + await act(async () => { + fireEvent.change(urlInput, { target: { value: "https://example.com/" } }); + fireEvent.blur(urlInput); + }); + + await waitFor(() => { + expect(screen.getByText(/URL must not end with a trailing slash/i)).toBeInTheDocument(); + }); + }); + + it("should show role mappings fields when use_role_mappings is checked for generic provider", async () => { + const TestWrapper = () => { + const [form] = Form.useForm(); + const handleSubmit = vi.fn(); + + return ; + }; + + renderWithProviders(); + + const providerSelect = screen.getByLabelText("SSO Provider"); + await act(async () => { + fireEvent.mouseDown(providerSelect); + }); + + await waitFor(() => { + const genericOption = screen.getByText(/generic sso/i); + fireEvent.click(genericOption); + }); + + await waitFor(() => { + expect(screen.getByText("Use Role Mappings")).toBeInTheDocument(); + }); + + const checkbox = screen.getByLabelText("Use Role Mappings"); + await act(async () => { + fireEvent.click(checkbox); + }); + + await waitFor(() => { + expect(screen.getByText("Group Claim")).toBeInTheDocument(); + expect(screen.getByText("Default Role")).toBeInTheDocument(); + }); + }); +}); + +describe("renderProviderFields", () => { + it("should return null for unknown provider", () => { + const result = renderProviderFields("unknown"); + expect(result).toBeNull(); + }); + + it("should return fields for google provider", () => { + const result = renderProviderFields("google"); + expect(result).not.toBeNull(); + expect(result?.length).toBe(2); + }); + + it("should return fields for microsoft provider", () => { + const result = renderProviderFields("microsoft"); + expect(result).not.toBeNull(); + expect(result?.length).toBe(3); + }); + + it("should return fields for okta provider", () => { + const result = renderProviderFields("okta"); + expect(result).not.toBeNull(); + expect(result?.length).toBe(5); + }); + + it("should return fields for generic provider", () => { + const result = renderProviderFields("generic"); + expect(result).not.toBeNull(); + expect(result?.length).toBe(5); + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/available_teams.test.tsx b/ui/litellm-dashboard/src/components/team/available_teams.test.tsx new file mode 100644 index 0000000000..af2a0247b4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/available_teams.test.tsx @@ -0,0 +1,138 @@ +import * as networking from "@/components/networking"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import AvailableTeamsPanel from "./available_teams"; + +vi.mock("@/components/networking", () => ({ + availableTeamListCall: vi.fn(), + teamMemberAddCall: vi.fn(), +})); + +describe("AvailableTeamsPanel", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should render", async () => { + vi.mocked(networking.availableTeamListCall).mockResolvedValue([]); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Team Name")).toBeInTheDocument(); + }); + }); + + it("should display teams when available", async () => { + const mockTeams = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + description: "Test Description 1", + models: ["gpt-4"], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }], + }, + { + team_id: "team-2", + team_alias: "Test Team 2", + description: "Test Description 2", + models: [], + members_with_roles: [{ user_id: "user-2", user_email: "user2@test.com", role: "user" }], + }, + ]; + + vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Test Team 1")).toBeInTheDocument(); + expect(screen.getByText("Test Team 2")).toBeInTheDocument(); + }); + }); + + it("should display empty state when no teams are available", async () => { + vi.mocked(networking.availableTeamListCall).mockResolvedValue([]); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("No available teams to join")).toBeInTheDocument(); + }); + }); + + it("should call teamMemberAddCall when join team button is clicked", async () => { + const mockTeams = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + description: "Test Description 1", + models: ["gpt-4"], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }], + }, + ]; + + vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams); + vi.mocked(networking.teamMemberAddCall).mockResolvedValue({}); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Test Team 1")).toBeInTheDocument(); + }); + + const joinButtons = screen.getAllByRole("button", { name: /join team/i }); + await act(async () => { + fireEvent.click(joinButtons[0]); + }); + + await waitFor(() => { + expect(networking.teamMemberAddCall).toHaveBeenCalledWith("token-123", "team-1", { + user_id: "user-123", + role: "user", + }); + }); + }); + + it("should display All Proxy Models badge when team has no models", async () => { + const mockTeams = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + description: "Test Description 1", + models: [], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }], + }, + ]; + + vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); + }); + }); + + it("should display model badges when team has models", async () => { + const mockTeams = [ + { + team_id: "team-1", + team_alias: "Test Team 1", + description: "Test Description 1", + models: ["gpt-4", "gpt-3.5-turbo"], + members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }], + }, + ]; + + vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("gpt-4")).toBeInTheDocument(); + expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/member_permissions.test.tsx b/ui/litellm-dashboard/src/components/team/member_permissions.test.tsx new file mode 100644 index 0000000000..cee2b8d587 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/member_permissions.test.tsx @@ -0,0 +1,160 @@ +import * as networking from "@/components/networking"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import MemberPermissions from "./member_permissions"; + +vi.mock("@/components/networking", () => ({ + getTeamPermissionsCall: vi.fn(), + teamPermissionsUpdateCall: vi.fn(), +})); + +describe("MemberPermissions", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should render", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Member Permissions")).toBeInTheDocument(); + }); + }); + + it("should display permissions table when permissions are available", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Method")).toBeInTheDocument(); + expect(screen.getByText("Endpoint")).toBeInTheDocument(); + expect(screen.getByText("Description")).toBeInTheDocument(); + expect(screen.getByText("Allow Access")).toBeInTheDocument(); + }); + }); + + it("should display empty state when no permissions are available", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: [], + team_member_permissions: [], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("No permissions available")).toBeInTheDocument(); + }); + }); + + it("should save permissions when save button is clicked", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + vi.mocked(networking.teamPermissionsUpdateCall).mockResolvedValue({}); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Member Permissions")).toBeInTheDocument(); + }); + + const checkboxes = screen.getAllByRole("checkbox"); + const unselectedCheckbox = checkboxes.find((cb) => !(cb as HTMLInputElement).checked); + + if (unselectedCheckbox) { + await act(async () => { + fireEvent.click(unselectedCheckbox); + }); + + await waitFor(() => { + const saveButton = screen.getByRole("button", { name: /save changes/i }); + expect(saveButton).toBeInTheDocument(); + }); + + const saveButton = screen.getByRole("button", { name: /save changes/i }); + await act(async () => { + fireEvent.click(saveButton); + }); + + await waitFor(() => { + expect(networking.teamPermissionsUpdateCall).toHaveBeenCalledWith( + "token-123", + "team-123", + expect.arrayContaining(["/key/generate", "/key/list"]), + ); + }); + } + }); + + it("should not show save button when canEditTeam is false", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Member Permissions")).toBeInTheDocument(); + }); + + const checkboxes = screen.getAllByRole("checkbox"); + checkboxes.forEach((checkbox) => { + expect(checkbox).toBeDisabled(); + }); + + expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument(); + }); + + it("should handle reset button click", async () => { + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Member Permissions")).toBeInTheDocument(); + }); + + const checkboxes = screen.getAllByRole("checkbox"); + const unselectedCheckbox = checkboxes.find((cb) => !(cb as HTMLInputElement).checked); + + if (unselectedCheckbox) { + await act(async () => { + fireEvent.click(unselectedCheckbox); + }); + + await waitFor(() => { + const resetButton = screen.getByRole("button", { name: /reset/i }); + expect(resetButton).toBeInTheDocument(); + }); + + vi.mocked(networking.getTeamPermissionsCall).mockResolvedValueOnce({ + all_available_permissions: ["/key/generate", "/key/list"], + team_member_permissions: ["/key/generate"], + }); + + const resetButton = screen.getByRole("button", { name: /reset/i }); + await act(async () => { + fireEvent.click(resetButton); + }); + + await waitFor(() => { + expect(networking.getTeamPermissionsCall).toHaveBeenCalledTimes(2); + }); + } + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/permission_definitions.test.tsx b/ui/litellm-dashboard/src/components/team/permission_definitions.test.tsx new file mode 100644 index 0000000000..2d8aeabf38 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/permission_definitions.test.tsx @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { getMethodForEndpoint, getPermissionInfo, PERMISSION_DESCRIPTIONS } from "./permission_definitions"; + +describe("permission_definitions", () => { + describe("getMethodForEndpoint", () => { + it("should return GET for info endpoints", () => { + expect(getMethodForEndpoint("/key/info")).toBe("GET"); + }); + + it("should return GET for list endpoints", () => { + expect(getMethodForEndpoint("/key/list")).toBe("GET"); + }); + + it("should return POST for other endpoints", () => { + expect(getMethodForEndpoint("/key/generate")).toBe("POST"); + expect(getMethodForEndpoint("/key/update")).toBe("POST"); + expect(getMethodForEndpoint("/key/delete")).toBe("POST"); + }); + }); + + describe("getPermissionInfo", () => { + it("should return correct info for exact match permission", () => { + const result = getPermissionInfo("/key/generate"); + expect(result.method).toBe("POST"); + expect(result.endpoint).toBe("/key/generate"); + expect(result.description).toBe(PERMISSION_DESCRIPTIONS["/key/generate"]); + expect(result.route).toBe("/key/generate"); + }); + + it("should return GET method for info endpoint", () => { + const result = getPermissionInfo("/key/info"); + expect(result.method).toBe("GET"); + expect(result.endpoint).toBe("/key/info"); + expect(result.description).toBe(PERMISSION_DESCRIPTIONS["/key/info"]); + }); + + it("should return GET method for list endpoint", () => { + const result = getPermissionInfo("/key/list"); + expect(result.method).toBe("GET"); + expect(result.endpoint).toBe("/key/list"); + expect(result.description).toBe(PERMISSION_DESCRIPTIONS["/key/list"]); + }); + + it("should find partial match for permission with pattern", () => { + const result = getPermissionInfo("/key/service-account/generate"); + expect(result.method).toBe("POST"); + expect(result.endpoint).toBe("/key/service-account/generate"); + expect(result.description).toBe(PERMISSION_DESCRIPTIONS["/key/service-account/generate"]); + }); + + it("should return fallback description for unknown permission", () => { + const result = getPermissionInfo("/unknown/endpoint"); + expect(result.method).toBe("POST"); + expect(result.endpoint).toBe("/unknown/endpoint"); + expect(result.description).toBe("Access /unknown/endpoint"); + expect(result.route).toBe("/unknown/endpoint"); + }); + }); +});