Files
litellm/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx
T
Ryan Crabbe a9d64f8620 refactor(ui/guardrails): expose full data from useGuardrails hook
Extend useGuardrails to return the full guardrail objects plus derived
globalGuardrailNames / optionalGuardrailNames sets via React Query's
select option, instead of just an array of names. Update its existing
consumer (AddModelForm) to extract names from the new shape.

The previous shape was tailored to AddModelForm's single use case
(populate a Select with names). The team info per-guardrail opt-out
work needs default_on per guardrail to split globals from non-globals,
which the old shape couldn't provide. Consolidating into the existing
hook gives both consumers one source of truth and one React Query
cache entry instead of two parallel fetches.

- useGuardrails.ts: rewrite return type, derive global/optional sets
  in select(); preserve the existing query key and auth-gate semantics
- AddModelForm.tsx: extract names from data?.guardrails.map(...)
- AddModelForm.test.tsx: update mock to return the new shape (also
  fixes a pre-existing shape mismatch in the mock)
- useGuardrails.test.ts: update 3 assertions to read names via
  data?.guardrails.map(...) instead of asserting against the flat array
2026-04-11 17:48:16 -07:00

296 lines
9.4 KiB
TypeScript

import { renderHook, screen, waitFor, renderWithProviders } from "../../../tests/test-utils";
import userEvent from "@testing-library/user-event";
import { Form } from "antd";
import type { UploadProps } from "antd/es/upload";
import { describe, expect, it, vi } from "vitest";
import type { Team } from "../key_team_helpers/key_list";
import type { CredentialItem } from "../networking";
import { Providers } from "../provider_info_helpers";
import AddModelForm from "./AddModelForm";
vi.mock("../molecules/models/ProviderLogo", () => ({
ProviderLogo: ({ provider, className }: { provider: string; className?: string }) => (
<div className={className} data-testid={`provider-logo-${provider}`}>
{provider}
</div>
),
}));
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getGuardrailsList: vi.fn().mockResolvedValue({
guardrails: [{ guardrail_name: "test-guardrail-1" }, { guardrail_name: "test-guardrail-2" }],
}),
tagListCall: vi.fn().mockResolvedValue({}),
modelAvailableCall: vi.fn().mockResolvedValue({
data: [{ id: "model-group-1" }, { id: "model-group-2" }],
}),
modelHubCall: vi.fn().mockResolvedValue({
data: [
{ model_group: "gpt-4", mode: "chat" },
{ model_group: "gpt-3.5-turbo", mode: "chat" },
],
}),
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: "OpenAI",
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [],
},
]),
};
});
vi.mock("@/app/(dashboard)/hooks/providers/useProviderFields", () => ({
useProviderFields: vi.fn().mockReturnValue({
data: [
{
provider: "OpenAI",
provider_display_name: "OpenAI",
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [],
},
],
isLoading: false,
error: null,
}),
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
useInfiniteTeams: () => ({
data: {
pages: [{
teams: [{ team_id: "team-1", team_alias: "Test Team", organization_id: "org-1" }],
total: 1, page: 1, page_size: 20, total_pages: 1,
}],
},
fetchNextPage: vi.fn(),
hasNextPage: false,
isFetchingNextPage: false,
isLoading: false,
}),
}));
vi.mock("@/app/(dashboard)/hooks/guardrails/useGuardrails", () => ({
useGuardrails: vi.fn().mockReturnValue({
data: {
guardrails: [{ guardrail_name: "test-guardrail" }],
globalGuardrailNames: new Set<string>(),
optionalGuardrailNames: new Set<string>(["test-guardrail"]),
},
isLoading: false,
error: null,
}),
}));
vi.mock("@/app/(dashboard)/hooks/tags/useTags", () => ({
useTags: vi.fn().mockReturnValue({
data: { tag1: ["model1", "model2"] },
isLoading: false,
error: null,
}),
}));
const mockAuthorizedUser = (userRole: string, userId: string, premiumUser: boolean) => ({
token: "test-token",
accessToken: "test-access-token",
userId,
userEmail: "test@example.com",
userRole,
premiumUser,
disabledPersonalKeyCreation: false,
showSSOBanner: false,
});
const testTeam: Team = {
team_id: "team-1",
team_alias: "Test Team",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "monthly",
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2024-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
};
const createTestProps = (userRole = "proxy_admin", userId = "user-1", isTeamAdmin = false) => {
const { result } = renderHook(() => Form.useForm());
const [form] = result.current;
const teams = [
{
...testTeam,
members_with_roles: isTeamAdmin ? [{ user_id: userId, role: "admin" }] : [],
},
];
const credentials: CredentialItem[] = [
{
credential_name: "test-credential",
credential_values: {},
credential_info: {
custom_llm_provider: "openai",
description: "Test credential",
},
},
];
const uploadProps: UploadProps = {
beforeUpload: () => false,
showUploadList: false,
};
return {
form,
handleOk: vi.fn(),
setSelectedProvider: vi.fn(),
setProviderModelsFn: vi.fn(),
getPlaceholder: vi.fn((provider: Providers) => `Enter ${provider} model name`),
setShowAdvancedSettings: vi.fn(),
selectedProvider: Providers.OpenAI,
providerModels: ["gpt-4", "gpt-3.5-turbo"],
showAdvancedSettings: false,
teams,
credentials,
uploadProps,
userRole,
userId,
};
};
describe("AddModelForm", () => {
it("should render", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("proxy_admin", "user-1", true));
const props = createTestProps();
renderWithProviders(<AddModelForm {...props} />);
expect(await screen.findByRole("heading", { name: "Add Model" })).toBeInTheDocument();
});
it("should show proxy admin only (not team admin) - should not see Select Team dropdown unless switch is toggled", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("proxy_admin", "user-1", true));
const props = createTestProps("proxy_admin", "user-1", false);
renderWithProviders(<AddModelForm {...props} />);
await screen.findByText("Provider");
expect(screen.queryByText("Team Selection Required")).not.toBeInTheDocument();
expect(screen.queryByText("Select Team")).not.toBeInTheDocument();
const teamSwitch = screen.getByRole("switch");
expect(teamSwitch).toBeInTheDocument();
expect(screen.queryByText("Select Team")).not.toBeInTheDocument();
await userEvent.click(teamSwitch);
expect(await screen.findByText("Select Team")).toBeInTheDocument();
});
it("should show proxy admin who is also team admin - should not see Select Team dropdown unless switch is toggled", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("proxy_admin", "user-1", true));
const props = createTestProps("proxy_admin", "user-1", true);
renderWithProviders(<AddModelForm {...props} />);
await screen.findByText("Provider");
expect(screen.queryByText("Team Selection Required")).not.toBeInTheDocument();
expect(screen.queryByText("Select Team")).not.toBeInTheDocument();
const teamSwitch = screen.getByRole("switch");
expect(teamSwitch).toBeInTheDocument();
expect(screen.queryByText("Select Team")).not.toBeInTheDocument();
await userEvent.click(teamSwitch);
expect(await screen.findByText("Select Team")).toBeInTheDocument();
});
it("should show team admin (not proxy admin) - should see alert and team select, must select team before seeing remaining fields", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("team_member", "user-1", true));
const props = createTestProps("team_member", "user-1", true);
renderWithProviders(<AddModelForm {...props} />);
await screen.findByRole("heading", { name: "Add Model" });
expect(screen.getByText("Team Selection Required")).toBeInTheDocument();
expect(screen.getByText("Select Team")).toBeInTheDocument();
expect(screen.queryByText("Provider")).not.toBeInTheDocument();
const teamSelect = screen.getByRole("combobox");
await userEvent.click(teamSelect);
await userEvent.click(screen.getByText("Test Team"));
await waitFor(() => {
expect(screen.getByText("Provider")).toBeInTheDocument();
});
});
it("should show team admin (not proxy admin) - should not see team-BYOK switch", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("team_member", "user-1", true));
const props = createTestProps("team_member", "user-1", true);
renderWithProviders(<AddModelForm {...props} />);
await screen.findByText("Select Team");
const teamSelect = screen.getByRole("combobox");
await userEvent.click(teamSelect);
await userEvent.click(screen.getByText("Test Team"));
await waitFor(() => {
expect(screen.getByText("Provider")).toBeInTheDocument();
});
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
});
it("should handle non-admin, non-team-admin users - should not see team selection or switch", async () => {
const mockUseAuthorized = vi.mocked(await import("@/app/(dashboard)/hooks/useAuthorized"));
mockUseAuthorized.default.mockReturnValue(mockAuthorizedUser("user", "user-1", false));
const props = createTestProps("user", "user-1", false);
renderWithProviders(<AddModelForm {...props} />);
await screen.findByRole("heading", { name: "Add Model" });
expect(screen.queryByText("Team Selection Required")).not.toBeInTheDocument();
expect(screen.queryByText("Select Team")).not.toBeInTheDocument();
expect(screen.queryByText("Provider")).not.toBeInTheDocument();
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
});
});