import { Toasty } from "@cloudflare/kumo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { userEvent } from "vitest/browser"; import type { ApiTokenCreateResult, ApiTokenInfo, CreateApiTokenInput, } from "../../../src/lib/api/api-tokens.js"; import { render } from "../../utils/render.js"; const mockFetchApiTokens = vi.fn<() => Promise>(); const mockCreateApiToken = vi.fn<(input: CreateApiTokenInput) => Promise>(); const mockRevokeApiToken = vi.fn<(id: string) => Promise>(); const mockFetchPlugins = vi.fn(); vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); return { ...actual, Link: ({ children, to, ...props }: any) => ( {children} ), }; }); vi.mock("../../../src/lib/api/api-tokens.js", async () => { const actual = await vi.importActual("../../../src/lib/api/api-tokens.js"); return { ...actual, fetchApiTokens: () => mockFetchApiTokens(), createApiToken: (input: CreateApiTokenInput) => mockCreateApiToken(input), revokeApiToken: (id: string) => mockRevokeApiToken(id), }; }); vi.mock("../../../src/lib/api/plugins.js", async () => { const actual = await vi.importActual("../../../src/lib/api/plugins.js"); return { ...actual, fetchPlugins: () => mockFetchPlugins(), }; }); const { ApiTokenSettings } = await import("../../../src/components/settings/ApiTokenSettings.js"); const token: ApiTokenInfo = { id: "token-1", name: "CI token", prefix: "emd_abc", scopes: ["content:read"], userId: "user-1", expiresAt: "2027-01-01T00:00:00.000Z", lastUsedAt: null, createdAt: "2026-01-01T00:00:00.000Z", }; const createdToken: ApiTokenCreateResult = { token: "emd_secret-token-value", info: token, }; function Wrapper({ children }: { children: React.ReactNode }) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return ( {children} ); } async function renderApiTokenSettings() { return render(, { wrapper: Wrapper }); } beforeEach(() => { vi.clearAllMocks(); mockFetchApiTokens.mockResolvedValue([]); mockFetchPlugins.mockResolvedValue([]); mockCreateApiToken.mockResolvedValue(createdToken); mockRevokeApiToken.mockResolvedValue(undefined); }); describe("ApiTokenSettings", () => { it("shows the shared frame while tokens load", async () => { mockFetchApiTokens.mockReturnValue(new Promise(() => undefined)); const screen = await renderApiTokenSettings(); await expect .element(screen.getByRole("heading", { name: "API Tokens", level: 1 })) .toBeInTheDocument(); await expect.element(screen.getByText("Loading...")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Create Token" }).query()).toBeNull(); }); it("shows a load failure without token actions", async () => { mockFetchApiTokens.mockRejectedValue(new Error("Token service unavailable")); const screen = await renderApiTokenSettings(); await expect.element(screen.getByRole("alert")).toHaveTextContent("An error occurred"); await expect.element(screen.getByRole("alert")).toHaveTextContent("Token service unavailable"); expect(screen.getByRole("button", { name: "Create Token" }).query()).toBeNull(); }); it("shows the empty state and opens the creation form", async () => { const screen = await renderApiTokenSettings(); await expect .element(screen.getByText("No API tokens yet. Create one to get started.")) .toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Create Token" })); await expect.element(screen.getByRole("textbox", { name: "Token Name" })).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Create Token" })).toBeDisabled(); }); it("creates a token and preserves its one-time reveal flow", async () => { const clipboardWrite = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); const screen = await renderApiTokenSettings(); await userEvent.click(screen.getByRole("button", { name: "Create Token" })); await screen.getByRole("textbox", { name: "Token Name" }).fill(" CI token "); await userEvent.click(screen.getByRole("checkbox", { name: /Content Read/ })); await userEvent.click(screen.getByRole("button", { name: "Create Token" })); await vi.waitFor(() => { expect(mockCreateApiToken).toHaveBeenCalledWith( expect.objectContaining({ name: "CI token", scopes: ["content:read"], expiresAt: expect.any(String), }), ); }); await expect.element(screen.getByText("Token created: CI token")).toBeInTheDocument(); expect(screen.getByText("emd_secret-token-value").query()).toBeNull(); await userEvent.click(screen.getByRole("button", { name: "Show token" })); await expect.element(screen.getByText("emd_secret-token-value")).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Hide token" })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Copy token" })); expect(clipboardWrite).toHaveBeenCalledWith("emd_secret-token-value"); await expect.element(screen.getByText("Copied to clipboard")).toBeInTheDocument(); clipboardWrite.mockRestore(); }); it("keeps the one-time token visible when the token list refetch fails", async () => { mockFetchApiTokens .mockResolvedValueOnce([]) .mockRejectedValueOnce(new Error("Token list refetch failed")); const screen = await renderApiTokenSettings(); await userEvent.click(screen.getByRole("button", { name: "Create Token" })); await screen.getByRole("textbox", { name: "Token Name" }).fill("CI token"); await userEvent.click(screen.getByRole("checkbox", { name: /Content Read/ })); await userEvent.click(screen.getByRole("button", { name: "Create Token" })); await vi.waitFor(() => expect(mockFetchApiTokens.mock.calls.length).toBeGreaterThanOrEqual(2)); await expect.element(screen.getByText("Token created: CI token")).toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Show token" })).toBeInTheDocument(); expect(screen.getByRole("alert").query()).toBeNull(); }); it("requires confirmation before revoking a token", async () => { mockFetchApiTokens.mockResolvedValue([token]); const screen = await renderApiTokenSettings(); await expect.element(screen.getByText("CI token")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Revoke token CI token" })); await expect.element(screen.getByRole("heading", { name: "Revoke?" })).toBeInTheDocument(); expect(mockRevokeApiToken).not.toHaveBeenCalled(); screen.getByRole("button", { name: "Confirm" }).element().click(); await vi.waitFor(() => { expect(mockRevokeApiToken).toHaveBeenCalledWith("token-1"); }); }); it("keeps the revoke dialog open and displays mutation errors", async () => { mockFetchApiTokens.mockResolvedValue([token]); mockRevokeApiToken.mockRejectedValue(new Error("Token is still in use")); const screen = await renderApiTokenSettings(); await expect.element(screen.getByText("CI token")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Revoke token CI token" })); screen.getByRole("button", { name: "Confirm" }).element().click(); await expect.element(screen.getByRole("alert")).toHaveTextContent("Token is still in use"); await expect.element(screen.getByRole("heading", { name: "Revoke?" })).toBeInTheDocument(); }); });