gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit3691e1763691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre3691e176/tests/browser.mjs

8.1 KB

  1. // tests/browser.mjs — THE GATE of tracker.worldapi.org#1: a real ident copy (own storage,
  2. // own mail sink — no live ident, no real mail) and a real headless Chrome prove signed out
  3. // -> sign in -> homepage (identity + sign out) -> signed out again.
  4. //
  5. // Own servers only: ident dev copy .scratch/ident-dev (port from IDENT gate port), this app
  6. // (port from TRACKER gate port). Registering the app in ident (name + origin) is done once
  7. // over the emit API, exactly what a person does once by hand in ident's /apps page — the
  8. // gate then drives the actual login button flow through the real UI in the browser.
  9. //
  10. // Run: node tests/browser.mjs (exit 0 = all passed)
  11. import { spawn } from 'node:child_process';
  12. import { readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
  13. import { dirname, join } from 'node:path';
  14. import { fileURLToPath } from 'node:url';
  15. import { launchBrowser, sleep } from './cdp.mjs';
  16. const APP = join(dirname(fileURLToPath(import.meta.url)), '..');
  17. const IDENT_DIR = join(APP, '.scratch/ident-dev');
  18. const G = join(APP, '.scratch/browser-gate');
  19. const IDENT_PORT = 8703, TRACKER_PORT = 8704;
  20. const IDENT = 'http://127.0.0.1:' + IDENT_PORT;
  21. const TRACKER = 'http://127.0.0.1:' + TRACKER_PORT;
  22. const CHROME_PORTS = [8705, 8709];
  23. let passed = 0, failed = 0;
  24. const check = (name, ok, detail = '') => {
  25. if (ok) { passed++; console.log(' ok ' + name); }
  26. else { failed++; console.log(' FAIL ' + name + (detail ? ' — ' + detail : '')); }
  27. };
  28. // ---- servers --------------------------------------------------------------------------
  29. const procs = [];
  30. function start(cwd, env, log) {
  31. const p = spawn(join(cwd, 'bin/hybriel'), ['project.hl'], {
  32. cwd, env: { ...process.env, SMTP_HOST: '', SMTP_USER: '', SMTP_PASSWORD: '', ...env },
  33. stdio: ['ignore', 'pipe', 'pipe'],
  34. });
  35. let out = '';
  36. p.stdout.on('data', d => { out += d; }); p.stderr.on('data', d => { out += d; });
  37. p.on('exit', () => writeFileSync(join(G, log), out));
  38. procs.push({ p, log, out: () => out });
  39. return p;
  40. }
  41. async function up(url) {
  42. for (let i = 0; i < 80; i++) { try { await fetch(url + '/', { redirect: 'manual' }); return; } catch {} await sleep(250); }
  43. throw new Error('server did not come up: ' + url);
  44. }
  45. function stopAll() { for (const { p } of procs) { try { p.kill(); } catch {} } }
  46. rmSync(G, { recursive: true, force: true });
  47. mkdirSync(G, { recursive: true });
  48. mkdirSync(join(G, 'ident'), { recursive: true });
  49. start(IDENT_DIR, {
  50. IDENT_PORT: String(IDENT_PORT), IDENT_STORAGE: join(G, 'ident/store'), IDENT_SESSIONS: join(G, 'ident/sess') + '/',
  51. IDENT_MAIL_SINK: join(G, 'ident/mail.txt'), IDENT_IP_LIMIT: '1000', IDENT_IP_DAY_LIMIT: '1000', IDENT_WATCH: '0',
  52. }, 'ident.log');
  53. await up(IDENT);
  54. // ---- bootstrap: sign in to the ident dev copy and register THIS app (over the emit API — --
  55. // the one-time admin step a person does by hand in ident's /apps page) ------------------
  56. let emitI = 0;
  57. async function emit(base, event, payload, cookie) {
  58. const r = await fetch(base + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', ...(cookie ? { cookie } : {}) }, body: JSON.stringify({ t: 'emit', i: ++emitI, event, payload }) });
  59. const t = await r.text();
  60. let j = null; try { j = JSON.parse(t); } catch {}
  61. return { status: r.status, cookie: (r.headers.get('set-cookie') || '').split(';')[0], value: j && j.value, raw: t };
  62. }
  63. const lastCode = (email) => {
  64. const lines = readFileSync(join(G, 'ident/mail.txt'), 'utf8').trim().split('\n').filter(l => l.startsWith(email + ' '));
  65. return lines.length ? lines[lines.length - 1].split(' ')[1] : null;
  66. };
  67. async function apiLogin(email) {
  68. const q = await fetch(IDENT + '/api/code', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email }) });
  69. if (q.status !== 200) throw new Error('code request failed: ' + q.status + ' ' + await q.text());
  70. const v = await emit(IDENT, 'verifyCode', [email, lastCode(email), 'UTC']);
  71. if (!v.value || !v.value.account || !v.cookie) throw new Error('api login failed: ' + v.raw);
  72. return v.cookie;
  73. }
  74. const adminCookie = await apiLogin('[email protected]');
  75. const created = await emit(IDENT, 'appCreate', [{ name: 'tracker gate', origins: [TRACKER] }], adminCookie);
  76. if (created.value == null || created.value.error != null) { throw new Error('appCreate failed: ' + created.raw); }
  77. const KEY = created.value.app.apiKey, SECRET = created.value.secret;
  78. check('registered a test app in the ident dev copy', KEY != null && SECRET != null, created.raw);
  79. // ---- start tracker, configured with that key/secret ------------------------------------
  80. start(APP, {
  81. TRACKER_PORT: String(TRACKER_PORT), TRACKER_URL: TRACKER, IDENT_URL: IDENT,
  82. TRACKER_KEY: KEY, TRACKER_SECRET: SECRET, TRACKER_WATCH: '0',
  83. }, 'tracker.log');
  84. await up(TRACKER);
  85. // a fresh document (goto, or a full navigation like window.location.assign) needs its
  86. // socket hydrated before a click's `on click` handler is wired — a click that lands before
  87. // then falls through to the native, unhandled DOM event (a form's plain GET submit, ident's
  88. // own gates hit the same race: tests/apps.mjs `ready()`).
  89. async function ready(p) { await p.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { timeout: 15000, label: 'hydrated' }); }
  90. // ---- the real browser: signed out -> sign in -> homepage -> sign out ------------------
  91. const browser = await launchBrowser({ debugPortRange: CHROME_PORTS });
  92. try {
  93. const page = await browser.newPage();
  94. page.captureNetwork();
  95. await page.goto(TRACKER + '/');
  96. await ready(page);
  97. check('tracker home, signed out: shows the sign-in link', (await page.text('#signin')) === 'Sign in with ident');
  98. check('signed-out home has no identity/sign-out yet', (await page.evaluate("document.getElementById('identity') == null && document.getElementById('signout') == null")) === true);
  99. await page.click('#signin');
  100. await page.waitFor("location.pathname.startsWith('/signin/')", { timeout: 15000 });
  101. await ready(page);
  102. check('the login button sent the browser to ident (a fresh /signin/<rid>)', true);
  103. await page.type('#email', '[email protected]');
  104. await page.click('#sendcode');
  105. await page.waitFor("location.pathname.endsWith('/code')", { timeout: 15000 });
  106. await ready(page);
  107. const code = lastCode('[email protected]');
  108. check('ident mailed a one-time code to the sink', code != null, 'mail.txt: ' + readFileSync(join(G, 'ident/mail.txt'), 'utf8'));
  109. await page.type('#code', code);
  110. await page.click('#verify');
  111. // first login for this address: the optional-names welcome form, skip it
  112. await page.waitForSelector('#skip', { timeout: 15000 });
  113. await page.click('#skip');
  114. // one identity, for an app's login request: the choice section, one click
  115. await page.waitForSelector('#chooselist .choose', { timeout: 15000 });
  116. await page.click('#chooselist .choose');
  117. await page.waitFor("location.origin === '" + TRACKER + "'", { timeout: 15000 });
  118. await ready(page);
  119. check('back on tracker after the exchange', (await page.evaluate('location.origin')) === TRACKER + '' || (await page.evaluate('location.href')).startsWith(TRACKER));
  120. const identityText = await page.text('#identity');
  121. check('tracker homepage shows the signed-in identity', /^Signed in as \S+$/.test(identityText || ''), identityText);
  122. check('the sign-out button is there', (await page.text('#signout')) === 'Sign out');
  123. check('the empty homepage has no data (no shows, nothing else)', (await page.text('#empty')) === 'Nothing here yet.');
  124. await page.click('#signout');
  125. await page.waitForSelector('#signin', { timeout: 15000 });
  126. check('sign out returns to the signed-out homepage', (await page.text('#signin')) === 'Sign in with ident');
  127. check('signed out again: no identity shown', (await page.evaluate("document.getElementById('identity') == null")) === true);
  128. // a reload stays signed out (the app's own session, not just client state)
  129. await page.goto(TRACKER + '/');
  130. check('reload after sign-out stays signed out', (await page.text('#signin')) === 'Sign in with ident');
  131. const problems = page.problems();
  132. check('no console errors/warnings', problems.length === 0, page.dumpConsole());
  133. } finally {
  134. await browser.close();
  135. stopAll();
  136. }
  137. console.log('\n' + passed + ' passed, ' + failed + ' failed');
  138. process.exit(failed === 0 ? 0 : 1);

Branches

Latest commits

  • 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre