118 lines
4.3 KiB
JavaScript
118 lines
4.3 KiB
JavaScript
/**
|
|||
|
|
* 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);
|
||
|
|
});
|
||
|
|
});
|