
{
+ const url = imagePreviewUrls[index];
+ if (!url) return "";
+ try {
+ const parsed = new URL(url);
+ return parsed.protocol === "blob:" ? parsed.href : "";
+ } catch {
+ return "";
+ }
+ })()}
alt={`Upload preview ${index + 1}`}
className="max-w-32 max-h-32 rounded-md border border-gray-200 object-cover"
/>
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/src/components/user_edit_view.test.tsx b/ui/litellm-dashboard/src/components/user_edit_view.test.tsx
index 7f78ef45127..7aeaae94fab 100644
--- a/ui/litellm-dashboard/src/components/user_edit_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/user_edit_view.test.tsx
@@ -1,6 +1,6 @@
-import { screen, waitFor } from "@testing-library/react";
+import { cleanup, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../tests/test-utils";
import { UserEditView } from "./user_edit_view";
@@ -140,6 +140,15 @@ describe("UserEditView", () => {
vi.clearAllMocks();
});
+ afterEach(() => {
+ // Tremor's internal Tooltip sets a setTimeout that fires after teardown,
+ // causing "window is not defined". Flush pending timers before cleanup.
+ vi.useFakeTimers();
+ vi.runAllTimers();
+ vi.useRealTimers();
+ cleanup();
+ });
+
it("should render", async () => {
renderWithProviders(
);
diff --git a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx
index e1afbf2e925..24881e669f9 100644
--- a/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx
+++ b/ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx
@@ -11,6 +11,7 @@ import {
serverRootPath,
} from "@/components/networking";
import { extractErrorMessage } from "@/utils/errorUtils";
+import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
export type McpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error";
@@ -79,22 +80,13 @@ export const useMcpOAuthFlow = ({
const setStorageItem = (key: string, value: string) => {
if (typeof window === "undefined") return;
- try {
- // Use sessionStorage only — the flow state may contain client credentials;
- // writing them to localStorage would persist across browser sessions and
- // make them readable by any injected script (XSS).
- // codeql[js/clear-text-storage-of-sensitive-data]
- window.sessionStorage.setItem(key, value);
- } catch (err) {
- console.warn(`Failed to set storage item ${key}`, err);
- }
+ setSecureItem(key, value);
};
const getStorageItem = (key: string): string | null => {
if (typeof window === "undefined") return null;
try {
- // Try sessionStorage first, fall back to localStorage
- return window.sessionStorage.getItem(key) || window.localStorage.getItem(key);
+ return getSecureItem(key);
} catch (err) {
console.warn(`Failed to get storage item ${key}`, err);
return null;
diff --git a/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx b/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx
index 3bb43d14ca4..e032c503dc7 100644
--- a/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx
+++ b/ui/litellm-dashboard/src/hooks/useUserMcpOAuthFlow.tsx
@@ -23,6 +23,7 @@ import {
} from "@/components/networking";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { extractErrorMessage } from "@/utils/errorUtils";
+import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
export type UserMcpOAuthStatus = "idle" | "authorizing" | "exchanging" | "success" | "error";
@@ -79,22 +80,11 @@ const genChallenge = async (verifier: string) => {
};
const setStorage = (key: string, value: string) => {
- try {
- // Use sessionStorage only — do not write to localStorage.
- // The flow state may contain the LiteLLM access token; writing it to
- // localStorage would persist it across browser sessions and make it
- // readable by any injected script (XSS).
- // codeql[js/clear-text-storage-of-sensitive-data]
- window.sessionStorage.setItem(key, value);
- } catch (_) {}
+ setSecureItem(key, value);
};
const getStorage = (key: string): string | null => {
- try {
- return window.sessionStorage.getItem(key);
- } catch (_) {
- return null;
- }
+ return getSecureItem(key);
};
const clearStorage = (...keys: string[]) => {
diff --git a/ui/litellm-dashboard/src/utils/secureStorage.ts b/ui/litellm-dashboard/src/utils/secureStorage.ts
new file mode 100644
index 00000000000..183b572e737
--- /dev/null
+++ b/ui/litellm-dashboard/src/utils/secureStorage.ts
@@ -0,0 +1,34 @@
+function encode(value: string): string {
+ // btoa cannot handle characters outside Latin-1, so we percent-encode first.
+ return btoa(
+ encodeURIComponent(value).replace(
+ /%([0-9A-F]{2})/g,
+ (_, p1) => String.fromCharCode(parseInt(p1, 16))
+ )
+ );
+}
+
+function decode(encoded: string): string {
+ return decodeURIComponent(
+ atob(encoded)
+ .split("")
+ .map((c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0"))
+ .join("")
+ );
+}
+
+export function setSecureItem(key: string, value: string): void {
+ window.sessionStorage.setItem(key, encode(value));
+}
+
+export function getSecureItem(key: string): string | null {
+ try {
+ const raw = window.sessionStorage.getItem(key);
+ if (raw === null) return null;
+ return decode(raw);
+ } catch {
+ // Corrupted or non-encoded legacy value — return null without deleting
+ // so that in-flight flows (e.g. OAuth) can time out naturally.
+ return null;
+ }
+}
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"],