// End-to-end test of the REAL server.js (better-sqlite3 shimmed to node:sqlite). import { rmSync } from 'node:fs'; const PORT = 8799; const TOKEN = 'test-admin-token'; const BASE = `http://127.0.0.1:${PORT}`; process.env.ADMIN_API_TOKEN = TOKEN; process.env.PORT = String(PORT); process.env.DATA_DIR = './.testdata'; process.env.SEED_PRODUCTS = 'gdpr-content-blocker:Content Blocker'; process.env.PUBLIC_BASE_URL = BASE; rmSync('./.testdata', { recursive: true, force: true }); let fail = 0; const ok = (name, cond, extra = '') => { console.log((cond ? '[PASS] ' : '[FAIL] ') + name + (cond ? '' : ' ' + extra)); if (!cond) fail++; }; async function admin(method, path, body) { const r = await fetch(BASE + path, { method, headers: { 'Content-Type': 'application/json', 'X-Admin-Token': TOKEN }, body: body ? JSON.stringify(body) : undefined, }); return { status: r.status, json: await r.json() }; } async function pub(path, body) { const r = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return { status: r.status, json: await r.json() }; } async function adminRaw(path, buffer, headers = {}) { const r = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/zip', 'X-Admin-Token': TOKEN, ...headers }, body: buffer, }); return { status: r.status, json: await r.json() }; } // Boot the real server. await import('../src/server.js'); await new Promise((r) => setTimeout(r, 400)); try { const P = 'gdpr-content-blocker'; // Health ok('healthz', (await (await fetch(BASE + '/healthz')).json()).ok === true); // Admin auth required const noAuth = await fetch(BASE + '/api/v1/licenses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: P, max_activations: 1 }), }); ok('generate without token → 401', noAuth.status === 401); // Generate single-site key (max=1) const g1 = await admin('POST', '/api/v1/licenses', { product: P, max_activations: 1, email: 'a@b.de' }); ok('generate key 201', g1.status === 201 && g1.json.ok === true, JSON.stringify(g1.json)); const key1 = g1.json.key; ok('key format', /^[A-Z2-9]{4}(-[A-Z2-9]{4}){3}$/.test(key1 || ''), key1); // Activate domain A const a1 = await pub('/api/v1/activate', { key: key1, product: P, domain: 'https://www.Kunde-A.de/' }); ok('activate A valid', a1.json.status === 'valid' && a1.json.activations_used === 1, JSON.stringify(a1.json)); // Re-activate same domain (idempotent, count stays 1) const a1b = await pub('/api/v1/activate', { key: key1, product: P, domain: 'kunde-a.de' }); ok('re-activate A idempotent', a1b.json.ok && a1b.json.activations_used === 1, JSON.stringify(a1b.json)); // Activate second domain → limit reached (max=1), response lists bound domains const a2 = await pub('/api/v1/activate', { key: key1, product: P, domain: 'kunde-b.de' }); ok('second domain → 409 limit', a2.status === 409 && a2.json.ok === false, JSON.stringify(a2.json)); ok('409 reports code limit_reached', a2.json.code === 'limit_reached', JSON.stringify(a2.json)); ok('409 lists occupied domain', Array.isArray(a2.json.domains) && a2.json.domains.includes('kunde-a.de'), JSON.stringify(a2.json)); // Validate activated domain const v1 = await pub('/api/v1/validate', { key: key1, product: P, domain: 'kunde-a.de' }); ok('validate A valid', v1.json.status === 'valid', JSON.stringify(v1.json)); // Validate non-activated domain const v2 = await pub('/api/v1/validate', { key: key1, product: P, domain: 'kunde-x.de' }); ok('validate unbound → 403', v2.status === 403 && v2.json.ok === false, JSON.stringify(v2.json)); // Deactivate A, then B should fit const d1 = await pub('/api/v1/deactivate', { key: key1, product: P, domain: 'kunde-a.de' }); ok('deactivate A', d1.json.ok === true); const a3 = await pub('/api/v1/activate', { key: key1, product: P, domain: 'kunde-b.de' }); ok('after deactivate, B fits', a3.json.status === 'valid', JSON.stringify(a3.json)); // 3-site key const g3 = await admin('POST', '/api/v1/licenses', { product: P, max_activations: 3 }); const key3 = g3.json.key; for (const d of ['s1.de', 's2.de', 's3.de']) { const r = await pub('/api/v1/activate', { key: key3, product: P, domain: d }); ok('3-site activate ' + d, r.json.status === 'valid', JSON.stringify(r.json)); } const g3over = await pub('/api/v1/activate', { key: key3, product: P, domain: 's4.de' }); ok('3-site 4th → 409', g3over.status === 409, JSON.stringify(g3over.json)); // Unlimited key const gU = await admin('POST', '/api/v1/licenses', { product: P, max_activations: -1 }); const keyU = gU.json.key; let allOk = true; for (let i = 0; i < 10; i++) { const r = await pub('/api/v1/activate', { key: keyU, product: P, domain: `u${i}.de` }); if (r.json.status !== 'valid') allOk = false; } ok('unlimited activates 10 domains', allOk); // Disable a license → activation blocked await admin('POST', `/api/v1/licenses/${key3}/disable`); const disabled = await pub('/api/v1/activate', { key: key3, product: P, domain: 'new.de' }); ok('disabled license → 403', disabled.status === 403, JSON.stringify(disabled.json)); // Wrong product const wrong = await pub('/api/v1/activate', { key: key1, product: 'nope', domain: 'kunde-b.de' }); ok('unknown product → 404', wrong.status === 404, JSON.stringify(wrong.json)); // Unknown key const unk = await pub('/api/v1/activate', { key: 'ZZZZ-ZZZZ-ZZZZ-ZZZZ', product: P, domain: 'x.de' }); ok('unknown key → 404', unk.status === 404, JSON.stringify(unk.json)); // Invalid max_activations const badMax = await admin('POST', '/api/v1/licenses', { product: P, max_activations: 0 }); ok('max_activations 0 → 400', badMax.status === 400, JSON.stringify(badMax.json)); // Add a new product (extensibility) and issue a key for it const np = await admin('POST', '/api/v1/products', { slug: 'next-plugin', name: 'Next Plugin' }); ok('add product 201', np.status === 201); const npKey = await admin('POST', '/api/v1/licenses', { product: 'next-plugin', max_activations: 1 }); ok('key for new product', npKey.status === 201 && npKey.json.ok, JSON.stringify(npKey.json)); // Inspect a license const info = await admin('GET', `/api/v1/licenses/${key1}`); ok('inspect shows 1 domain (kunde-b)', info.json.activations_used === 1 && info.json.domains[0].domain === 'kunde-b.de', JSON.stringify(info.json)); // ── Scan endpoint guards ── // Unactivated domain → 403 const scanUnbound = await pub('/api/v1/scan', { key: key1, product: P, domain: 'kunde-a.de' }); ok('scan unbound domain → 403', scanUnbound.status === 403, JSON.stringify(scanUnbound.json)); // SSRF guard: target url on a different host than the licensed domain → 400 const scanWrongHost = await pub('/api/v1/scan', { key: key1, product: P, domain: 'kunde-b.de', urls: ['https://evil.example.org/'] }); ok('scan foreign host filtered → 400', scanWrongHost.status === 400, JSON.stringify(scanWrongHost.json)); // Private/loopback licensed domain rejected const scanLocal = await pub('/api/v1/scan', { key: key1, product: P, domain: '127.0.0.1' }); ok('scan private domain → 400', scanLocal.status === 400, JSON.stringify(scanLocal.json)); // Valid bound domain: host is unreachable in test, so endpoint returns ok:true // with a per-page fetch error and empty findings (pipeline wiring works). const scanOk = await pub('/api/v1/scan', { key: key1, product: P, domain: 'kunde-b.de' }); ok('scan bound domain → 200 ok', scanOk.status === 200 && scanOk.json.ok === true, JSON.stringify(scanOk.json)); ok('scan reports the attempted page', Array.isArray(scanOk.json.scanned) && scanOk.json.scanned.length === 1, JSON.stringify(scanOk.json)); // ── Update delivery ── // A minimal but valid ZIP (PK signature). key1 is active on kunde-b.de. const zip = Buffer.from([0x50, 0x4b, 0x03, 0x04, 1, 2, 3, 4, 5, 6, 7, 8]); const upBad = await adminRaw('/api/v1/releases?product=gdpr-content-blocker&version=bad', zip); ok('upload bad version → 400', upBad.status === 400, JSON.stringify(upBad.json)); const upEmpty = await adminRaw('/api/v1/releases?product=gdpr-content-blocker&version=1.1.0', Buffer.alloc(0)); ok('upload empty body → 400', upEmpty.status === 400, JSON.stringify(upEmpty.json)); const up = await adminRaw('/api/v1/releases?product=gdpr-content-blocker&version=1.1.0', zip, { 'X-Changelog': 'Neues' }); ok('upload release 201', up.status === 201 && up.json.bytes === zip.length, JSON.stringify(up.json)); // No update when current >= latest const uNo = await pub('/api/v1/update', { key: key1, product: P, domain: 'kunde-b.de', version: '1.1.0' }); ok('update: current==latest → none', uNo.json.ok && uNo.json.update_available === false, JSON.stringify(uNo.json)); // Update available when current < latest const uYes = await pub('/api/v1/update', { key: key1, product: P, domain: 'kunde-b.de', version: '1.0.0' }); ok('update available', uYes.json.update_available === true && uYes.json.version === '1.1.0', JSON.stringify(uYes.json)); ok('update has package url', typeof uYes.json.package === 'string' && uYes.json.package.includes('/api/v1/download?token='), JSON.stringify(uYes.json)); // Unactivated domain cannot check updates const uUnbound = await pub('/api/v1/update', { key: key1, product: P, domain: 'fremd.de', version: '1.0.0' }); ok('update unbound domain → 403', uUnbound.status === 403, JSON.stringify(uUnbound.json)); // Download via the signed package URL → real ZIP bytes const dl = await fetch(uYes.json.package); const dlBuf = Buffer.from(await dl.arrayBuffer()); ok('download 200 zip', dl.status === 200 && dl.headers.get('content-type') === 'application/zip', String(dl.status)); ok('download bytes match upload', dlBuf.length === zip.length && dlBuf[0] === 0x50 && dlBuf[1] === 0x4b); // Tampered token rejected const badToken = uYes.json.package.replace('token=', 'token=x'); const dlBad = await fetch(badToken); ok('download tampered token → 403', dlBad.status === 403, String(dlBad.status)); // Admin can list releases const relList = await admin('GET', '/api/v1/releases/gdpr-content-blocker'); ok('release list shows 1.1.0', relList.json.ok && relList.json.releases.some((r) => r.version === '1.1.0'), JSON.stringify(relList.json)); // ── Release from URL: guards (real fetch not exercised offline) ── const fuProd = await admin('POST', '/api/v1/releases/from-url', { product: 'nope', version: '1.2.0', zip_url: 'https://example.com/a.zip' }); ok('from-url unknown product → 404', fuProd.status === 404, JSON.stringify(fuProd.json)); const fuVer = await admin('POST', '/api/v1/releases/from-url', { product: P, version: 'x', zip_url: 'https://example.com/a.zip' }); ok('from-url bad version → 400', fuVer.status === 400, JSON.stringify(fuVer.json)); const fuUrl = await admin('POST', '/api/v1/releases/from-url', { product: P, version: '1.2.0', zip_url: 'not-a-url' }); ok('from-url invalid url → 400', fuUrl.status === 400, JSON.stringify(fuUrl.json)); const fuLocal = await admin('POST', '/api/v1/releases/from-url', { product: P, version: '1.2.0', zip_url: 'http://127.0.0.1/a.zip' }); ok('from-url private host → 400', fuLocal.status === 400, JSON.stringify(fuLocal.json)); } catch (e) { console.error(e); fail++; } console.log('\n' + (fail === 0 ? 'ALL PASSED' : fail + ' FAILED')); try { rmSync('./.testdata', { recursive: true, force: true }); } catch { /* WAL handle still open; harmless */ } process.exit(fail === 0 ? 0 : 1);