import { describe, it, expect, vi } from "vitest"; import { buildEvidence, type CapturedPage } from "../src/capture/capture.js"; import { createAud0Emitter, noopAud0 } from "../src/audit/aud0.js"; import { startScanWorker } from "../src/worker/scan-worker.js"; import type { PoolLike } from "../src/services/registries.js"; import type { FastifyBaseLogger } from "fastify"; const PAGE: CapturedPage = { finalUrl: "https://example.com/", status: 200, contentType: "text/html", title: "Example", description: "desc", lang: "en", charset: "UTF-8", viewportMeta: "width=device-width", domCounts: { script: 3, img: 2, a: 10 }, stylesheets: ["https://example.com/app.css"], images: ["https://example.com/logo.png"], computedStyle: { body: { "font-family": "sans-serif" }, h1: null }, networkRequests: [{ url: "https://example.com/", method: "GET", resourceType: "document", status: 200 }], screenshotBase64: "abc", }; describe("capture evidence builder", () => { it("produces measured EvidenceRecords per kind", () => { const { evidence, summary } = buildEvidence("reg-1", PAGE); const kinds = evidence.map((e) => e.kind); expect(kinds).toEqual( expect.arrayContaining(["runtime-html", "dom", "computed-style", "stylesheet", "asset", "network-request", "screenshot"]), ); for (const e of evidence) { expect(e.confidence).toBe("measured"); expect(e.targetId).toBe("reg-1"); expect(e.source.tool).toContain("playwright-core"); expect(e.method).toBe("deterministic-browser-capture"); expect(e.capturedAt).toBeTruthy(); } expect(summary).toMatchObject({ url: "https://example.com/", status: 200, domElementCount: 15, stylesheetCount: 1, screenshotCaptured: true, }); }); it("omits screenshot evidence when capture missed it (evidence gap explicit)", () => { const { evidence, summary } = buildEvidence("reg-1", { ...PAGE, screenshotBase64: null }); expect(evidence.some((e) => e.kind === "screenshot")).toBe(false); expect(summary.screenshotCaptured).toBe(false); }); }); describe("aud0 emitter", () => { const base = { event_type: "c0py.scan_completed", tenant_id: "org-1", actor_id: "u1", app_id: "c0py", capability_id: "c0py.scan_run", resource_type: "scan", resource_id: "s1", risk_level: "L2" as const, }; it("no-op when unconfigured", () => { const fetchImpl = vi.fn(); const emit = createAud0Emitter({ fetchImpl: fetchImpl as unknown as typeof fetch }); emit(base); expect(fetchImpl).not.toHaveBeenCalled(); }); it("posts event with tenant header (fire-and-forget)", async () => { const fetchImpl = vi.fn().mockResolvedValue(new Response("{}", { status: 200 })); const emit = createAud0Emitter({ baseUrl: "https://aud0.siax.io", authToken: "tok", fetchImpl: fetchImpl as unknown as typeof fetch, }); emit(base); await new Promise((r) => setTimeout(r, 10)); expect(fetchImpl).toHaveBeenCalledWith( "https://aud0.siax.io/v1/aud0/events", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "x-tenant-id": "org-1" }), }), ); }); it("never throws on network failure (advisory)", async () => { const fetchImpl = vi.fn().mockRejectedValue(new Error("down")); const emit = createAud0Emitter({ baseUrl: "https://aud0.siax.io", authToken: "tok", fetchImpl: fetchImpl as unknown as typeof fetch, }); expect(() => emit(base)).not.toThrow(); await new Promise((r) => setTimeout(r, 10)); }); it("noop emitter accepts anything", () => { expect(() => noopAud0(base)).not.toThrow(); }); }); describe("scan worker", () => { const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as unknown as FastifyBaseLogger; function dbWith(queries: { text: string; rows: Record[] }[]) { const calls: string[] = []; const db: PoolLike = { query: (text) => { calls.push(text); const row = queries.find((q) => text.includes(q.text)); return Promise.resolve({ rows: row?.rows ?? [] }); }, }; return { db, calls }; } it("claims, captures, completes and emits AUD0", async () => { const { db } = dbWith([ { text: "UPDATE scans SET status = 'running'", rows: [{ id: "s1", registry_id: "r1", owner_sub: "u1", tenant_id: "org-1" }] }, { text: "SELECT url FROM registries", rows: [{ url: "https://example.com" }] }, { text: "UPDATE scans SET status = 'completed'", rows: [] }, ]); const aud0 = vi.fn(); const worker = startScanWorker({ db, logger, aud0: aud0 as never, enabled: false, capture: async (url) => { expect(url).toBe("https://example.com"); return buildEvidence("scan", PAGE); }, }); const did = await worker.processOne(); expect(did).toBe(true); expect(aud0).toHaveBeenCalledWith( expect.objectContaining({ event_type: "c0py.scan_completed", tenant_id: "org-1", risk_level: "L2" }), ); }); it("marks scan failed and emits c0py.scan_failed on capture error", async () => { const { db } = dbWith([ { text: "UPDATE scans SET status = 'running'", rows: [{ id: "s1", registry_id: "r1", owner_sub: "u1", tenant_id: "org-1" }] }, { text: "SELECT url FROM registries", rows: [] }, { text: "UPDATE scans SET status = 'failed'", rows: [] }, ]); const aud0 = vi.fn(); const worker = startScanWorker({ db, logger, aud0: aud0 as never, enabled: false, capture: async () => { throw new Error("boom"); }, }); const did = await worker.processOne(); expect(did).toBe(true); expect(aud0).toHaveBeenCalledWith( expect.objectContaining({ event_type: "c0py.scan_failed" }), ); }); it("returns false when no pending scan", async () => { const { db } = dbWith([{ text: "UPDATE scans SET status = 'running'", rows: [] }]); const worker = startScanWorker({ db, logger, aud0: noopAud0, enabled: false, capture: async () => buildEvidence("s", PAGE) }); const did = await worker.processOne(); expect(did).toBe(false); }); });