import { test } from 'node:test'; import assert from 'node:assert/strict'; import { loadConfig } from '../src/config.js'; import { openDb } from '../src/db.js'; import { createServer } from '../src/server.js'; const KEY = 'abcdef0123456789abcd'; async function setup(adminToken = 'geheim') { const db = openDb(':memory:'); const server = createServer({ config: { adminToken }, db }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); const base = `http://127.0.0.1:${server.address().port}`; const call = async (method, path, body, token = 'geheim', raw) => { const headers = {}; if (token !== null) headers.Authorization = `Bearer ${token}`; if (body !== undefined || raw !== undefined) headers['Content-Type'] = 'application/json'; const res = await fetch(base + path, { method, headers, body: raw ?? (body === undefined ? undefined : JSON.stringify(body)) }); return { status: res.status, body: await res.json() }; }; return { db, call, close: () => server.close() }; } const row = (db, k = KEY) => db.prepare('SELECT * FROM sites WHERE siteKey = ?').get(k); const valid = { projectId: 'p1', domains: ['abc.project.webklar.com'], active: true }; test('loadConfig: Vorgaben und Umgebung', () => { const c = loadConfig({}); assert.deepEqual(c, { port: 8090, dataDir: '/srv/webklar-analytics', adminToken: '', heatSecret: '', geoFile: '', baseHost: 'project.webklar.com' }); const d = loadConfig({ ANALYTICS_PORT: '9000', ANALYTICS_ADMIN_TOKEN: 't', ANALYTICS_HEAT_SECRET: 'h' }); assert.equal(d.port, 9000); assert.equal(d.adminToken, 't'); assert.equal(d.heatSecret, 'h'); }); test('openDb ist idempotent und legt Tabellen an', () => { const db = openDb(':memory:'); const names = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name); for (const n of ['sites', 'events_raw', 'clicks', 'daily']) assert.ok(names.includes(n), n); }); test('GET /a/health ohne Token', async () => { const t = await setup(); const r = await t.call('GET', '/a/health', undefined, null); assert.equal(r.status, 200); assert.deepEqual(r.body, { ok: true }); t.close(); }); test('Admin: 401 ohne, mit falschem Token und bei leerem adminToken', async () => { const t = await setup(); assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, null)).status, 401); assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, valid, 'falsch')).status, 401); assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`, undefined, 'falsch')).status, 401); assert.equal(row(t.db), undefined); t.close(); const e = await setup(''); assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, '')).status, 401); assert.equal((await e.call('PUT', `/a/admin/sites/${KEY}`, valid, 'x')).status, 401); e.close(); }); test('PUT legt an und ändert', async () => { const t = await setup(); const r = await t.call('PUT', `/a/admin/sites/${KEY}`, valid); assert.equal(r.status, 200); assert.equal(r.body.ok, true); let s = row(t.db); assert.equal(s.projectId, 'p1'); assert.deepEqual(JSON.parse(s.domains), ['abc.project.webklar.com']); assert.equal(s.active, 1); assert.equal(s.offSince, null); await t.call('PUT', `/a/admin/sites/${KEY}`, { projectId: 'p2', domains: ['a.de', 'b.de'], active: true }); s = row(t.db); assert.equal(s.projectId, 'p2'); assert.deepEqual(JSON.parse(s.domains), ['a.de', 'b.de']); assert.equal(t.db.prepare('SELECT COUNT(*) c FROM sites').get().c, 1); t.close(); }); test('active=false setzt offSince einmalig, active=true löscht es', async () => { const t = await setup(); await t.call('PUT', `/a/admin/sites/${KEY}`, valid); await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false }); const s1 = row(t.db); assert.equal(s1.active, 0); assert.ok(!Number.isNaN(Date.parse(s1.offSince))); assert.match(s1.offSince, /^\d{4}-\d\d-\d\dT/); t.db.prepare('UPDATE sites SET offSince = ? WHERE siteKey = ?').run('2026-01-01T00:00:00.000Z', KEY); await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: false }); assert.equal(row(t.db).offSince, '2026-01-01T00:00:00.000Z'); await t.call('PUT', `/a/admin/sites/${KEY}`, valid); assert.equal(row(t.db).offSince, null); t.close(); }); test('PUT: ungültiger siteKey 400', async () => { const t = await setup(); for (const k of ['kurz', 'ABCDEF0123456789ABCD', 'abcdef0123456789abc!', 'a'.repeat(41)]) { assert.equal((await t.call('PUT', `/a/admin/sites/${k}`, valid)).status, 400, k); } assert.equal((await t.call('DELETE', '/a/admin/sites/kurz')).status, 400); t.close(); }); test('PUT: Domains nur Hostnamen, max 5; projectId/active geprüft', async () => { const t = await setup(); const bad = [ ['a.de:8080'], ['a.de/pfad'], ['https://a.de'], ['A.de'], ['localhost'], [''], ['-a.de'], [1], 'a.de', [], ['a.de', 'b.de', 'c.de', 'd.de', 'e.de', 'f.de'], ]; for (const domains of bad) { const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains }); assert.equal(r.status, 400, JSON.stringify(domains)); assert.equal(typeof r.body.error, 'string'); } assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: '' })).status, 400); assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, active: 'ja' })).status, 400); assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, undefined, 'geheim', '{kaputt')).status, 400); assert.equal(row(t.db), undefined); assert.equal((await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, domains: ['a.de', 'b.de', 'c.de', 'd.de', 'e.de'] })).status, 200); t.close(); }); test('PUT: zu großer Body 413', async () => { const t = await setup(); const r = await t.call('PUT', `/a/admin/sites/${KEY}`, { ...valid, projectId: 'x'.repeat(70000) }); assert.equal(r.status, 413); t.close(); }); test('DELETE entfernt Website, unbekannte Route 404', async () => { const t = await setup(); await t.call('PUT', `/a/admin/sites/${KEY}`, valid); assert.equal((await t.call('DELETE', `/a/admin/sites/${KEY}`)).status, 200); assert.equal(row(t.db), undefined); assert.equal((await t.call('GET', '/a/nichts', undefined, null)).status, 404); t.close(); });