123 lines
5.3 KiB
JavaScript
123 lines
5.3 KiB
JavaScript
|
|
#!/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 (!/<option value="scan-first" selected>scan-first<\/option>[\s\S]*?Global/.test(html6)
|
||
|
|
&& !/Global[\s\S]*?<option value="scan-first" selected>scan-first<\/option>/.test(html6)) {
|
||
|
|
// Simpler check: just verify the string appears for the global row
|
||
|
|
if (!html6.includes(`<option value="scan-first" selected>scan-first</option>`)) {
|
||
|
|
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));
|