import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { UserApiKeyRow } from "@/components/ui/user-api-key-row"; import type { UserApiKeyStatus } from "@/lib/types"; import { renderWithQueryClient } from "./test-utils"; const anthropicKey: UserApiKeyStatus = { provider: "anthropic", label: "Anthropic", configured: false, value: null, credits: null, credits_note: "Anthropic doesn't expose a credit/usage-balance API.", free: false, requires_government_id: false, }; const usptoKey: UserApiKeyStatus = { provider: "uspto", label: "USPTO", configured: true, value: "my-uspto-key", credits: null, credits_note: "Free - USPTO Open Data Portal has no usage limit or credit cost.", free: true, requires_government_id: true, }; const ninjapearKey: UserApiKeyStatus = { provider: "ninjapear", label: "NinjaPear", configured: true, value: "my-ninjapear-key", credits: null, credits_note: "Credit balance shown in System configuration below.", free: false, requires_government_id: false, }; beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); describe("UserApiKeyRow", () => { it("shows the provider label and a masked input for a configured key", () => { renderWithQueryClient(); expect(screen.getByText("USPTO")).toBeInTheDocument(); const textbox = screen.getByDisplayValue("my-uspto-key") as HTMLInputElement; expect(textbox.type).toBe("password"); }); it("shows the free + government-ID badge for USPTO", () => { renderWithQueryClient(); expect(screen.getByText(/free ยท requires government id approval/i)).toBeInTheDocument(); }); it("does not show the free badge for a paid provider", () => { renderWithQueryClient(); expect(screen.queryByText(/requires government id approval/i)).not.toBeInTheDocument(); }); it("toggles the input between masked and revealed", async () => { const user = userEvent.setup(); renderWithQueryClient(); const textbox = screen.getByDisplayValue("my-uspto-key") as HTMLInputElement; expect(textbox.type).toBe("password"); await user.click(screen.getByTitle(/show key/i)); expect(textbox.type).toBe("text"); await user.click(screen.getByTitle(/hide key/i)); expect(textbox.type).toBe("password"); }); it("disables Update until the value actually changes", async () => { const user = userEvent.setup(); renderWithQueryClient(); const updateButton = screen.getByRole("button", { name: /update/i }); expect(updateButton).toBeDisabled(); const textbox = screen.getByPlaceholderText(/^not set$/i); await user.type(textbox, "sk-new-key"); expect(updateButton).toBeEnabled(); }); it("submits the new key via PUT and reflects the saved value", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ ...anthropicKey, configured: true, value: "sk-new-key" }), }); vi.stubGlobal("fetch", fetchMock); const user = userEvent.setup(); renderWithQueryClient(); const textbox = screen.getByPlaceholderText(/^not set$/i); await user.type(textbox, "sk-new-key"); await user.click(screen.getByRole("button", { name: /update/i })); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/api/v1/user-api-keys/anthropic"), expect.objectContaining({ method: "PUT", body: JSON.stringify({ key: "sk-new-key" }), }), ); }); await waitFor(() => { expect(screen.getByDisplayValue("sk-new-key")).toBeInTheDocument(); }); }); it("ignores the backend's own credits field and shows nothing extra when liveCredits is not passed", () => { renderWithQueryClient(); expect( screen.getByText(/credit balance shown in system configuration below/i), ).toBeInTheDocument(); expect(screen.queryByText(/credits remaining/i)).not.toBeInTheDocument(); }); it("shows the liveCredits number when passed, sourced from system status rather than this row's own fetch", () => { renderWithQueryClient(); expect(screen.getByText(/891 credits remaining/i)).toBeInTheDocument(); }); it("falls back to the note-only text when liveCredits is explicitly null", () => { renderWithQueryClient(); expect(screen.queryByText(/credits remaining/i)).not.toBeInTheDocument(); expect( screen.getByText(/credit balance shown in system configuration below/i), ).toBeInTheDocument(); }); });