const http = require("node:http"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); // GET /defenseurs/state serves the Sergent's findings-state.json, and shares // serveJsonFile() with GET /defenseurs. Both routes are covered here: the // helper extraction must leave /defenseurs byte-identical to what it served. const TOKEN = "test-token"; const INGEST_TOKEN = "test-ingest-token"; // Deliberately non-canonical JSON (odd spacing, key order, trailing newline): // a handler that parsed and re-serialized the file would not reproduce it. const STATE_RAW = '{ "schemaVersion":1, "findings": {"a-deps-1": {"firstSeen":"2026-09-01T00:00:00.000Z"}},"history":[] }\n'; const STATUS_RAW = '{"timestamp":"2026-09-24T00:00:00.000Z", "agents": [] }\n'; let tmpDir; let statePath; let statusPath; let server; let baseUrl; function startServer() { // Fresh import so module-level constants pick up the stubbed env. delete require.cache[require.resolve("../index.js")]; const { handler } = require("../index.js"); server = http.createServer(handler); return new Promise((resolve) => { server.listen(0, () => { baseUrl = `http://127.0.0.1:${server.address().port}`; resolve(); }); }); } function stopServer() { return new Promise((resolve) => { if (!server) return resolve(); server.close(() => resolve()); }); } async function request(route, { method = "GET", auth = `Bearer ${TOKEN}` } = {}) { const headers = {}; if (auth) headers.Authorization = auth; const res = await fetch(`${baseUrl}${route}`, { method, headers }); return { status: res.status, text: await res.text() }; } beforeEach(async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vps-health-state-test-")); statePath = path.join(tmpDir, "findings-state.json"); statusPath = path.join(tmpDir, "status.json"); process.env.HEALTH_TOKEN = TOKEN; process.env.HOSTS_INGEST_TOKEN = INGEST_TOKEN; process.env.DEFENSEURS_FINDINGS_STATE_PATH = statePath; process.env.DEFENSEURS_STATUS_PATH = statusPath; await startServer(); }); afterEach(async () => { await stopServer(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe("GET /defenseurs/state", () => { test("200 with the file content byte for byte when present", async () => { fs.writeFileSync(statePath, STATE_RAW); const { status, text } = await request("/defenseurs/state"); expect(status).toBe(200); expect(text).toBe(STATE_RAW); }); test("200 no_data when the file is absent", async () => { const { status, text } = await request("/defenseurs/state"); expect(status).toBe(200); expect(JSON.parse(text)).toEqual({ status: "no_data" }); }); test("serves findings-state.json, never status.json", async () => { fs.writeFileSync(statusPath, STATUS_RAW); const { status, text } = await request("/defenseurs/state"); expect(status).toBe(200); expect(JSON.parse(text)).toEqual({ status: "no_data" }); }); test("401 with no Authorization header", async () => { fs.writeFileSync(statePath, STATE_RAW); const { status, text } = await request("/defenseurs/state", { auth: null }); expect(status).toBe(401); expect(text).not.toContain("schemaVersion"); }); test("401 with a wrong bearer token", async () => { fs.writeFileSync(statePath, STATE_RAW); const { status, text } = await request("/defenseurs/state", { auth: "Bearer wrong" }); expect(status).toBe(401); expect(text).not.toContain("schemaVersion"); }); test("401 with the ingest token", async () => { fs.writeFileSync(statePath, STATE_RAW); const { status, text } = await request("/defenseurs/state", { auth: `Bearer ${INGEST_TOKEN}`, }); expect(status).toBe(401); expect(text).not.toContain("schemaVersion"); }); test("404 (not 401) on POST without Authorization", async () => { const { status } = await request("/defenseurs/state", { method: "POST", auth: null }); expect(status).toBe(404); }); }); describe("GET /defenseurs — unchanged by the serveJsonFile extraction", () => { test("200 with status.json byte for byte when present", async () => { fs.writeFileSync(statusPath, STATUS_RAW); const { status, text } = await request("/defenseurs"); expect(status).toBe(200); expect(text).toBe(STATUS_RAW); }); test("200 no_data when status.json is absent", async () => { const { status, text } = await request("/defenseurs"); expect(status).toBe(200); expect(JSON.parse(text)).toEqual({ status: "no_data" }); }); });