import { screen, waitFor, within } from "@testing-library/react"; import { CONFIG_PATH, DATABASE, MANAGED_FILE, contentArea, renderPage, renderRoute, stubApi } from "./testFixtures"; /** * Authority is three-state, and the difference between the states is the whole * point: an unresolved status is never treated as database mode, and file mode * is a different rendering rather than the same forms with their controls * turned off. */ const NEVER = new Promise(() => {}); /** * Every button the file-mode configuration content is allowed to contain: the * runtime actions, and the Retry an error surface offers. Anything else is a * mutation control that should not have been rendered at all. */ const RUNTIME_BUTTONS = ["Update now", "Reload certificates", "Retry"]; afterEach(() => { vi.unstubAllGlobals(); }); function mutationControls(): Element[] { return [ ...contentArea().querySelectorAll( 'input, textarea, select, [role="combobox"], [role="checkbox"], [contenteditable]', ), ]; } function buttonLabels(): string[] { return [...contentArea().querySelectorAll("button")].map((button) => (button.textContent ?? "").trim()); } test("a pending status renders neither form nor definition list on Protection", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } }); await renderPage("/configuration/protection", "Protection"); expect(await screen.findByText(/which configuration source this server obeys/i)).toBeTruthy(); expect(mutationControls()).toHaveLength(0); expect(screen.queryByRole("button", { name: "Create" })).toBeNull(); }); test("a pending status still leaves the runtime actions live (R3-4)", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } }); await renderPage("/configuration/system", "System"); const reload = (await screen.findByRole("button", { name: "Reload certificates" })) as HTMLButtonElement; expect(reload.disabled).toBe(false); // The settings form is not drawn behind it. expect(screen.queryByRole("button", { name: "Save" })).toBeNull(); }); test("a pending status renders neither form nor definition list on Resolution", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } }); await renderPage("/configuration/resolution", "Resolution"); expect(await screen.findByText(/which configuration source this server obeys/i)).toBeTruthy(); expect(mutationControls()).toHaveLength(0); expect(screen.queryByRole("button", { name: "Add upstream" })).toBeNull(); }); test("a failed status renders the error and a Retry, never editable forms", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) }, }); await renderPage("/configuration/protection", "Protection"); await screen.findByText(/cannot say whether a file or the database owns/i); expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); expect(mutationControls()).toHaveLength(0); }); test("a failed status renders the error and a Retry on Resolution too", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) }, }); await renderPage("/configuration/resolution", "Resolution"); await screen.findByText(/cannot say whether a file or the database owns/i); expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); expect(mutationControls()).toHaveLength(0); expect(screen.queryByRole("button", { name: "Add upstream" })).toBeNull(); }); test("a failed status keeps Update now enabled on the Sources tab", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) }, }); await renderPage("/configuration/protection?tab=sources", "Protection"); await screen.findByText(/cannot say whether a file or the database owns/i); const update = screen.getByRole("button", { name: "Update now" }) as HTMLButtonElement; expect(update.disabled).toBe(false); }); test("a failed status is announced by the shell on a page that is not configuration", async () => { stubApi(DATABASE, { responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }), "GET /api/stats?period=24h": { period: "24h", since: 0, until: 86400, queries: 0, blocked: 0, clients: 0, avg_response_time_us: null, coverage: { complete: true, available_since: 0 }, }, }, }); renderRoute("/overview"); await waitFor(() => expect(screen.getByText(/file authority and pending restarts cannot be shown/i)).toBeTruthy()); }); test("database authority renders the editable groups form", async () => { stubApi(DATABASE); await renderPage("/configuration/protection", "Protection"); expect(await screen.findByRole("button", { name: "Create" })).toBeTruthy(); expect(screen.getByLabelText("New group")).toBeTruthy(); expect(screen.queryByText(/loaded from/i)).toBeNull(); }); test("file authority renders definitions with their exact ZON keys, not a form", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/protection", "Protection"); const note = await screen.findByText(/are loaded from/i); expect(note.textContent).toContain(CONFIG_PATH); const detail = await screen.findByText("Safe search"); expect(within(detail).getByText("groups[].safe_search")).toBeTruthy(); expect(screen.queryByRole("button", { name: "Create" })).toBeNull(); }); test("file mode: Protection renders zero mutation controls (D12)", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/protection", "Protection"); await screen.findByText("groups[].safe_search"); expect(mutationControls()).toHaveLength(0); for (const label of buttonLabels()) expect(RUNTIME_BUTTONS).toContain(label); }); test("file mode: the Sources tab shows the catalogue with its key and only Update now", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/protection?tab=sources", "Protection"); await screen.findByText("blocklist_sources"); expect(mutationControls()).toHaveLength(0); expect(buttonLabels()).toEqual(["Update now"]); }); test("file mode: the catalogue keeps provenance and both skipped counters", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/protection?tab=sources", "Protection"); await screen.findByText("blocklist_sources"); // Trackers is the suggested source, and the one with skipped lines: hiding // either fact under file authority would hide a parse failure. const row = screen.getByText("Trackers").closest("tr")!; expect(within(row).getByText("Suggested")).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "Skipped regex" })).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "Skipped unsupported" })).toBeTruthy(); const cells = [...row.querySelectorAll("td")].map((cell) => cell.textContent); expect(cells).toContain("3"); expect(cells).toContain("4"); expect( screen.getByText(/Skipped unsupported lines are syntax nxdns cannot translate into a DNS decision/), ).toBeTruthy(); }); test("file mode: the Resolution upstream pool is a table with no controls", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/resolution", "Resolution"); await screen.findByText("upstreams"); expect(mutationControls()).toHaveLength(0); expect(buttonLabels()).toEqual([]); }); test("file mode: the Records tab is a table with no controls", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/resolution?tab=records", "Resolution"); await screen.findByText("local_records"); expect(mutationControls()).toHaveLength(0); expect(buttonLabels()).toEqual([]); }); test("file mode: the Forward zones tab is a table with no controls", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/resolution?tab=zones", "Resolution"); await screen.findByText("forward_zones"); expect(mutationControls()).toHaveLength(0); expect(buttonLabels()).toEqual([]); }); test("file mode: System renders scalars as definitions and keeps Reload certificates", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/system", "System"); const retention = await screen.findByText("Retention days"); expect(within(retention).getByText("logging.retention_days")).toBeTruthy(); expect(mutationControls()).toHaveLength(0); expect(buttonLabels()).toEqual(["Reload certificates"]); }); test("file mode: authentication is a derived status with no invented key (D13)", async () => { stubApi(MANAGED_FILE); await renderPage("/configuration/system", "System"); const auth = await screen.findByText("Authentication"); expect(auth.textContent).toBe("Authentication"); expect(within(auth).queryByText(/web\.auth_enabled/)).toBeNull(); expect(screen.queryByText("web.auth_enabled")).toBeNull(); });