ba8bfc3f46
User-facing - New post-login home (/home): chat rail + Share/Connect (embedded) + Meeting; login lives here when logged out - Landing: "Log in with BizGaze" + no-login screen share - Console replaced by a role-scoped Dashboard (/dashboard): admins see all team sessions, others see only their own; stats + CSV/PDF export - Recordings saved as MP4 (H.264/AAC) with WebM fallback; old .webm still downloadable - Fix: duplicate "Sign in" on the login card Auth / integration - BizGaze as identity provider: /api/login validates against BIZGAZE_LOGIN_URL (env-gated) and provisions a local user - Phase 2 start: /api/v1 alias for all /api routes; Authorization: Bearer accepted across HTTP + WS; login returns a token (for native desktop/mobile clients) Backend refactor (Phase 1, behavior-preserving) - Split server.js into config/lib/session/presence/routes/static/signaling + repos (data-access) + bizgaze (service) - All SQL behind repos.js, tenant-scoped (tenantId == team_id for now) - e2e updated to current flow (21/21 pass before and after) Docs: ARCHITECTURE.md (target architecture + phased plan), CLAUDE.md repo layout, .env.example BIZGAZE_LOGIN_URL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 рядки
6.8 KiB
JavaScript
153 рядки
6.8 KiB
JavaScript
// 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); });
|