Files
VibeFinance/backend/test/app.test.js
T

118 lines
4.3 KiB
JavaScript
Raw Normal View History

/**
* 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 = "<div id=\"root\"></div>";
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"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`);
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);
});
});