/** * Integratietests voor de gecombineerde app-container: de Express-app serveert * zowel de API op /api als de gebouwde frontend op /. * * Draait zonder database: NODE_ENV=test slaat het schema-initialisatie en * app.listen() in server.js over, en geen van de geteste routes raakt de db. */ import { after, before, describe, it } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const INDEX_MARKER = "
"; const ASSET_BODY = "console.log('vibefinance');"; let server; let basis; before(async () => { // Nagebootste Vite-build: index.html + een asset met hash in de naam. const staticDir = mkdtempSync(join(tmpdir(), "vibefinance-static-")); mkdirSync(join(staticDir, "assets")); writeFileSync(join(staticDir, "index.html"), `${INDEX_MARKER}`); writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY); process.env.NODE_ENV = "test"; process.env.STATIC_DIR = staticDir; process.env.UPDATE_CHECK_URL = ""; // geen uitgaande call in tests process.env.JWT_SECRET = "test-secret"; const { default: app } = await import("../src/server.js"); server = app.listen(0); await new Promise((res) => server.once("listening", res)); basis = `http://127.0.0.1:${server.address().port}`; }); after(() => server?.close()); describe("API", () => { it("health geeft status ok", async () => { const res = await fetch(`${basis}/api/health`); assert.equal(res.status, 200); const body = await res.json(); assert.equal(body.status, "ok"); assert.equal(typeof body.uptime, "number"); }); it("onbekende API-route geeft 404 als JSON, niet de SPA", async () => { const res = await fetch(`${basis}/api/bestaat-niet`); assert.equal(res.status, 404); assert.match(res.headers.get("content-type") ?? "", /application\/json/); assert.equal((await res.json()).error, "Endpoint niet gevonden."); }); it("beschermde route zonder token geeft 401", async () => { const res = await fetch(`${basis}/api/data`); assert.equal(res.status, 401); assert.equal((await res.json()).error, "Niet ingelogd."); }); it("update-check zonder ingestelde URL meldt niets", async () => { const res = await fetch(`${basis}/api/update-check`); const body = await res.json(); assert.equal(res.status, 200); assert.equal(body.latest, null); assert.equal(body.disabled, true); }); }); describe("Frontend serveren", () => { it("root geeft index.html", async () => { const res = await fetch(`${basis}/`); assert.equal(res.status, 200); assert.match(res.headers.get("content-type") ?? "", /text\/html/); assert.match(await res.text(), new RegExp(INDEX_MARKER)); }); it("onbekend pad valt terug op index.html (React-routing)", async () => { const res = await fetch(`${basis}/voortgang`); assert.equal(res.status, 200); assert.match(await res.text(), new RegExp(INDEX_MARKER)); }); it("index.html wordt niet gecached", async () => { const res = await fetch(`${basis}/`); assert.match(res.headers.get("cache-control") ?? "", /no-store/); }); it("assets met hash worden lang gecached", async () => { const res = await fetch(`${basis}/assets/index-abc12345.js`); assert.equal(res.status, 200); assert.equal(await res.text(), ASSET_BODY); assert.match(res.headers.get("cache-control") ?? "", /immutable/); }); }); describe("Security headers", () => { it("CSP staat Google Fonts en inline styles toe", async () => { const res = await fetch(`${basis}/`); const csp = res.headers.get("content-security-policy") ?? ""; assert.match(csp, /style-src [^;]*'unsafe-inline'/); assert.match(csp, /fonts\.googleapis\.com/); assert.match(csp, /font-src [^;]*fonts\.gstatic\.com/); assert.doesNotMatch(csp, /upgrade-insecure-requests/); }); it("statische bestanden krijgen geen rate-limit headers", async () => { const res = await fetch(`${basis}/assets/index-abc12345.js`); assert.equal(res.headers.get("ratelimit-limit"), null); }); it("de API krijgt wel rate-limit headers", async () => { const res = await fetch(`${basis}/api/health`); assert.notEqual(res.headers.get("ratelimit-limit"), null); }); });