Usage top key view conditional rendering

This commit is contained in:
yuneng-jiang 2025-12-18 10:43:40 -08:00
parent fcd524ca0f
commit 3bc5de125a
3 changed files with 199 additions and 9 deletions

View file

@ -30,10 +30,14 @@ vi.mock("../molecules/notifications_manager", () => {
return { default: Notifications };
});
// Roles: ensure 'admin' has write access
vi.mock("../../utils/roles", () => ({
rolesWithWriteAccess: ["admin"],
}));
// Roles: ensure 'admin' has write access and include all role helper functions
vi.mock("../../utils/roles", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../utils/roles")>();
return {
...actual,
rolesWithWriteAccess: ["admin"],
};
});
// Helpers used in rendering
vi.mock("@/utils/dataUtils", () => ({
@ -218,6 +222,40 @@ vi.mock("../common_components/AutoRotationView", async () => {
return { __esModule: true, default: AutoRotationView };
});
// Mock Next.js router to avoid "invariant expected app router to be mounted" error
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
}),
}));
// Mock useTeams hook
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
default: vi.fn(() => ({
teams: [],
setTeams: vi.fn(),
})),
}));
// Mock useAuthorized hook to avoid Next.js router dependency
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(() => ({
accessToken: "access_abc",
userId: "user_1",
userRole: "admin",
premiumUser: true,
token: "token_123",
userEmail: "test@example.com",
disabledPersonalKeyCreation: false,
showSSOBanner: false,
})),
}));
// KeyEditView mock: triggers onSubmit with our injected form values
vi.mock("./key_edit_view", async () => {
const React = await import("react");

View file

@ -1,9 +1,20 @@
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { KeyResponse } from "../key_team_helpers/key_list";
import { render, waitFor, screen } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
import KeyInfoView from "./key_info_view";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
default: vi.fn(),
}));
describe("KeyInfoView", () => {
beforeEach(() => {
vi.mocked(useTeams).mockReturnValue({
teams: [],
setTeams: vi.fn(),
});
});
const MOCK_KEY_DATA: KeyResponse = {
token: "test-token-123",
token_id: "test-token-123",
@ -114,4 +125,134 @@ describe("KeyInfoView", () => {
expect(metadataBlock?.textContent?.trim()).toBe("{}");
});
});
it("should allow proxy admin to modify key", async () => {
vi.mocked(useTeams).mockReturnValue({
teams: [],
setTeams: vi.fn(),
});
const keyData = { ...MOCK_KEY_DATA, user_id: "other-user-id" };
render(
<KeyInfoView
keyData={keyData}
onClose={() => {}}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
accessToken={"test-token"}
userID={"proxy-admin-user"}
userRole={"proxy_admin"}
premiumUser={true}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Regenerate Key")).toBeInTheDocument();
expect(screen.getByText("Delete Key")).toBeInTheDocument();
});
});
it("should allow team admin to modify key", async () => {
const teamId = "test-team-id";
const teamAdminUserId = "team-admin-user";
const mockTeam: Team = {
team_id: teamId,
team_alias: "Test Team",
models: [],
max_budget: null,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2025-01-01T00:00:00Z",
keys: [],
members_with_roles: [
{
user_id: teamAdminUserId,
role: "admin",
},
],
};
vi.mocked(useTeams).mockReturnValue({
teams: [mockTeam],
setTeams: vi.fn(),
});
const keyData = { ...MOCK_KEY_DATA, team_id: teamId, user_id: "other-user-id" };
render(
<KeyInfoView
keyData={keyData}
onClose={() => {}}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
accessToken={"test-token"}
userID={teamAdminUserId}
userRole={"user"}
premiumUser={true}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Regenerate Key")).toBeInTheDocument();
expect(screen.getByText("Delete Key")).toBeInTheDocument();
});
});
it("should allow owner to modify their own key", async () => {
vi.mocked(useTeams).mockReturnValue({
teams: [],
setTeams: vi.fn(),
});
const ownerUserId = "owner-user-id";
const keyData = { ...MOCK_KEY_DATA, user_id: ownerUserId };
render(
<KeyInfoView
keyData={keyData}
onClose={() => {}}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
accessToken={"test-token"}
userID={ownerUserId}
userRole={"user"}
premiumUser={true}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Regenerate Key")).toBeInTheDocument();
expect(screen.getByText("Delete Key")).toBeInTheDocument();
});
});
it("should not allow other user to modify key", async () => {
vi.mocked(useTeams).mockReturnValue({
teams: [],
setTeams: vi.fn(),
});
const keyData = { ...MOCK_KEY_DATA, user_id: "owner-user-id" };
render(
<KeyInfoView
keyData={keyData}
onClose={() => {}}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
accessToken={"test-token"}
userID={"other-user-id"}
userRole={"user"}
premiumUser={true}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.queryByText("Regenerate Key")).not.toBeInTheDocument();
expect(screen.queryByText("Delete Key")).not.toBeInTheDocument();
});
});
});

View file

@ -1,3 +1,4 @@
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { ArrowLeftIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
@ -5,7 +6,7 @@ import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels,
import { Button as AntdButton, Form, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { rolesWithWriteAccess } from "../../utils/roles";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
import AutoRotationView from "../common_components/AutoRotationView";
import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata } from "../key_info_utils";
@ -54,6 +55,7 @@ export default function KeyInfoView({
setAccessToken,
backButtonText = "Back to Keys",
}: KeyInfoViewProps) {
const { teams: teamsData } = useTeams();
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -301,6 +303,15 @@ export default function KeyInfoView({
return `${dateStr} at ${timeStr}`;
};
const canModifyKey =
isProxyAdminRole(userRole || "") ||
(teamsData &&
isUserTeamAdminForSingleTeam(
teamsData?.filter((team) => team.team_id === currentKeyData.team_id)[0],
userID || "",
)) ||
userID === currentKeyData.user_id;
return (
<div className="w-full h-screen p-4">
<div className="flex justify-between items-center mb-6">
@ -349,7 +360,7 @@ export default function KeyInfoView({
)}
</div>
</div>
{userRole && rolesWithWriteAccess.includes(userRole) && (
{canModifyKey && (
<div className="flex gap-2">
<Tooltip
title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}