tracker
All repositories: gitoria
8.1 KB
// tests/browser.mjs — THE GATE of tracker.worldapi.org#1: a real ident copy (own storage,// own mail sink — no live ident, no real mail) and a real headless Chrome prove signed out// -> sign in -> homepage (identity + sign out) -> signed out again.//// Own servers only: ident dev copy .scratch/ident-dev (port from IDENT gate port), this app// (port from TRACKER gate port). Registering the app in ident (name + origin) is done once// over the emit API, exactly what a person does once by hand in ident's /apps page — the// gate then drives the actual login button flow through the real UI in the browser.//// Run: node tests/browser.mjs (exit 0 = all passed)import { spawn } from 'node:child_process';import { readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';import { dirname, join } from 'node:path';import { fileURLToPath } from 'node:url';import { launchBrowser, sleep } from './cdp.mjs';const APP = join(dirname(fileURLToPath(import.meta.url)), '..');const IDENT_DIR = join(APP, '.scratch/ident-dev');const G = join(APP, '.scratch/browser-gate');const IDENT_PORT = 8703, TRACKER_PORT = 8704;const IDENT = 'http://127.0.0.1:' + IDENT_PORT;const TRACKER = 'http://127.0.0.1:' + TRACKER_PORT;const CHROME_PORTS = [8705, 8709];let passed = 0, failed = 0;const check = (name, ok, detail = '') => {if (ok) { passed++; console.log(' ok ' + name); }else { failed++; console.log(' FAIL ' + name + (detail ? ' — ' + detail : '')); }};// ---- servers --------------------------------------------------------------------------const procs = [];function start(cwd, env, log) {const p = spawn(join(cwd, 'bin/hybriel'), ['project.hl'], {cwd, env: { ...process.env, SMTP_HOST: '', SMTP_USER: '', SMTP_PASSWORD: '', ...env },stdio: ['ignore', 'pipe', 'pipe'],});let out = '';p.stdout.on('data', d => { out += d; }); p.stderr.on('data', d => { out += d; });p.on('exit', () => writeFileSync(join(G, log), out));procs.push({ p, log, out: () => out });return p;}async function up(url) {for (let i = 0; i < 80; i++) { try { await fetch(url + '/', { redirect: 'manual' }); return; } catch {} await sleep(250); }throw new Error('server did not come up: ' + url);}function stopAll() { for (const { p } of procs) { try { p.kill(); } catch {} } }rmSync(G, { recursive: true, force: true });mkdirSync(G, { recursive: true });mkdirSync(join(G, 'ident'), { recursive: true });start(IDENT_DIR, {IDENT_PORT: String(IDENT_PORT), IDENT_STORAGE: join(G, 'ident/store'), IDENT_SESSIONS: join(G, 'ident/sess') + '/',IDENT_MAIL_SINK: join(G, 'ident/mail.txt'), IDENT_IP_LIMIT: '1000', IDENT_IP_DAY_LIMIT: '1000', IDENT_WATCH: '0',}, 'ident.log');await up(IDENT);// ---- bootstrap: sign in to the ident dev copy and register THIS app (over the emit API — --// the one-time admin step a person does by hand in ident's /apps page) ------------------let emitI = 0;async function emit(base, event, payload, cookie) {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 }) });const t = await r.text();let j = null; try { j = JSON.parse(t); } catch {}return { status: r.status, cookie: (r.headers.get('set-cookie') || '').split(';')[0], value: j && j.value, raw: t };}const lastCode = (email) => {const lines = readFileSync(join(G, 'ident/mail.txt'), 'utf8').trim().split('\n').filter(l => l.startsWith(email + ' '));return lines.length ? lines[lines.length - 1].split(' ')[1] : null;};async function apiLogin(email) {const q = await fetch(IDENT + '/api/code', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email }) });if (q.status !== 200) throw new Error('code request failed: ' + q.status + ' ' + await q.text());const v = await emit(IDENT, 'verifyCode', [email, lastCode(email), 'UTC']);if (!v.value || !v.value.account || !v.cookie) throw new Error('api login failed: ' + v.raw);return v.cookie;}const adminCookie = await apiLogin('[email protected]');const created = await emit(IDENT, 'appCreate', [{ name: 'tracker gate', origins: [TRACKER] }], adminCookie);if (created.value == null || created.value.error != null) { throw new Error('appCreate failed: ' + created.raw); }const KEY = created.value.app.apiKey, SECRET = created.value.secret;check('registered a test app in the ident dev copy', KEY != null && SECRET != null, created.raw);// ---- start tracker, configured with that key/secret ------------------------------------start(APP, {TRACKER_PORT: String(TRACKER_PORT), TRACKER_URL: TRACKER, IDENT_URL: IDENT,TRACKER_KEY: KEY, TRACKER_SECRET: SECRET, TRACKER_WATCH: '0',}, 'tracker.log');await up(TRACKER);// a fresh document (goto, or a full navigation like window.location.assign) needs its// socket hydrated before a click's `on click` handler is wired — a click that lands before// then falls through to the native, unhandled DOM event (a form's plain GET submit, ident's// own gates hit the same race: tests/apps.mjs `ready()`).async function ready(p) { await p.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { timeout: 15000, label: 'hydrated' }); }// ---- the real browser: signed out -> sign in -> homepage -> sign out ------------------const browser = await launchBrowser({ debugPortRange: CHROME_PORTS });try {const page = await browser.newPage();page.captureNetwork();await page.goto(TRACKER + '/');await ready(page);check('tracker home, signed out: shows the sign-in link', (await page.text('#signin')) === 'Sign in with ident');check('signed-out home has no identity/sign-out yet', (await page.evaluate("document.getElementById('identity') == null && document.getElementById('signout') == null")) === true);await page.click('#signin');await page.waitFor("location.pathname.startsWith('/signin/')", { timeout: 15000 });await ready(page);check('the login button sent the browser to ident (a fresh /signin/<rid>)', true);await page.type('#email', '[email protected]');await page.click('#sendcode');await page.waitFor("location.pathname.endsWith('/code')", { timeout: 15000 });await ready(page);const code = lastCode('[email protected]');check('ident mailed a one-time code to the sink', code != null, 'mail.txt: ' + readFileSync(join(G, 'ident/mail.txt'), 'utf8'));await page.type('#code', code);await page.click('#verify');// first login for this address: the optional-names welcome form, skip itawait page.waitForSelector('#skip', { timeout: 15000 });await page.click('#skip');// one identity, for an app's login request: the choice section, one clickawait page.waitForSelector('#chooselist .choose', { timeout: 15000 });await page.click('#chooselist .choose');await page.waitFor("location.origin === '" + TRACKER + "'", { timeout: 15000 });await ready(page);check('back on tracker after the exchange', (await page.evaluate('location.origin')) === TRACKER + '' || (await page.evaluate('location.href')).startsWith(TRACKER));const identityText = await page.text('#identity');check('tracker homepage shows the signed-in identity', /^Signed in as \S+$/.test(identityText || ''), identityText);check('the sign-out button is there', (await page.text('#signout')) === 'Sign out');check('the empty homepage has no data (no shows, nothing else)', (await page.text('#empty')) === 'Nothing here yet.');await page.click('#signout');await page.waitForSelector('#signin', { timeout: 15000 });check('sign out returns to the signed-out homepage', (await page.text('#signin')) === 'Sign in with ident');check('signed out again: no identity shown', (await page.evaluate("document.getElementById('identity') == null")) === true);// a reload stays signed out (the app's own session, not just client state)await page.goto(TRACKER + '/');check('reload after sign-out stays signed out', (await page.text('#signin')) === 'Sign in with ident');const problems = page.problems();check('no console errors/warnings', problems.length === 0, page.dumpConsole());} finally {await browser.close();stopAll();}console.log('\n' + passed + ' passed, ' + failed + ' failed');process.exit(failed === 0 ? 0 : 1);
Branches
- mainmain branch
Latest commits
- 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre