#!/usr/bin/env node /** * End-to-end admin flow against a locally running service. * * 1. GET /admin → expect 302 to /admin/login (no session yet). * 2. GET /admin/login → scrape the challenge out of the HTML. * 3. Sign the challenge with the deterministic test key. * 4. POST /admin/login → expect 302 + set-cookie. * 5. GET /admin with the cookie → expect 200 and "Dashboard" in the body. * 6. POST /admin/modes → change global to scan-first; verify it stuck. * 7. GET /admin/audit → expect at least one row (the login + the mode set). * * Server must be started with: * MOCK_SCAN=clean \ * MOCK_BNS_OWNER=bchtest:qpumqqygwcnt999fz3gp5nxjy66ckg6esvmzshj478 \ * ADMIN_PUBLISHER=admin.x \ * node service/server.js * * (MOCK_BNS_OWNER applies to every lookup, so both the admin's name and any * publisher name resolve to the same deterministic test address.) */ import { signMessageBip137 } from '../service/lib/sign-message.js'; const BASE = process.env.BASE || 'http://127.0.0.1:3700'; const PRIVKEY_HEX = process.env.TEST_PRIVKEY || '0101010101010101010101010101010101010101010101010101010101010101'; const privateKey = Buffer.from(PRIVKEY_HEX, 'hex'); const log = (...a) => console.log('[admin-test]', ...a); const fail = (msg, extra) => { console.error('[admin-test] FAIL:', msg); if (extra !== undefined) console.error(extra); process.exit(1); }; async function main() { // 1. GET /admin — should redirect. const r1 = await fetch(`${BASE}/admin`, { redirect: 'manual' }); if (r1.status !== 302 || !r1.headers.get('location')?.includes('/admin/login')) { fail(`GET /admin without session: expected 302 to login, got ${r1.status}`, r1.headers.get('location')); } log(`✓ GET /admin without session → 302 ${r1.headers.get('location')}`); // 2. GET /admin/login — scrape challenge. const r2 = await fetch(`${BASE}/admin/login`); if (r2.status !== 200) fail(`GET /admin/login: expected 200, got ${r2.status}`); const html2 = await r2.text(); const m = html2.match(/challenge" value="([A-Za-z0-9_\-=]+)"/); if (!m) fail('could not find challenge in login page', html2.slice(0, 400)); const challenge = m[1]; const nameMatch = html2.match(/name=([a-z0-9.\-]+)\n/); const adminName = nameMatch ? nameMatch[1] : 'admin.x'; log(`✓ GET /admin/login → challenge ${challenge.slice(0, 16)}… for ${adminName}`); // 3. Sign. const message = `PROMETHEUS-ADMIN-LOGIN\nname=${adminName}\nchallenge=${challenge}`; const signature = signMessageBip137(message, privateKey); log(`✓ signed challenge (${signature.length} base64 chars)`); // 4. POST /admin/login. const r4 = await fetch(`${BASE}/admin/login`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, redirect: 'manual', body: new URLSearchParams({ challenge, signature }).toString(), }); if (r4.status !== 302) { fail(`POST /admin/login: expected 302, got ${r4.status}`, await r4.text()); } const cookie = r4.headers.get('set-cookie'); if (!cookie || !cookie.startsWith('prom_admin=')) { fail('login did not set prom_admin cookie', cookie); } const token = cookie.split(';')[0]; // "prom_admin=…" log(`✓ POST /admin/login → 302 + ${token.slice(0, 24)}…`); const headers = { cookie: token }; // 5. GET /admin with cookie. const r5 = await fetch(`${BASE}/admin`, { headers }); if (r5.status !== 200) fail(`GET /admin with session: expected 200, got ${r5.status}`); const html5 = await r5.text(); if (!/Dashboard/.test(html5)) fail('dashboard HTML missing "Dashboard"', html5.slice(0, 400)); log(`✓ GET /admin with session → 200 (dashboard)`); // 6. POST /admin/modes — flip global to scan-first. const modesBody = new URLSearchParams(); modesBody.append('global', 'scan-first'); for (const slug of ['communication', 'privacy', 'wallets', 'games', 'devtools', 'media', 'utilities', 'other']) { modesBody.append(`cat_${slug}`, ''); } const r6 = await fetch(`${BASE}/admin/modes`, { method: 'POST', headers: { ...headers, 'content-type': 'application/x-www-form-urlencoded' }, redirect: 'manual', body: modesBody.toString(), }); if (r6.status !== 302) fail(`POST /admin/modes: expected 302, got ${r6.status}`); const r6r = await fetch(`${BASE}/admin/modes`, { headers }); const html6 = await r6r.text(); if (!/`)) { fail('mode toggle did not persist (no selected scan-first in HTML)'); } } log(`✓ POST /admin/modes global=scan-first → persisted`); // 7. GET /admin/audit — expect rows. const r7 = await fetch(`${BASE}/admin/audit`, { headers }); const html7 = await r7.text(); if (!/set_mode/.test(html7)) fail('audit log missing set_mode row'); if (!/login/.test(html7)) fail('audit log missing login row'); log(`✓ GET /admin/audit → login + set_mode rows present`); log('all admin-flow checks passed.'); } main().catch((err) => fail(err?.message || String(err), err?.stack));