Files
nxdns/admin/src/features/configuration/authority.test.tsx
T
mokhtar 7e0df5fd94
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
milestone 32: task-shaped configuration, file mode as a rendering, config status api
2026-08-22 22:42:50 +02:00

223 lines
8.7 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"], [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();
});