149 lines
7.0 KiB
JavaScript
149 lines
7.0 KiB
JavaScript
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);
|
|
|
|
const KEY = 'abcdef0123456789abcd';
|
|
const OFFKEY = 'offoffoffoffoffoffoff';
|
|
async function start(config = {}) {
|
|
const db = openDb(':memory:');
|
|
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(KEY, 'p', '[]');
|
|
db.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,0)').run(OFFKEY, 'p', '[]');
|
|
const server = createPublicServer({ config, db });
|
|
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}${file === 'w.js' ? `?s=${KEY}` : ''}`, { '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}${file === 'w.js' ? `?s=${KEY}` : ''}`, { '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?s=${KEY}`);
|
|
const hit = await raw(s.base, `/a/w.js?s=${KEY}`, { '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?s=${KEY}`, { '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();
|
|
});
|
|
|
|
const EMPTY = '/* WEBklar Analyse: aus */\n';
|
|
for (const [name, q] of [['unbekannter Schlüssel', '?s=unbekannt0123456789ab'], ['ungültiger Schlüssel', '?s=ABC'], ['ohne s', ''], ['ausgeschaltete Website', `?s=${OFFKEY}`]]) {
|
|
test(`w.js ${name}: leeres Skript`, async () => {
|
|
const s = await start();
|
|
const r = await raw(s.base, `/a/w.js${q}`, { 'Accept-Encoding': 'gzip' });
|
|
assert.equal(r.status, 200);
|
|
assert.equal(r.body.toString(), EMPTY);
|
|
assert.equal(r.headers['content-type'], 'text/javascript; charset=utf-8');
|
|
assert.equal(r.headers['cache-control'], 'public, max-age=60');
|
|
assert.equal(r.headers['x-content-type-options'], 'nosniff');
|
|
assert.equal(r.headers['cross-origin-resource-policy'], 'cross-origin');
|
|
assert.equal(r.headers['access-control-allow-origin'], '*');
|
|
assert.equal(r.headers.vary, 'Accept-Encoding');
|
|
assert.equal(r.headers['content-encoding'], undefined);
|
|
assert.equal(r.headers.etag, undefined);
|
|
s.close();
|
|
});
|
|
}
|
|
|
|
test('Ende-zu-Ende: Ausschalten wirkt sofort, Einschalten ebenfalls', async () => {
|
|
const db = openDb(':memory:');
|
|
const pub = createPublicServer({ config: {}, db });
|
|
const adm = createAdminServer({ config: { adminToken: 't' }, db });
|
|
await Promise.all([pub, adm].map((x) => new Promise((r) => x.listen(0, '127.0.0.1', r))));
|
|
const pb = `http://127.0.0.1:${pub.address().port}`;
|
|
const ab = `http://127.0.0.1:${adm.address().port}`;
|
|
const put = (active) => fetch(`${ab}/a/admin/sites/${KEY}`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ projectId: 'p', domains: ['abc.project.webklar.com'], active }) });
|
|
const get = async () => (await raw(pb, `/a/w.js?s=${KEY}`)).body.toString();
|
|
assert.equal(await get(), EMPTY);
|
|
assert.equal((await put(true)).status, 200);
|
|
assert.notEqual(await get(), EMPTY);
|
|
assert.equal((await put(false)).status, 200);
|
|
assert.equal(await get(), EMPTY);
|
|
assert.equal((await put(true)).status, 200);
|
|
assert.notEqual(await get(), EMPTY);
|
|
pub.close(); adm.close();
|
|
});
|