const http = require("node:http"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const TOKEN = "test-token"; function makeFinding(id, severity, category) { return { id, severity, category, title: `${id} title`, description: `${id} desc`, location: `${id}/loc`, recommendation: `${id} reco`, firstSeen: "2026-05-01T00:00:00.000Z", status: "open", }; } const ALL_FINDINGS = [ makeFinding("c1", "CRITICAL", "deps"), makeFinding("h1", "HIGH", "secrets"), makeFinding("m1", "MEDIUM", "deps"), makeFinding("l1", "LOW", "code"), makeFinding("i1", "INFO", "infra"), ]; let tmpDir; let reportsDir; let agentsMapPath; let server; let baseUrl; let handler; function writeReport(agent, timestamp, findings, subdir = "") { const dir = subdir ? path.join(reportsDir, subdir) : reportsDir; fs.mkdirSync(dir, { recursive: true }); const safeTs = timestamp.replace(/[:.]/g, "-"); const file = path.join(dir, `defenseur-${agent}_${safeTs}.json`); fs.writeFileSync( file, JSON.stringify({ agent: `defenseur-${agent}`, timestamp, findings }), ); return file; } function writeAgentsMap(map) { fs.writeFileSync(agentsMapPath, JSON.stringify(map)); } function writeRawAgentsMap(raw) { fs.writeFileSync(agentsMapPath, raw); } function startServer() { // Fresh import so module-level constants pick up the stubbed env. delete require.cache[require.resolve("../index.js")]; ({ handler } = require("../index.js")); server = http.createServer(handler); return new Promise((resolve) => { server.listen(0, () => { const { port } = server.address(); baseUrl = `http://127.0.0.1:${port}`; resolve(); }); }); } function stopServer() { return new Promise((resolve) => { if (!server) return resolve(); server.close(() => resolve()); }); } async function get(path, { auth = `Bearer ${TOKEN}` } = {}) { const headers = {}; if (auth) headers.Authorization = auth; const res = await fetch(`${baseUrl}${path}`, { headers }); const body = await res.json().catch(() => ({})); return { status: res.status, body }; } beforeEach(async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vps-health-test-")); reportsDir = path.join(tmpDir, "reports"); fs.mkdirSync(reportsDir, { recursive: true }); agentsMapPath = path.join(tmpDir, "agents-map.json"); process.env.HEALTH_TOKEN = TOKEN; process.env.REPORTS_DIR = reportsDir; process.env.DEFENSEURS_AGENTS_MAP_PATH = agentsMapPath; writeAgentsMap({ "la-suite-booking": "defenseur-booking" }); await startServer(); }); afterEach(async () => { await stopServer(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe("GET /defenseurs/findings", () => { test("401 when no Authorization header", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { status } = await get("/defenseurs/findings?project=la-suite-booking", { auth: null }); expect(status).toBe(401); }); test("401 on invalid token", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { status } = await get("/defenseurs/findings?project=la-suite-booking", { auth: "Bearer wrong" }); expect(status).toBe(401); }); test("400 when project param is missing", async () => { const { status, body } = await get("/defenseurs/findings"); expect(status).toBe(400); expect(body.error).toMatch(/project/); }); test("404 on unknown project", async () => { const { status } = await get("/defenseurs/findings?project=unknown-x"); expect(status).toBe(404); }); test("200 default (no severity) returns MEDIUM+HIGH+CRITICAL, hides LOW+INFO", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { status, body } = await get("/defenseurs/findings?project=la-suite-booking"); expect(status).toBe(200); expect(body.agent).toBe("defenseur-booking"); expect(body.project).toBe("la-suite-booking"); expect(body.timestamp).toBe("2026-05-12T00:00:00.000Z"); expect(body.findings).toHaveLength(3); expect(body.findings.map((f) => f.severity).sort()).toEqual(["CRITICAL", "HIGH", "MEDIUM"]); expect(body.status).toBeUndefined(); }); test("200 ?category=deps filters by category exact match", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { status, body } = await get("/defenseurs/findings?project=la-suite-booking&category=deps"); expect(status).toBe(200); expect(body.findings.map((f) => f.id).sort()).toEqual(["c1", "m1"]); }); test("200 ?severity=HIGH returns HIGH+CRITICAL only", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { body } = await get("/defenseurs/findings?project=la-suite-booking&severity=HIGH"); expect(body.findings.map((f) => f.severity).sort()).toEqual(["CRITICAL", "HIGH"]); }); test("200 ?severity=LOW returns LOW+MEDIUM+HIGH+CRITICAL but hides INFO", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { body } = await get("/defenseurs/findings?project=la-suite-booking&severity=LOW"); expect(body.findings.map((f) => f.severity).sort()).toEqual(["CRITICAL", "HIGH", "LOW", "MEDIUM"]); expect(body.findings.some((f) => f.severity === "INFO")).toBe(false); }); test("200 ?severity=INFO returns INFO only", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { body } = await get("/defenseurs/findings?project=la-suite-booking&severity=INFO"); expect(body.findings.map((f) => f.severity)).toEqual(["INFO"]); }); test("200 ?category=deps&severity=CRITICAL returns just c1", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", ALL_FINDINGS); const { body } = await get("/defenseurs/findings?project=la-suite-booking&category=deps&severity=CRITICAL"); expect(body.findings.map((f) => f.id)).toEqual(["c1"]); }); test("200 picks latest report (mixed top-level + archive)", async () => { writeReport("booking", "2026-05-10T00:00:00.000Z", [makeFinding("old", "HIGH", "deps")], "archive"); writeReport("booking", "2026-05-12T00:00:00.000Z", [makeFinding("new", "HIGH", "deps")]); const { body } = await get("/defenseurs/findings?project=la-suite-booking&severity=HIGH"); expect(body.timestamp).toBe("2026-05-12T00:00:00.000Z"); expect(body.findings.map((f) => f.id)).toEqual(["new"]); }); test("200 report present + findings:[] (scan clean) returns no status field", async () => { writeReport("booking", "2026-05-12T00:00:00.000Z", []); const { status, body } = await get("/defenseurs/findings?project=la-suite-booking"); expect(status).toBe(200); expect(body.findings).toEqual([]); expect(body.status).toBeUndefined(); expect(body.agent).toBe("defenseur-booking"); }); test("200 report absent returns {findings:[], status:'no_data'}", async () => { const { status, body } = await get("/defenseurs/findings?project=la-suite-booking"); expect(status).toBe(200); expect(body).toEqual({ findings: [], status: "no_data" }); }); test("500 on corrupt agents-map.json", async () => { writeRawAgentsMap("{not valid json"); const { status, body } = await get("/defenseurs/findings?project=la-suite-booking"); expect(status).toBe(500); expect(body.error).toBe("Internal error"); }); });