import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { describe, it, expect, vi, beforeEach } from "vitest"; import AdminPage from "./AdminPage"; import * as client from "../api/client"; // --------------------------------------------------------------------------- // Mock data // --------------------------------------------------------------------------- const MOCK_SETTINGS = { guest_access_enabled: false, default_endpoint_id: "ep1", token_budget_daily: 100000, token_budget_monthly: 2000000, api_keys: [ { id: "key1", label: "CI Pipeline", prefix: "pl_abc", created_at: "2026-04-01T10:00:00Z", }, ], }; const MOCK_STATS = { total_runs: 1234, total_experiments: 56, total_projects: 8, cache_entries: 890, cache_hit_rate: 0.73, storage_bytes: 52428800, tokens_spent: 456789, }; const MOCK_ENDPOINTS: client.EndpointResponse[] = [ { id: "ep1", name: "OpenAI", url: "https://api.openai.com", default_model: "gpt-4" }, { id: "ep2", name: "Local vLLM", url: "http://localhost:8080", default_model: null }, ]; const MOCK_WEBHOOKS: client.WebhookResponse[] = [ { id: "wh1", event_type: "run.completed", url: "https://example.com/hook", headers: null, is_active: true, }, ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function mockAllApis() { vi.spyOn(client.admin, "getSettings").mockResolvedValue( MOCK_SETTINGS as unknown as Record, ); vi.spyOn(client.admin, "getStats").mockResolvedValue( MOCK_STATS as unknown as Record, ); vi.spyOn(client.admin, "updateSettings").mockResolvedValue( {} as Record, ); vi.spyOn(client.endpoints, "list").mockResolvedValue({ items: MOCK_ENDPOINTS, total: MOCK_ENDPOINTS.length, }); vi.spyOn(client.webhooks, "list").mockResolvedValue({ items: MOCK_WEBHOOKS, total: MOCK_WEBHOOKS.length, }); vi.spyOn(client.webhooks, "create").mockResolvedValue({ id: "wh2", event_type: "run.started", url: "https://new.com/hook", headers: null, is_active: true, }); vi.spyOn(client.webhooks, "delete").mockResolvedValue(undefined as never); } function renderAdmin() { return render( , ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("AdminPage", () => { beforeEach(() => { vi.restoreAllMocks(); }); it("shows loading state initially", () => { vi.spyOn(client.admin, "getSettings").mockReturnValue( new Promise(() => {}), ); vi.spyOn(client.admin, "getStats").mockReturnValue(new Promise(() => {})); vi.spyOn(client.endpoints, "list").mockReturnValue(new Promise(() => {})); vi.spyOn(client.webhooks, "list").mockReturnValue(new Promise(() => {})); renderAdmin(); expect(screen.getByText("Loading admin data...")).toBeInTheDocument(); }); it("shows error state on API failure", async () => { vi.spyOn(client.admin, "getSettings").mockRejectedValue( new client.ApiError(500, "Internal Server Error", {}), ); vi.spyOn(client.admin, "getStats").mockRejectedValue( new client.ApiError(500, "Internal Server Error", {}), ); vi.spyOn(client.endpoints, "list").mockRejectedValue( new client.ApiError(500, "Internal Server Error", {}), ); vi.spyOn(client.webhooks, "list").mockRejectedValue( new client.ApiError(500, "Internal Server Error", {}), ); renderAdmin(); await waitFor(() => { expect(screen.getByRole("alert")).toBeInTheDocument(); }); expect(screen.getByText(/Failed to load admin data/)).toBeInTheDocument(); }); it("shows network error message on non-API errors", async () => { vi.spyOn(client.admin, "getSettings").mockRejectedValue( new Error("fetch failed"), ); vi.spyOn(client.admin, "getStats").mockResolvedValue({}); vi.spyOn(client.endpoints, "list").mockResolvedValue({ items: [], total: 0, }); vi.spyOn(client.webhooks, "list").mockResolvedValue({ items: [], total: 0, }); renderAdmin(); await waitFor(() => { expect( screen.getByText("Network error. Is the server running?"), ).toBeInTheDocument(); }); }); it("renders settings section after loading", async () => { mockAllApis(); renderAdmin(); await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); // Guest access toggle (should be off) const toggle = screen.getByTestId("guest-access-toggle"); expect(toggle).toHaveAttribute("aria-checked", "false"); // Default endpoint should be selected const endpointSelect = screen.getByTestId( "default-endpoint-select", ) as HTMLSelectElement; expect(endpointSelect.value).toBe("ep1"); // Token budgets const daily = screen.getByTestId("budget-daily") as HTMLInputElement; expect(daily.value).toBe("100000"); const monthly = screen.getByTestId("budget-monthly") as HTMLInputElement; expect(monthly.value).toBe("2000000"); }); it("toggles guest access and saves", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("guest-access-toggle")).toBeInTheDocument(); }); await user.click(screen.getByTestId("guest-access-toggle")); await waitFor(() => { expect(client.admin.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ guest_access_enabled: true }), ); }); }); it("renders system stats", async () => { mockAllApis(); renderAdmin(); await waitFor(() => { expect(screen.getByText("System Stats")).toBeInTheDocument(); }); expect(screen.getByTestId("stat-total-runs")).toHaveTextContent("1,234"); expect(screen.getByTestId("stat-total-experiments")).toHaveTextContent("56"); expect(screen.getByTestId("stat-cache-hit-rate")).toHaveTextContent("73.0%"); expect(screen.getByTestId("stat-tokens-spent")).toHaveTextContent("456,789"); }); it("renders API keys and revoke works", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByText("API Keys")).toBeInTheDocument(); }); expect(screen.getByText("CI Pipeline")).toBeInTheDocument(); expect(screen.getByText(/pl_abc/)).toBeInTheDocument(); // Revoke key await user.click(screen.getByTestId("revoke-key-key1")); await waitFor(() => { expect(client.admin.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ action: "revoke_api_key", key_id: "key1", }), ); }); // Key should be removed from UI await waitFor(() => { expect(screen.queryByText("CI Pipeline")).not.toBeInTheDocument(); }); }); it("generates a new API key", async () => { mockAllApis(); vi.spyOn(client.admin, "updateSettings").mockResolvedValue({ api_key: { id: "key2", label: "Dev Key", prefix: "pl_xyz", created_at: "2026-04-07T12:00:00Z", }, } as unknown as Record); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("api-key-label")).toBeInTheDocument(); }); await user.type(screen.getByTestId("api-key-label"), "Dev Key"); await user.click(screen.getByTestId("generate-key-btn")); await waitFor(() => { expect(client.admin.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ action: "generate_api_key", label: "Dev Key", }), ); }); await waitFor(() => { expect(screen.getByText("Dev Key")).toBeInTheDocument(); }); }); it("renders webhooks and supports delete", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByText("Webhooks")).toBeInTheDocument(); }); expect(screen.getByText("1 webhook configured")).toBeInTheDocument(); expect(screen.getByText("run.completed")).toBeInTheDocument(); expect(screen.getByText("https://example.com/hook")).toBeInTheDocument(); // Delete webhook await user.click(screen.getByTestId("delete-webhook-wh1")); await waitFor(() => { expect(client.webhooks.delete).toHaveBeenCalledWith("wh1"); }); await waitFor(() => { expect( screen.queryByText("https://example.com/hook"), ).not.toBeInTheDocument(); }); }); it("creates a new webhook", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("add-webhook-btn")).toBeInTheDocument(); }); // Open form await user.click(screen.getByTestId("add-webhook-btn")); await waitFor(() => { expect(screen.getByTestId("webhook-form")).toBeInTheDocument(); }); await user.type( screen.getByTestId("webhook-url-input"), "https://new.com/hook", ); await user.click(screen.getByTestId("create-webhook-btn")); await waitFor(() => { expect(client.webhooks.create).toHaveBeenCalledWith({ event_type: "run.started", url: "https://new.com/hook", }); }); // New webhook should appear await waitFor(() => { expect(screen.getByText("https://new.com/hook")).toBeInTheDocument(); }); }); it("retry button reloads data after error", async () => { const getSettingsSpy = vi .spyOn(client.admin, "getSettings") .mockRejectedValueOnce(new client.ApiError(500, "Error", {})) .mockResolvedValueOnce(MOCK_SETTINGS as unknown as Record); vi.spyOn(client.admin, "getStats") .mockRejectedValueOnce(new client.ApiError(500, "Error", {})) .mockResolvedValueOnce(MOCK_STATS as unknown as Record); vi.spyOn(client.endpoints, "list") .mockRejectedValueOnce(new client.ApiError(500, "Error", {})) .mockResolvedValueOnce({ items: MOCK_ENDPOINTS, total: MOCK_ENDPOINTS.length, }); vi.spyOn(client.webhooks, "list") .mockRejectedValueOnce(new client.ApiError(500, "Error", {})) .mockResolvedValueOnce({ items: MOCK_WEBHOOKS, total: MOCK_WEBHOOKS.length, }); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByRole("alert")).toBeInTheDocument(); }); await user.click(screen.getByText("Retry")); await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); expect(getSettingsSpy).toHaveBeenCalledTimes(2); }); it("shows empty API keys message when none exist", async () => { vi.spyOn(client.admin, "getSettings").mockResolvedValue({ ...MOCK_SETTINGS, api_keys: [], } as unknown as Record); vi.spyOn(client.admin, "getStats").mockResolvedValue( MOCK_STATS as unknown as Record, ); vi.spyOn(client.endpoints, "list").mockResolvedValue({ items: MOCK_ENDPOINTS, total: MOCK_ENDPOINTS.length, }); vi.spyOn(client.webhooks, "list").mockResolvedValue({ items: [], total: 0, }); renderAdmin(); await waitFor(() => { expect( screen.getByText("No API keys generated yet."), ).toBeInTheDocument(); }); }); it("updates default endpoint selection", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("default-endpoint-select")).toBeInTheDocument(); }); await user.selectOptions( screen.getByTestId("default-endpoint-select"), "ep2", ); await waitFor(() => { expect(client.admin.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ default_endpoint_id: "ep2" }), ); }); }); it("updates token budget values", async () => { mockAllApis(); const user = userEvent.setup(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("budget-daily")).toBeInTheDocument(); }); const dailyInput = screen.getByTestId("budget-daily"); await user.clear(dailyInput); await user.type(dailyInput, "50000"); await waitFor(() => { expect(client.admin.updateSettings).toHaveBeenCalled(); }); }); it("shows no webhooks configured message when empty", async () => { vi.spyOn(client.admin, "getSettings").mockResolvedValue( MOCK_SETTINGS as unknown as Record, ); vi.spyOn(client.admin, "getStats").mockResolvedValue( MOCK_STATS as unknown as Record, ); vi.spyOn(client.endpoints, "list").mockResolvedValue({ items: MOCK_ENDPOINTS, total: MOCK_ENDPOINTS.length, }); vi.spyOn(client.webhooks, "list").mockResolvedValue({ items: [], total: 0, }); renderAdmin(); await waitFor(() => { expect( screen.getByText("No webhooks configured."), ).toBeInTheDocument(); }); }); it("shows storage usage formatted correctly", async () => { mockAllApis(); renderAdmin(); await waitFor(() => { expect(screen.getByTestId("stat-storage-usage")).toHaveTextContent( "50.0 MB", ); }); }); });