import { 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 type { QueryDetail } from "@/lib/types"; import { provenance } from "@/features/provenance/provenanceFixture"; import { health } from "@/lib/healthFixture"; function detail(id: number, sections: Parameters[0] = {}): QueryDetail { return { id, ...provenance(sections) }; } let responses: Record; beforeEach(() => { responses = { "/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 }, "/api/health": health(), }; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const payload = responses[String(input)]; if (payload === undefined) { return new Response(JSON.stringify({ error: "no such query" }), { status: 404, headers: { "content-type": "application/json" }, }); } return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" }, }); }), ); }); afterEach(() => vi.unstubAllGlobals()); function renderDetail(id: number, search = "") { const queryClient = createQueryClient(); const router = createAppRouter( createMemoryHistory({ initialEntries: [`/activity/queries/${id}${search}`] }), queryClient, ); render( , ); return router; } /** The search parameters a link carries, so an assertion states them by name. */ function hrefSearch(link: HTMLElement): Record { const query = link.getAttribute("href")?.split("?")[1] ?? ""; return Object.fromEntries(new URLSearchParams(query)); } /** The value beside a term, so a section's facts are read as pairs. */ function factValue(label: string): string { const term = screen.getByText(label); const value = term.nextElementSibling; return value?.textContent ?? ""; } /** The line under the domain, which is what a page is read as at a glance. */ function subtitle(domain: string): string { const heading = screen.getByRole("heading", { name: domain }); return heading.nextElementSibling?.textContent ?? ""; } test("a blocked query explains itself in the six sections, in pipeline order", async () => { responses["/api/queries/42"] = detail(42, { request: { time: 1_700_000_000, domain: "ads.example", client: "192.0.2.11", qtype: 28, qclass: 1 }, group: { id: 3, name: "kids" }, policy: { action: "block", reason: "blocklist_wildcard", matched: "||tracker.example^", source_id: 5, source_name: "StevenBlack", }, rewrites: { cname_target: "cdn.tracker.example" }, route: { kind: "blocked", upstream: "" }, response: { rcode: 0, duration_us: 1234 }, }); renderDetail(42); await screen.findByRole("heading", { name: "ads.example" }); const headings = screen.getAllByRole("heading", { level: 2 }).map((node) => node.textContent); expect(headings).toEqual(["Request", "Group", "Policy", "Rewrites", "Route", "Response", "Related"]); expect(factValue("Client")).toBe("192.0.2.11"); expect(factValue("Type")).toBe("AAAA"); expect(factValue("Class")).toBe("IN (1)"); expect(factValue("Name")).toBe("kids"); expect(factValue("Id")).toBe("3"); expect(factValue("Decision")).toBe("Blocked"); expect(factValue("Reason")).toBe("Blocklist (wildcard)"); expect(factValue("Matched")).toBe("||tracker.example^"); expect(factValue("Blocklist")).toBe("StevenBlack (#5)"); expect(factValue("CNAME target")).toBe("cdn.tracker.example"); expect(factValue("Answered by")).toBe("Blocked locally"); expect(factValue("Upstream")).toBe("No upstream exchange"); expect(factValue("Result")).toBe("NOERROR (0)"); expect(factValue("Took")).toBe("1.2 ms"); // NOERROR is the ordinary case and adds nothing to the verdict. expect(subtitle("ads.example")).toMatch(/ — Blocked$/); }); test("an upstream SERVFAIL names the resolver that failed and the code the client saw", async () => { responses["/api/queries/7"] = detail(7, { request: { domain: "news.example" }, route: { kind: "upstream", upstream: "https://dns.example/dns-query" }, response: { rcode: 2, duration_us: null }, }); renderDetail(7); await screen.findByRole("heading", { name: "news.example" }); expect(factValue("Answered by")).toBe("Upstream resolver"); expect(factValue("Upstream")).toBe("https://dns.example/dns-query"); expect(factValue("Result")).toBe("SERVFAIL (2)"); expect(factValue("Took")).toBe("Not measured"); // The policy allowed the query; the client still got nothing, and the // headline has to say so rather than reading as a success. expect(subtitle("news.example")).toMatch(/ — Allowed — SERVFAIL \(2\)$/); }); test("a forward-zone answer says the matcher never ran, not that nothing matched", async () => { responses["/api/queries/14"] = detail(14, { request: { domain: "nas.lan.home" }, policy: { action: "allow", reason: "forward_zone", matched: "" }, route: { kind: "forward_zone", forward_zone: "lan.home", upstream: "udp://192.168.1.1:53" }, }); renderDetail(14); await screen.findByRole("heading", { name: "nas.lan.home" }); expect(factValue("Reason")).toBe("Forward zone"); expect(factValue("Matched")).toBe("The matcher never ran"); }); test("a query the matcher did evaluate keeps the honest empty verdict", async () => { responses["/api/queries/15"] = detail(15, { policy: { action: "allow", reason: "no_match", matched: "" }, }); renderDetail(15); await screen.findByRole("heading", { name: "example.com" }); expect(factValue("Reason")).toBe("No match"); expect(factValue("Matched")).toBe("Nothing matched"); }); test("empty text fields read as absent facts, never as blank values", async () => { responses["/api/queries/8"] = detail(8, { group: { id: null, name: "" }, policy: { action: "not_evaluated", reason: "paused", matched: "", source_id: null, source_name: "" }, route: { kind: "upstream", forward_zone: "", upstream: "udp://9.9.9.9:53" }, }); renderDetail(8); await screen.findByRole("heading", { name: "example.com" }); expect(factValue("Name")).toBe("No group recorded"); expect(factValue("Matched")).toBe("The matcher never ran"); expect(factValue("Blocklist")).toBe("Not a blocklist decision"); expect(factValue("Safe search")).toBe("No rewrite"); expect(factValue("Reason")).toBe("Filtering paused"); }); test("a log with hidden domains renders the server's marker, with nothing invented around it", async () => { responses["/api/queries/9"] = detail(9, { request: { domain: "hidden" }, policy: { action: "block", reason: "blocklist_domain", matched: "hidden", source_name: "StevenBlack" }, rewrites: { cname_target: "hidden", safe_search_target: "hidden" }, route: { kind: "blocked", upstream: "" }, }); renderDetail(9); await screen.findByRole("heading", { name: "hidden" }); expect(factValue("Domain")).toBe("hidden"); expect(factValue("Matched")).toBe("hidden"); expect(factValue("CNAME target")).toBe("hidden"); expect(factValue("Safe search")).toBe("hidden"); // The client is governed by its own flag and stays visible here. expect(factValue("Client")).toBe("192.0.2.10"); }); test("the related actions carry absolute bounds around the query, and the domain into the simulation", async () => { responses["/api/queries/11"] = detail(11, { request: { time: 1_700_000_000, domain: "shop.example", client: "192.0.2.12" }, }); renderDetail(11); await screen.findByRole("heading", { name: "shop.example" }); const related = screen.getByRole("heading", { name: "Related" }).parentElement!; expect( within(related) .getByRole("link", { name: /Test this domain/ }) .getAttribute("href"), ).toBe("/activity/test?domain=shop.example"); // No origin bound at all: five minutes either side of the query itself. expect(hrefSearch(within(related).getByRole("link", { name: "All activity for this domain" }))).toEqual({ mode: "history", domain: "shop.example", since: "1699999700", until: "1700000300", }); expect(hrefSearch(within(related).getByRole("link", { name: "All activity from this client" }))).toEqual({ mode: "history", client: "192.0.2.12", since: "1699999700", until: "1700000300", }); // The diagnostics window is the query's own moment, never the origin's. expect(hrefSearch(within(related).getByRole("link", { name: /Diagnostics around/ }))).toEqual({ since: "1699999700", until: "1700000300", }); }); test("an origin bound wins over the default window, one bound at a time", async () => { responses["/api/queries/17"] = detail(17, { request: { time: 1_700_000_000, domain: "shop.example", client: "192.0.2.12" }, }); renderDetail(17, "?mode=history&since=1600000000"); await screen.findByRole("heading", { name: "shop.example" }); const related = screen.getByRole("heading", { name: "Related" }).parentElement!; const link = hrefSearch(within(related).getByRole("link", { name: "All activity for this domain" })); expect(link["since"]).toBe("1600000000"); expect(link["until"]).toBe("1700000300"); }); test("both origin bounds carry through untouched", async () => { responses["/api/queries/18"] = detail(18, { request: { time: 1_700_000_000, domain: "shop.example" } }); renderDetail(18, "?mode=history&since=1600000000&until=1600000060"); await screen.findByRole("heading", { name: "shop.example" }); const related = screen.getByRole("heading", { name: "Related" }).parentElement!; const link = hrefSearch(within(related).getByRole("link", { name: "All activity for this domain" })); expect(link["since"]).toBe("1600000000"); expect(link["until"]).toBe("1600000060"); }); test("the back link restores the investigation the reader came from", async () => { responses["/api/queries/19"] = detail(19, { request: { domain: "shop.example" } }); renderDetail(19, "?mode=history&domain=shop&since=1600000000&blocked=true"); await screen.findByRole("heading", { name: "shop.example" }); expect(hrefSearch(screen.getByRole("link", { name: "← Activity" }))).toEqual({ mode: "history", domain: "shop", since: "1600000000", blocked: "true", }); }); function clientList(client: { ip: string; name: string; learned_name: string }) { return { clients: [ { id: 1, group_id: 1, group: "default", hand_edited: client.name !== "", first_seen: 1_700_000_000, last_seen: 1_700_000_100, ...client, }, ], }; } /** The related section, whose text is read whole because it is prose, not facts. */ async function relatedText(expected: string) { const related = screen.getByRole("heading", { name: "Related" }).parentElement!; await waitFor(() => expect(related.textContent?.replace(/\s+/g, " ")).toContain(expected)); } test("the record keeps the address the query came from, and Related carries the name it has now", async () => { responses["/api/queries/12"] = detail(12, { request: { domain: "shop.example", client: "192.0.2.12" } }); responses["/api/clients"] = clientList({ ip: "192.0.2.12", name: "Kids iPad", learned_name: "ipad.lan" }); renderDetail(12); await screen.findByRole("heading", { name: "shop.example" }); await relatedText("The client list currently names 192.0.2.12 “Kids iPad”."); expect(factValue("Client")).toBe("192.0.2.12"); }); test("a learned name is told as the reverse-DNS lookup it is, never as a recorded fact", async () => { responses["/api/queries/13"] = detail(13, { request: { client: "192.0.2.13" } }); responses["/api/clients"] = clientList({ ip: "192.0.2.13", name: "", learned_name: "printer.lan" }); renderDetail(13); await screen.findByRole("heading", { name: "example.com" }); await relatedText("Reverse DNS currently resolves 192.0.2.13 to printer.lan."); expect(factValue("Client")).toBe("192.0.2.13"); }); test("a row retention has pruned explains the 404 and keeps the way back to the log", async () => { renderDetail(404, "?mode=history&domain=gone"); await screen.findByRole("alert"); expect(screen.getByText(/no such query/)).toBeTruthy(); expect(hrefSearch(screen.getByRole("link", { name: "← Activity" }))).toEqual({ mode: "history", domain: "gone", }); }); /** The related-actions region of a query detail. */ function related(): HTMLElement { return screen.getByRole("region", { name: "Related" }); } /** * Related is links only. Pause is a resolver-wide control and lives in the * sidebar alone, so a blocked query — the case that used to carry one here — * offers no button of any kind. */ test("Related carries its four links and no control, blocked query or not", async () => { responses["/api/queries/50"] = detail(50, { policy: { action: "block", reason: "blocklist_domain", matched: "ads.example" }, route: { kind: "blocked", upstream: "" }, }); renderDetail(50); await screen.findByRole("heading", { name: "example.com" }); await waitFor(() => expect(within(related()).getByText("Diagnostics around this query")).toBeTruthy()); expect(within(related()).getAllByRole("link").map((link) => link.textContent)).toEqual([ "Test this domain against current policy", "All activity for this domain", "All activity from this client", "Diagnostics around this query", ]); expect(within(related()).queryByRole("button")).toBeNull(); });