import test from 'node:test'; import assert from 'node:assert/strict'; import zlib from 'node:zlib'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import Fastify from 'fastify'; import fastifyCompress from '@fastify/compress'; import { registerFrontend, resolveAppVersion } from '../src/frontend.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const publicRoot = join(__dirname, '..', 'public'); // Zelfde volgorde als src/server.js: compress vóór de frontend-registratie. async function makeApp(version = '1.2.3-test') { const app = Fastify(); await app.register(fastifyCompress, { global: true, encodings: ['br', 'gzip'], threshold: 1024, brotliOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } }, zlibOptions: { level: 6 }, }); await registerFrontend(app, { root: publicRoot, version }); await app.ready(); return app; } test('index.html wordt geserveerd met versie-injectie en no-cache', async () => { const app = await makeApp(); const res = await app.inject({ url: '/' }); assert.equal(res.statusCode, 200); assert.match(res.headers['content-type'], /text\/html/); assert.equal(res.headers['cache-control'], 'no-cache'); assert.ok(res.payload.includes('?v=1.2.3-test'), 'versie hoort geïnjecteerd te zijn'); assert.ok(!res.payload.includes('__V__'), 'geen placeholder meer in de uitvoer'); const alias = await app.inject({ url: '/index.html' }); assert.equal(alias.headers['cache-control'], 'no-cache'); await app.close(); }); test('js en css zijn een jaar immutable; fonts en img korter zonder immutable', async () => { const app = await makeApp(); for (const url of ['/js/core.js', '/css/teach.css']) { const res = await app.inject({ url }); assert.equal(res.statusCode, 200, url); assert.equal(res.headers['cache-control'], 'public, max-age=31536000, immutable', url); } const font = await app.inject({ url: '/fonts/quicksand.woff2' }); assert.equal(font.headers['cache-control'], 'public, max-age=604800'); assert.ok(font.headers.etag, 'etag als 304-vangnet voor ongeversioneerde assets'); const img = await app.inject({ url: '/img/logo.svg' }); assert.equal(img.headers['cache-control'], 'public, max-age=86400'); await app.close(); }); test('assets gaan gecomprimeerd over de lijn (gzip én brotli)', async () => { const app = await makeApp(); const gz = await app.inject({ url: '/css/teach.css', headers: { 'accept-encoding': 'gzip' } }); assert.equal(gz.headers['content-encoding'], 'gzip'); assert.match(gz.headers.vary ?? '', /accept-encoding/i); const cssRaw = zlib.gunzipSync(gz.rawPayload).toString('utf8'); assert.ok(cssRaw.includes('toolbar'), 'gzip-inhoud is geldige css'); const br = await app.inject({ url: '/js/core.js', headers: { 'accept-encoding': 'br' } }); assert.equal(br.headers['content-encoding'], 'br'); const jsRaw = zlib.brotliDecompressSync(br.rawPayload).toString('utf8'); assert.ok(jsRaw.includes('const VERSION'), 'brotli-inhoud is geldige js'); assert.ok(br.rawPayload.length < jsRaw.length / 2, 'compressie levert echt winst op'); await app.close(); }); test('conditional GET levert 304 zonder body', async () => { const app = await makeApp(); const first = await app.inject({ url: '/fonts/quicksand.woff2' }); const res = await app.inject({ url: '/fonts/quicksand.woff2', headers: { 'if-none-match': first.headers.etag } }); assert.equal(res.statusCode, 304); assert.equal(res.rawPayload.length, 0); await app.close(); }); test('resolveAppVersion geeft de echte versie door en valt lokaal nooit stale terug', () => { assert.equal(resolveAppVersion({ APP_VERSION: '0.4.0-beta' }), '0.4.0-beta'); const a = resolveAppVersion({}); const b = resolveAppVersion({ APP_VERSION: 'dev' }); assert.match(a, /^dev-\d+$/); assert.match(b, /^dev-\d+$/); }); test('index.html: elk script heeft defer + cache-busting, font-preload blijft ongeversioneerd', async () => { const [html, server] = await Promise.all([ readFile(join(publicRoot, 'index.html'), 'utf8'), readFile(join(__dirname, '..', 'src', 'server.js'), 'utf8'), ]); const scripts = [...html.matchAll(/