import { test } from 'node:test'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import http from 'node:http'; import { readFileSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { gunzipSync } from 'node:zlib'; import { openDb } from '../src/db.js'; import { createPublicServer, createAdminServer } from '../src/server.js'; const DIST = new URL('../client/dist/', import.meta.url); async function start(config = {}) { const server = createPublicServer({ config, db: openDb(':memory:') }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); return { base: `http://127.0.0.1:${server.address().port}`, close: () => server.close() }; } // fetch dekodiert gzip selbst; für den rohen Body http.request verwenden. function raw(base, path, headers = {}, method = 'GET') { return new Promise((resolve, reject) => { const req = http.request(base + path, { method, headers }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); }); req.on('error', reject); req.end(); }); } for (const file of ['w.js', 'h.js']) { test(`/a/${file}: Header, identity und gzip liefern den dist-Inhalt`, async () => { const s = await start(); const want = readFileSync(new URL(file, DIST)); const etag = `"${crypto.createHash('sha256').update(want).digest('hex')}"`; const id = await raw(s.base, `/a/${file}`, { 'Accept-Encoding': 'identity' }); assert.equal(id.status, 200); assert.deepEqual(id.body, want); assert.equal(id.headers['content-type'], 'text/javascript; charset=utf-8'); assert.equal(id.headers['cache-control'], 'public, max-age=3600'); assert.equal(id.headers.etag, etag); assert.equal(id.headers.vary, 'Accept-Encoding'); assert.equal(id.headers['x-content-type-options'], 'nosniff'); assert.equal(id.headers['cross-origin-resource-policy'], 'cross-origin'); assert.equal(id.headers['access-control-allow-origin'], '*'); assert.equal(id.headers['content-encoding'], undefined); const gz = await raw(s.base, `/a/${file}`, { 'Accept-Encoding': 'gzip, deflate' }); assert.equal(gz.status, 200); assert.equal(gz.headers['content-encoding'], 'gzip'); assert.equal(gz.headers.etag, etag); assert.ok(gz.body.length < want.length); assert.deepEqual(gunzipSync(gz.body), want); s.close(); }); } test('ETag: 304 bei If-None-Match, 200 bei anderem Wert', async () => { const s = await start(); const first = await raw(s.base, '/a/w.js'); const hit = await raw(s.base, '/a/w.js', { 'If-None-Match': first.headers.etag }); assert.equal(hit.status, 304); assert.equal(hit.body.length, 0); assert.equal(hit.headers.etag, first.headers.etag); assert.equal(hit.headers['cache-control'], 'public, max-age=3600'); assert.equal((await raw(s.base, '/a/w.js', { 'If-None-Match': '"anders"' })).status, 200); s.close(); }); test('HEAD: Header wie GET, kein Body', async () => { const s = await start(); const get = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' }); const head = await raw(s.base, '/a/h.js', { 'Accept-Encoding': 'identity' }, 'HEAD'); assert.equal(head.status, 200); assert.equal(head.body.length, 0); assert.equal(head.headers['content-length'], String(get.body.length)); assert.equal(head.headers.etag, get.headers.etag); assert.equal((await raw(s.base, '/a/h.js', {}, 'POST')).status, 405); s.close(); }); test('Admin-Listener liefert die Skripte nicht', async () => { const server = createAdminServer({ config: { adminToken: 't' }, db: openDb(':memory:') }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); const base = `http://127.0.0.1:${server.address().port}`; assert.equal((await fetch(`${base}/a/w.js`)).status, 404); assert.equal((await fetch(`${base}/a/h.js`)).status, 404); server.close(); }); test('fehlende dist-Datei: 503 nur für diesen Pfad, deutscher Fehler im Log', async () => { const dir = mkdtempSync(join(tmpdir(), 'dist-')); writeFileSync(join(dir, 'w.js'), 'console.log(1)'); const orig = console.error; const logged = []; console.error = (...a) => logged.push(a.join(' ')); let s; try { s = await start({ distDir: dir }); } finally { console.error = orig; } assert.ok(logged.some((l) => l.includes('h.js') && l.includes('fehlt'))); assert.equal((await fetch(`${s.base}/a/w.js`)).status, 200); assert.equal((await fetch(`${s.base}/a/h.js`)).status, 503); s.close(); });