// End-to-end test of the backend platform. // Exercises the full flow: register -> login -> enroll machine -> agent online -> // technician requests session -> consent -> signaling relay -> audit trail. // No browser/Electron needed: the "agent" and "viewer" are raw WebSocket clients. // (Login currently marks the session MFA-passed directly, so there is no separate // TOTP step in the product flow; the MFA endpoints still exist but aren't exercised here.) const fs = require('fs'); const os = require('os'); const path = require('path'); const DB = path.join(os.tmpdir(), 'ra-e2e.db'); process.env.DB_PATH = DB; for (const f of [DB, DB + '-wal', DB + '-shm']) { try { fs.unlinkSync(f); } catch {} } const PORT = 8099; process.env.PORT = PORT; process.env.HTTPS_PORT = 8444; // avoid clashing with a running dev server on 8443 const { server } = require('../server'); const A = require('../auth'); const WebSocket = require('ws'); const BASE = `http://localhost:${PORT}`; let passed = 0, failed = 0; function check(name, cond) { if (cond) { console.log(' ok -', name); passed++; } else { console.log(' FAIL-', name); failed++; } } // minimal cookie-aware fetch async function call(path, body, cookie) { const r = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}) }, body: body ? JSON.stringify(body) : undefined, }); const setCookie = r.headers.get('set-cookie'); const data = await r.json().catch(() => ({})); return { status: r.status, data, cookie: setCookie ? setCookie.split(';')[0] : cookie }; } async function get(path, cookie) { const r = await fetch(BASE + path, { headers: cookie ? { Cookie: cookie } : {} }); return { status: r.status, data: await r.json().catch(() => ({})) }; } const wait = (ms) => new Promise((r) => setTimeout(r, ms)); function wsClient() { const ws = new WebSocket(`ws://localhost:${PORT}/ws`); ws.q = []; ws.on('message', (d) => ws.q.push(JSON.parse(d))); return ws; } function nextMsg(ws, type, timeout = 3000) { return new Promise((resolve, reject) => { const start = Date.now(); (function poll() { const i = ws.q.findIndex((m) => m.type === type); if (i >= 0) return resolve(ws.q.splice(i, 1)[0]); if (Date.now() - start > timeout) return reject(new Error('timeout waiting for ' + type)); setTimeout(poll, 20); })(); }); } (async () => { await wait(300); // let server bind console.log('E2E backend tests:'); // 1. Register (first user becomes admin) const email = 'tech@example.com'; const reg = await call('/api/register', { email, password: 'supersecret', teamName: 'Acme IT' }); check('register succeeds', reg.status === 200 && reg.data.ok === true); // 2. Login -> session cookie (login marks the session MFA-passed) const login = await call('/api/login', { email, password: 'supersecret' }); check('login sets session cookie', !!login.cookie); const cookie = login.cookie; // 3. Protected route works right after login, role=admin const me = await get('/api/me', cookie); check('me works after login, role=admin', me.status === 200 && me.data.role === 'admin'); // 4. Wrong password rejected const badLogin = await call('/api/login', { email, password: 'wrong' }); check('wrong password rejected', badLogin.status === 401); // 8. Enroll a machine (consent-required) const mach = await call('/api/machines', { name: 'Dana-Laptop', unattended: false }, cookie); check('machine enrolled, returns token', mach.status === 200 && mach.data.enrollToken); const enrollToken = mach.data.enrollToken; // 9. Agent comes online const agent = wsClient(); await new Promise((r) => agent.on('open', r)); agent.send(JSON.stringify({ type: 'agent-hello', enrollToken })); const reg2 = await nextMsg(agent, 'agent-registered'); check('agent registers via enroll token', reg2.name === 'Dana-Laptop'); // machine shows online in API const machines = await get('/api/machines', cookie); check('machine reports online', machines.data[0].online === true); // 10. Technician (viewer) requests a session — needs cookie on the WS upgrade const viewer = new WebSocket(`ws://localhost:${PORT}/ws`, { headers: { Cookie: cookie } }); viewer.q = []; viewer.on('message', (d) => viewer.q.push(JSON.parse(d))); await new Promise((r) => viewer.on('open', r)); viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id })); const pending = await nextMsg(viewer, 'session-pending'); check('viewer gets session-pending', !!pending.sessionId); // 11. Agent receives the consent request const reqMsg = await nextMsg(agent, 'session-request'); check('agent receives session-request with technician email', reqMsg.technician === email); // 12. Agent grants consent -> both sides proceed agent.send(JSON.stringify({ type: 'consent', sessionId: reqMsg.sessionId, granted: true })); const ready = await nextMsg(viewer, 'session-ready'); const startStream = await nextMsg(agent, 'start-stream'); check('consent grant -> viewer session-ready', !!ready); check('consent grant -> agent start-stream', !!startStream); // 13. Signaling relay: agent offer reaches viewer; viewer answer reaches agent agent.send(JSON.stringify({ type: 'offer', sessionId: reqMsg.sessionId, sdp: { fake: 'offer' } })); const relayedOffer = await nextMsg(viewer, 'offer'); check('offer relayed agent->viewer', relayedOffer.sdp.fake === 'offer'); viewer.send(JSON.stringify({ type: 'answer', sessionId: reqMsg.sessionId, sdp: { fake: 'answer' } })); const relayedAnswer = await nextMsg(agent, 'answer'); check('answer relayed viewer->agent', relayedAnswer.sdp.fake === 'answer'); // 14. End session viewer.send(JSON.stringify({ type: 'end-session', sessionId: reqMsg.sessionId })); await nextMsg(agent, 'session-ended'); check('session-ended delivered to agent', true); // 15. Audit log captured the full flow const audit = await get('/api/audit', cookie); const actions = audit.data.map((a) => a.action); for (const a of ['user_registered', 'login', 'machine_enrolled', 'session_requested', 'consent_granted', 'session_ended']) { check(`audit contains "${a}"`, actions.includes(a)); } // 16. Denial path viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id })); const pending2 = await nextMsg(viewer, 'session-pending'); const req2 = await nextMsg(agent, 'session-request'); agent.send(JSON.stringify({ type: 'consent', sessionId: req2.sessionId, granted: false })); const denied = await nextMsg(viewer, 'session-denied'); check('consent denial -> viewer session-denied', !!denied); agent.close(); viewer.close(); console.log(`\n${passed} passed, ${failed} failed.`); server.close(); process.exit(failed ? 1 : 0); })().catch((e) => { console.error('E2E ERROR:', e); process.exit(1); });