Files
nxdns/admin/src/features/diagnostics/DiagnosticDetailPage.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

224 lines
8.5 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { DIAGNOSTIC_CODES, type DiagnosticEvent } from "@/lib/types";
import { health } from "@/lib/healthFixture";
import { EVENT_COPY } from "./eventCopy";
const NOW_S = Math.floor(Date.now() / 1000);
function event(overrides: Partial<DiagnosticEvent> = {}): DiagnosticEvent {
return {
id: 42,
code: "blocklist.refresh",
component: "blocklist",
subject: "StevenBlack",
severity: "warning",
first_seen: NOW_S - 7200,
last_seen: NOW_S - 600,
occurrences: 4,
resolved_at: null,
detail: "download failed: ConnectionTimedOut",
...overrides,
};
}
/** A 204: what `DELETE /api/diagnostics/{id}` answers on a purge. */
const NO_CONTENT = Symbol("204");
let responses: Record<string, unknown>;
let requested: string[];
beforeEach(() => {
requested = [];
responses = {
"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
// The shell reads health for the Diagnostics nav badge on every route.
"/api/health": health(),
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: { method?: string }) => {
const url = String(input);
const method = init?.method ?? "GET";
const key = method === "GET" ? url : `${method} ${url}`;
requested.push(key);
const payload = responses[key];
if (payload === undefined)
return new Response(JSON.stringify({ error: "no such event" }), {
status: 404,
headers: { "content-type": "application/json" },
});
if (payload === NO_CONTENT) return new Response(null, { status: 204 });
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "content-type": "application/json" },
});
}),
);
});
afterEach(() => {
vi.unstubAllGlobals();
});
/**
* `retry` is off in the failure test: the shared client backs 5xx off for
* seconds, which the render assertions would sit through for nothing.
*/
function renderDetail(id: number, { retry = true } = {}) {
const queryClient = createQueryClient();
if (!retry) {
const defaults = queryClient.getDefaultOptions();
queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } });
}
const router = createAppRouter(createMemoryHistory({ initialEntries: [`/diagnostics/${id}`] }), queryClient);
render(
<AuthProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</AuthProvider>,
);
return router;
}
test("an open episode shows its facts, its copy and the error the server sent", async () => {
responses["/api/diagnostics/42"] = event();
renderDetail(42);
await screen.findByRole("heading", { name: "Blocklist source failed to update" });
expect(screen.getByText("Warning")).toBeTruthy();
expect(screen.getByText("StevenBlack")).toBeTruthy();
expect(screen.getByText("Active for 2h")).toBeTruthy();
expect(screen.getByText("Not yet — still failing")).toBeTruthy();
expect(screen.getByText("4")).toBeTruthy();
expect(screen.getByText("blocklist.refresh")).toBeTruthy();
expect(screen.getByText(EVENT_COPY["blocklist.refresh"].impact)).toBeTruthy();
expect(screen.getByText(EVENT_COPY["blocklist.refresh"].remediation)).toBeTruthy();
expect(screen.getByText("download failed: ConnectionTimedOut")).toBeTruthy();
expect(screen.getByRole("link", { name: "Go to Blocklist sources" }).getAttribute("href")).toBe(
"/configuration/protection?tab=sources",
);
});
test("a resolved episode states how long it lasted, not how long it has run", async () => {
responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
renderDetail(7);
await screen.findByRole("heading", { name: "Blocklist source failed to update" });
expect(screen.getByText("Resolved after 1h")).toBeTruthy();
expect(screen.queryByText("Not yet — still failing")).toBeNull();
});
test("an open episode offers no purge", async () => {
responses["/api/diagnostics/42"] = event();
renderDetail(42);
await screen.findByRole("heading", { name: "Blocklist source failed to update" });
expect(screen.queryByRole("button", { name: "Purge" })).toBeNull();
});
test("purging a resolved episode asks first, then returns to the list", async () => {
responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
responses["DELETE /api/diagnostics/7"] = NO_CONTENT;
responses["/api/diagnostics?state=active"] = { events: [], next_before: null, active: { warnings: 0, errors: 0 } };
responses["/api/diagnostics?state=resolved"] = {
events: [],
next_before: null,
active: { warnings: 0, errors: 0 },
};
const router = renderDetail(7);
await screen.findByRole("heading", { name: "Blocklist source failed to update" });
fireEvent.click(screen.getByRole("button", { name: "Purge" }));
const dialog = await screen.findByRole("alertdialog");
expect(dialog.textContent).toContain("Purge this resolved event? Its history is gone for good.");
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
expect(requested).not.toContain("DELETE /api/diagnostics/7");
fireEvent.click(screen.getByRole("button", { name: "Purge" }));
fireEvent.click(within(await screen.findByRole("alertdialog")).getByRole("button", { name: "Purge" }));
await waitFor(() => expect(requested).toContain("DELETE /api/diagnostics/7"));
// The row it was showing no longer exists, so the page it navigates to is
// the list rather than a 404 of its own.
await waitFor(() => expect(router.state.location.pathname).toBe("/diagnostics"));
});
test("a refused purge stays on the event and shows why", async () => {
responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: { method?: string }) => {
if (init?.method === "DELETE")
return new Response(JSON.stringify({ error: "the event is still active" }), {
status: 409,
headers: { "content-type": "application/json" },
});
return new Response(JSON.stringify(responses[String(input)] ?? {}), {
status: 200,
headers: { "content-type": "application/json" },
});
}),
);
const router = renderDetail(7, { retry: false });
await screen.findByRole("heading", { name: "Blocklist source failed to update" });
fireEvent.click(screen.getByRole("button", { name: "Purge" }));
fireEvent.click(within(await screen.findByRole("alertdialog")).getByRole("button", { name: "Purge" }));
const alert = await screen.findByRole("alert");
expect(alert.textContent).toContain("the event is still active");
expect(router.state.location.pathname).toBe("/diagnostics/7");
});
test("every code renders its own title, impact and remediation", async () => {
for (const [index, code] of DIAGNOSTIC_CODES.entries()) {
const id = 100 + index;
responses[`/api/diagnostics/${id}`] = event({ id, code, component: code.slice(0, code.indexOf(".")) });
renderDetail(id);
const copy = EVENT_COPY[code];
await screen.findByRole("heading", { name: copy.title });
expect(screen.getByText(copy.impact), code).toBeTruthy();
expect(screen.getByText(copy.remediation), code).toBeTruthy();
screen.getByText(code);
cleanup();
}
});
test("an event retention has removed shows the server's message, not an empty page", async () => {
renderDetail(999);
await screen.findByText("no such event");
expect(screen.getByRole("link", { name: "← All diagnostics" })).toBeTruthy();
});
test("an unavailable store reports the failure instead of loading forever", async () => {
responses["/api/diagnostics/42"] = event();
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) =>
String(input).startsWith("/api/diagnostics/")
? new Response(JSON.stringify({ error: "store unavailable" }), {
status: 503,
headers: { "content-type": "application/json" },
})
: new Response(JSON.stringify(responses[String(input)] ?? {}), {
status: 200,
headers: { "content-type": "application/json" },
}),
),
);
renderDetail(42, { retry: false });
const alert = await screen.findByRole("alert");
expect(alert.textContent).toContain("The server is starting or degraded.");
expect(screen.queryByText("Loading event…")).toBeNull();
expect(screen.getByRole("link", { name: "← All diagnostics" })).toBeTruthy();
});