Bez popisu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

e2e.js 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. // End-to-end test of the backend platform.
  2. // Exercises the full flow: register -> login -> enroll machine -> agent online ->
  3. // technician requests session -> consent -> signaling relay -> audit trail.
  4. // No browser/Electron needed: the "agent" and "viewer" are raw WebSocket clients.
  5. // (Login currently marks the session MFA-passed directly, so there is no separate
  6. // TOTP step in the product flow; the MFA endpoints still exist but aren't exercised here.)
  7. const fs = require('fs');
  8. const os = require('os');
  9. const path = require('path');
  10. const DB = path.join(os.tmpdir(), 'ra-e2e.db');
  11. process.env.DB_PATH = DB;
  12. for (const f of [DB, DB + '-wal', DB + '-shm']) { try { fs.unlinkSync(f); } catch {} }
  13. const PORT = 8099;
  14. process.env.PORT = PORT;
  15. process.env.HTTPS_PORT = 8444; // avoid clashing with a running dev server on 8443
  16. const { server } = require('../server');
  17. const A = require('../auth');
  18. const WebSocket = require('ws');
  19. const BASE = `http://localhost:${PORT}`;
  20. let passed = 0, failed = 0;
  21. function check(name, cond) {
  22. if (cond) { console.log(' ok -', name); passed++; }
  23. else { console.log(' FAIL-', name); failed++; }
  24. }
  25. // minimal cookie-aware fetch
  26. async function call(path, body, cookie) {
  27. const r = await fetch(BASE + path, {
  28. method: 'POST',
  29. headers: { 'Content-Type': 'application/json', ...(cookie ? { Cookie: cookie } : {}) },
  30. body: body ? JSON.stringify(body) : undefined,
  31. });
  32. const setCookie = r.headers.get('set-cookie');
  33. const data = await r.json().catch(() => ({}));
  34. return { status: r.status, data, cookie: setCookie ? setCookie.split(';')[0] : cookie };
  35. }
  36. async function get(path, cookie) {
  37. const r = await fetch(BASE + path, { headers: cookie ? { Cookie: cookie } : {} });
  38. return { status: r.status, data: await r.json().catch(() => ({})) };
  39. }
  40. const wait = (ms) => new Promise((r) => setTimeout(r, ms));
  41. function wsClient() {
  42. const ws = new WebSocket(`ws://localhost:${PORT}/ws`);
  43. ws.q = [];
  44. ws.on('message', (d) => ws.q.push(JSON.parse(d)));
  45. return ws;
  46. }
  47. function nextMsg(ws, type, timeout = 3000) {
  48. return new Promise((resolve, reject) => {
  49. const start = Date.now();
  50. (function poll() {
  51. const i = ws.q.findIndex((m) => m.type === type);
  52. if (i >= 0) return resolve(ws.q.splice(i, 1)[0]);
  53. if (Date.now() - start > timeout) return reject(new Error('timeout waiting for ' + type));
  54. setTimeout(poll, 20);
  55. })();
  56. });
  57. }
  58. (async () => {
  59. await wait(300); // let server bind
  60. console.log('E2E backend tests:');
  61. // 1. Register (first user becomes admin)
  62. const email = 'tech@example.com';
  63. const reg = await call('/api/register', { email, password: 'supersecret', teamName: 'Acme IT' });
  64. check('register succeeds', reg.status === 200 && reg.data.ok === true);
  65. // 2. Login -> session cookie (login marks the session MFA-passed)
  66. const login = await call('/api/login', { email, password: 'supersecret' });
  67. check('login sets session cookie', !!login.cookie);
  68. const cookie = login.cookie;
  69. // 3. Protected route works right after login, role=admin
  70. const me = await get('/api/me', cookie);
  71. check('me works after login, role=admin', me.status === 200 && me.data.role === 'admin');
  72. // 4. Wrong password rejected
  73. const badLogin = await call('/api/login', { email, password: 'wrong' });
  74. check('wrong password rejected', badLogin.status === 401);
  75. // 8. Enroll a machine (consent-required)
  76. const mach = await call('/api/machines', { name: 'Dana-Laptop', unattended: false }, cookie);
  77. check('machine enrolled, returns token', mach.status === 200 && mach.data.enrollToken);
  78. const enrollToken = mach.data.enrollToken;
  79. // 9. Agent comes online
  80. const agent = wsClient();
  81. await new Promise((r) => agent.on('open', r));
  82. agent.send(JSON.stringify({ type: 'agent-hello', enrollToken }));
  83. const reg2 = await nextMsg(agent, 'agent-registered');
  84. check('agent registers via enroll token', reg2.name === 'Dana-Laptop');
  85. // machine shows online in API
  86. const machines = await get('/api/machines', cookie);
  87. check('machine reports online', machines.data[0].online === true);
  88. // 10. Technician (viewer) requests a session — needs cookie on the WS upgrade
  89. const viewer = new WebSocket(`ws://localhost:${PORT}/ws`, { headers: { Cookie: cookie } });
  90. viewer.q = []; viewer.on('message', (d) => viewer.q.push(JSON.parse(d)));
  91. await new Promise((r) => viewer.on('open', r));
  92. viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id }));
  93. const pending = await nextMsg(viewer, 'session-pending');
  94. check('viewer gets session-pending', !!pending.sessionId);
  95. // 11. Agent receives the consent request
  96. const reqMsg = await nextMsg(agent, 'session-request');
  97. check('agent receives session-request with technician email', reqMsg.technician === email);
  98. // 12. Agent grants consent -> both sides proceed
  99. agent.send(JSON.stringify({ type: 'consent', sessionId: reqMsg.sessionId, granted: true }));
  100. const ready = await nextMsg(viewer, 'session-ready');
  101. const startStream = await nextMsg(agent, 'start-stream');
  102. check('consent grant -> viewer session-ready', !!ready);
  103. check('consent grant -> agent start-stream', !!startStream);
  104. // 13. Signaling relay: agent offer reaches viewer; viewer answer reaches agent
  105. agent.send(JSON.stringify({ type: 'offer', sessionId: reqMsg.sessionId, sdp: { fake: 'offer' } }));
  106. const relayedOffer = await nextMsg(viewer, 'offer');
  107. check('offer relayed agent->viewer', relayedOffer.sdp.fake === 'offer');
  108. viewer.send(JSON.stringify({ type: 'answer', sessionId: reqMsg.sessionId, sdp: { fake: 'answer' } }));
  109. const relayedAnswer = await nextMsg(agent, 'answer');
  110. check('answer relayed viewer->agent', relayedAnswer.sdp.fake === 'answer');
  111. // 14. End session
  112. viewer.send(JSON.stringify({ type: 'end-session', sessionId: reqMsg.sessionId }));
  113. await nextMsg(agent, 'session-ended');
  114. check('session-ended delivered to agent', true);
  115. // 15. Audit log captured the full flow
  116. const audit = await get('/api/audit', cookie);
  117. const actions = audit.data.map((a) => a.action);
  118. for (const a of ['user_registered', 'login', 'machine_enrolled', 'session_requested', 'consent_granted', 'session_ended']) {
  119. check(`audit contains "${a}"`, actions.includes(a));
  120. }
  121. // 16. Denial path
  122. viewer.send(JSON.stringify({ type: 'viewer-connect', machineId: machines.data[0].id }));
  123. const pending2 = await nextMsg(viewer, 'session-pending');
  124. const req2 = await nextMsg(agent, 'session-request');
  125. agent.send(JSON.stringify({ type: 'consent', sessionId: req2.sessionId, granted: false }));
  126. const denied = await nextMsg(viewer, 'session-denied');
  127. check('consent denial -> viewer session-denied', !!denied);
  128. agent.close(); viewer.close();
  129. console.log(`\n${passed} passed, ${failed} failed.`);
  130. server.close();
  131. process.exit(failed ? 1 : 0);
  132. })().catch((e) => { console.error('E2E ERROR:', e); process.exit(1); });