gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit836f644f836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre836f644f/tests/signoutall.mjs

5.1 KB

  1. // tests/signoutall.mjs — ticket ident#19 gate: "Sign out everywhere" makes every OPEN page on
  2. // every device switch to signed out at once (no click), and the sessions are deleted.
  3. // Own server :8702, two real headless Chromes (two cookie jars = two devices; debug ports
  4. // 8703-8709, --disable-gpu, closed at the end). Home page AND /apps page are open on device A.
  5. // node tests/signoutall.mjs
  6. import { spawn } from 'node:child_process';
  7. import { rmSync, mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync } from 'node:fs';
  8. import { dirname, join, resolve } from 'node:path';
  9. import { fileURLToPath } from 'node:url';
  10. import { launchBrowser } from './cdp.mjs';
  11. if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';
  12. const APP = resolve(dirname(fileURLToPath(import.meta.url)), '..');
  13. const G = join(APP, '.scratch/signoutall-gate');
  14. rmSync(G, { recursive: true, force: true });
  15. mkdirSync(join(G, 'sess'), { recursive: true });
  16. const PORT = 8702, BASE = `http://127.0.0.1:${PORT}`, SINK = join(G, 'mail.txt');
  17. const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  18. let passed = 0, failed = 0;
  19. const check = (n, ok, d = '') => { if (ok) { passed++; console.log('ok ' + n); } else { failed++; console.log('FAIL ' + n + (d ? ' — ' + d : '')); } };
  20. let log = '';
  21. const srv = spawn(join(APP, 'bin/hybriel'), ['project.hl'], { cwd: APP, env: { ...process.env, SMTP_HOST: '', IDENT_PORT: String(PORT), IDENT_STORAGE: join(G, 'store'), IDENT_SESSIONS: join(G, 'sess'), IDENT_MAIL_SINK: SINK, IDENT_IP_LIMIT: '1000', IDENT_IP_DAY_LIMIT: '1000' }, stdio: ['ignore', 'pipe', 'pipe'] });
  22. srv.stdout.on('data', (d) => log += d); srv.stderr.on('data', (d) => log += d);
  23. const browsers = [];
  24. async function finish() {
  25. for (const b of browsers) { try { await b.close(); } catch {} }
  26. try { process.kill(srv.pid); } catch {}
  27. writeFileSync(join(G, 'server.log'), log);
  28. console.log(`\n${passed} passed, ${failed} failed`);
  29. process.exit(failed ? 1 : 0);
  30. }
  31. try {
  32. for (let i = 0; i < 80; i++) { try { await fetch(BASE + '/'); break; } catch {} await sleep(250); }
  33. const email = '[email protected]';
  34. const code = async () => { for (let i = 0; i < 30; i++) { if (existsSync(SINK)) { const l = readFileSync(SINK, 'utf8').split('\n').filter((x) => x.startsWith(email + ' ')); if (l.length) return l.at(-1).split(' ')[1]; } await sleep(100); } };
  35. const connected = (p) => p.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1');
  36. async function login(p) {
  37. await p.goto(BASE + '/');
  38. await p.waitForSelector('#email'); await connected(p);
  39. await p.type('#email', email, { clear: true });
  40. await p.click('#sendcode');
  41. await p.waitForSelector('#code');
  42. await connected(p); // ident#20: the code step is its own page (/code) — wait for it to hydrate
  43. await sleep(300);
  44. await p.type('#code', await code(), { clear: true });
  45. await p.click('#verify');
  46. await p.waitForSelector('#signoutall');
  47. }
  48. const A = await launchBrowser({ debugPortRange: [8703, 8706] }); browsers.push(A);
  49. const B = await launchBrowser({ debugPortRange: [8707, 8709] }); browsers.push(B);
  50. const a1 = await A.newPage(), a2 = await A.newPage(), b1 = await B.newPage();
  51. await login(a1);
  52. await login(b1);
  53. await a2.goto(BASE + '/apps');
  54. await a2.waitFor('!!document.querySelector("#newapp, button")'); await connected(a2);
  55. await connected(a1);
  56. check('device A home + apps pages and device B are signed in', await a1.evaluate('!!document.querySelector("#signoutall")') && !(await a2.evaluate('!!document.querySelector("#email")')));
  57. check('two session files carry the account', readdirSync(join(G, 'sess')).filter((f) => JSON.parse(readFileSync(join(G, 'sess', f), 'utf8')).user).length === 2);
  58. b1.conn.onEvent((m) => { if (m.method === 'Page.javascriptDialogOpening' && m.sessionId === b1.sessionId) b1.send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {}); });
  59. await b1.click('#signoutall');
  60. // NO click on device A: the pages must switch by themselves
  61. await a1.waitFor('!!document.querySelector("#email") && !document.querySelector("#signoutall")', { timeout: 8000, label: 'device A home signed out' }).then(() => check('device A home page switched to signed out without a click', true), (e) => check('device A home page switched to signed out without a click', false, e.message));
  62. await a2.waitFor('location.pathname === "/apps" && !!document.querySelector("#email, .lead")', { timeout: 8000 }).catch(() => {});
  63. const a2text = await a2.evaluate('document.body.innerText');
  64. check('device A apps page shows signed out too', !/Sign out everywhere/.test(a2text), a2text.slice(0, 200));
  65. await b1.waitFor('!!document.querySelector("#email")', { timeout: 8000 }).then(() => check('device B (the caller) is signed out', true), (e) => check('device B (the caller) is signed out', false, e.message));
  66. check('no session file carries the account any more', readdirSync(join(G, 'sess')).filter((f) => JSON.parse(readFileSync(join(G, 'sess', f), 'utf8')).user).length === 0);
  67. } catch (e) { check('gate ran without an exception', false, e.stack || e.message); }
  68. await finish();

Branches

Latest commits

  • 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
  • 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
  • 81b15b7bState of 2026-09-27, before the move to gitoriamre