Files
nxdns/admin/src/features/configuration/authority.test.tsx
T
mokhtar 207252acee admin: enable toggles become rac switches
the upstream, blocklist source, and safe search enable controls mutate the row the moment they move, so they now carry the switch role via a shared drawn rac switch with a 44px hit area, a focus-visible ring, and naming modes made exclusive by a discriminated union. safe search supersedes its one-commit-old checkbox form, and the file-authority guard now names the switch role instead of relying on the bare input selector.
2026-08-29 12:40:01 +02:00

226 lines
8.9 KiB
TypeScript

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<Response>(() => {});
/**
* 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"], [role="switch"], [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/overview?period=24h": {
period: "24h",
since: 0,
until: 86400,
bucket_seconds: 1800,
totals: { queries: 0, blocked: 0, clients: 0, avg_response_time_us: null },
buckets: [],
clients: [],
other: [],
types: [],
routes: [],
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();
});