import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { buildCollector, type CollectorApp } from "./app.js"; // The Training section's proxy: reviewer-gated, forwards a fixed set of paths to the // trainer's job API, passes its status codes through, and degrades cleanly when the trainer // is not configured or not reachable. The trainer is faked with a bare node http server. const REVIEWER = { user: "julian", pass: "review-pass-123" }; const basic = "Basic " + Buffer.from(`${REVIEWER.user}:${REVIEWER.pass}`).toString("base64"); let dir: string; let fake: Server; let fakeUrl: string; let seen: { method: string; url: string; body: string }[]; let app: CollectorApp; async function start(trainerUrl: string | null): Promise { app = await buildCollector({ host: "127.0.0.1", port: 0, dataDir: dir, boothTokens: new Map(), reviewer: REVIEWER, trainerUrl }, { dbFile: ":memory:" }); await app.ready(); } beforeEach(async () => { dir = await mkdtemp(path.join(tmpdir(), "collector-")); seen = []; fake = createServer((req: IncomingMessage, res: ServerResponse) => { let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { seen.push({ method: req.method ?? "", url: req.url ?? "", body }); const json = (code: number, obj: unknown) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); }; if (req.url === "/health") return json(200, { ok: true, busy: false }); if (req.url === "/readiness") return json(200, { ready: false, labelled: { total: 3 } }); if (req.url === "/versions") return json(200, { versions: [{ version: "v1", written: true }] }); if (req.url === "/jobs" && req.method === "GET") return json(200, { jobs: [{ id: "j1" }], current: null }); if (req.url === "/jobs" && req.method === "POST") return body.includes('"busy"') ? json(409, { error: "a job is already running" }) : json(202, { id: "j2", status: "running" }); if (req.url === "/jobs/j1") return json(200, { id: "j1", status: "done", log: "ok" }); if (req.url === "/versions/v1/report") { res.writeHead(200, { "content-type": "text/markdown; charset=utf-8" }); return res.end("# Body-type classifier v1\n"); } return json(404, { error: "not found" }); }); }); await new Promise((r) => fake.listen(0, "127.0.0.1", r)); const a = fake.address() as { port: number }; fakeUrl = `http://127.0.0.1:${a.port}`; }); afterEach(async () => { await app?.close(); await new Promise((r) => fake.close(() => r())); await rm(dir, { recursive: true, force: true }); }); describe("review page script", () => { it("parses as JavaScript (an apostrophe in a template literal once broke the whole page)", async () => { const { reviewPage } = await import("./review-page.js"); const html = reviewPage(); const script = html.slice(html.indexOf("")); expect(() => new Function(script)).not.toThrow(); }); }); describe("training proxy", () => { it("is hidden when no trainer is configured", async () => { await start(null); const s = await app.inject({ method: "GET", url: "/api/training/status", headers: { authorization: basic } }); expect(s.json()).toEqual({ configured: false }); const j = await app.inject({ method: "POST", url: "/api/training/jobs", headers: { authorization: basic }, payload: { kind: "train" } }); expect(j.statusCode).toBe(503); }); it("aggregates status and forwards jobs and reports behind the reviewer login", async () => { await start(fakeUrl); expect((await app.inject({ method: "GET", url: "/api/training/status" })).statusCode).toBe(401); const s = await app.inject({ method: "GET", url: "/api/training/status", headers: { authorization: basic } }); expect(s.statusCode).toBe(200); const body = s.json(); expect(body.configured).toBe(true); expect(body.reachable).toBe(true); expect(body.readiness.labelled.total).toBe(3); expect(body.versions[0].version).toBe("v1"); expect(body.jobs[0].id).toBe("j1"); const j = await app.inject({ method: "POST", url: "/api/training/jobs", headers: { authorization: basic }, payload: { kind: "train", mode: "features", minAccuracy: 0.9, secret: "nope", version: "v2" }, }); expect(j.statusCode).toBe(202); expect(j.json().id).toBe("j2"); const posted = seen.find((r) => r.method === "POST")!; expect(JSON.parse(posted.body)).toEqual({ kind: "train", mode: "features", minAccuracy: 0.9, version: "v2" }); // unknown keys dropped const busy = await app.inject({ method: "POST", url: "/api/training/jobs", headers: { authorization: basic }, payload: { kind: "evaluate", version: "busy" } }); expect(busy.statusCode).toBe(409); // the trainer's answer passes through const bad = await app.inject({ method: "POST", url: "/api/training/jobs", headers: { authorization: basic }, payload: { kind: "rm-rf" } }); expect(bad.statusCode).toBe(400); const one = await app.inject({ method: "GET", url: "/api/training/jobs/j1", headers: { authorization: basic } }); expect(one.json().status).toBe("done"); expect((await app.inject({ method: "GET", url: "/api/training/jobs/..%2Fx", headers: { authorization: basic } })).statusCode).toBe(400); const rep = await app.inject({ method: "GET", url: "/api/training/versions/v1/report", headers: { authorization: basic } }); expect(rep.statusCode).toBe(200); expect(rep.headers["content-type"]).toContain("text/markdown"); expect(rep.body).toContain("# Body-type classifier v1"); }); it("reports an unreachable trainer without failing the page", async () => { await start("http://127.0.0.1:9"); // nothing listens on the discard port const s = await app.inject({ method: "GET", url: "/api/training/status", headers: { authorization: basic } }); expect(s.statusCode).toBe(200); expect(s.json().reachable).toBe(false); const j = await app.inject({ method: "POST", url: "/api/training/jobs", headers: { authorization: basic }, payload: { kind: "train" } }); expect(j.statusCode).toBe(502); }); });