#!/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));