ident
All repositories: gitoria
7.4 KB
// tests/shortid.mjs — THE SHORT ID GATE (ident#23): one public id per identity, the same in every app.// Own ident :8706 (fresh storage .scratch/shortid-gate), no browser: every step is HTTP.// * every identity (default and added ones) has 5 characters from the alphabet without look-alikes// * the identity page shows it; the ids are unique; the id never changes (edit, sign out of an app)// * two apps, one identity → the exchange gives the SAME id; another identity → another id// * notifications name the identity by it (case does not matter); a wrong id / another app's user → 404// * migrate-ids: strict body, wrong secret 401, nothing to migrate on a new storeimport { 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 { sleep } from './cdp.mjs';const APP = join(dirname(fileURLToPath(import.meta.url)), '..');const G = join(APP, '.scratch/shortid-gate');const ID = 'http://127.0.0.1:8706';const SITE = 'http://127.0.0.1:8707';const J = JSON.stringify;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 : '')); }};rmSync(G, { recursive: true, force: true });mkdirSync(G, { recursive: true });const p = spawn(join(APP, 'bin/hybriel'), ['project.hl'], {cwd: APP, stdio: ['ignore', 'pipe', 'pipe'],env: { ...process.env, SMTP_HOST: '', SMTP_USER: '', SMTP_PASSWORD: '', IDENT_PORT: '8706', IDENT_STORAGE: join(G, 'ident'), IDENT_SESSIONS: join(G, 'sess') + '/', IDENT_MAIL_SINK: join(G, 'mail.txt'), IDENT_IP_LIMIT: '1000', IDENT_IP_DAY_LIMIT: '1000' },});let log = ''; p.stdout.on('data', d => { log += d; }); p.stderr.on('data', d => { log += d; });const SHORT = /^[2-9a-hj-km-np-z]{5}$/;let emitI = 0;async function emit(event, payload, cookie) {const r = await fetch(ID + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', ...(cookie ? { cookie } : {}) }, body: J({ t: 'emit', i: ++emitI, event, payload }) });const j = JSON.parse(await r.text());return { cookie: (r.headers.get('set-cookie') || '').split(';')[0], value: j.value };}async function api(path, body) {const r = await fetch(ID + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: typeof body === 'string' ? body : J(body) });const t = await r.text(); let j = null; try { j = JSON.parse(t); } catch {}return { status: r.status, j, t };}const lastCode = (email) => readFileSync(join(G, 'mail.txt'), 'utf8').trim().split('\n').filter(l => l.startsWith(email + ' ')).pop().split(' ')[1];async function login(email) {await api('/api/code', { email });const v = await emit('verifyCode', [email, lastCode(email), 'UTC']);return { cookie: v.cookie, ids: v.value.identities };}async function appLogin(cookie, app, identity) {const l = await fetch(ID + '/login?key=' + app.key + '&return=' + encodeURIComponent(SITE + '/cb'), { redirect: 'manual' });const rid = (l.headers.get('location') || '').split('/').pop();const c = await emit('chooseIdentity', [rid, identity], cookie);const x = await api('/api/exchange', { key: app.key, secret: app.secret, code: new URL(c.value.url).searchParams.get('ident_code') });if (x.status !== 200) throw new Error('exchange failed: ' + x.t);return x.j.identity;}try {for (let i = 0; i < 80; i++) { try { await fetch(ID + '/', { redirect: 'manual' }); break; } catch {} await sleep(250); }const a = await login('[email protected]');const def = a.ids[0];check('the default identity has a short id: 5 characters, no look-alikes', SHORT.test(def.shortId), J(def));const added = (await emit('addIdentity', [{ identityName: 'Work' }], a.cookie)).value;const work = added.identities.find(i => i.identityName === 'Work');check('a new identity gets one too, another one', SHORT.test(work.shortId) && work.shortId !== def.shortId, J(added.identities));const b = await login('[email protected]');check('another account: another id', SHORT.test(b.ids[0].shortId) && ![def.shortId, work.shortId].includes(b.ids[0].shortId));const home = await (await fetch(ID + '/', { headers: { cookie: a.cookie } })).text();check('the identity page shows the id of each identity', home.includes('>' + def.shortId + '<') && home.includes('>' + work.shortId + '<'));// many ids: all valid, all differentconst many = [];for (let i = 0; i < 40; i++) many.push((await emit('addIdentity', [{ identityName: 'x' + i }], a.cookie)).value.identities.at(-1).shortId);check('40 more identities: all valid and different from each other', many.every(v => SHORT.test(v)) && new Set([...many, def.shortId, work.shortId]).size === 42);const noLookalike = many.join('') + def.shortId + work.shortId;check('no 0, 1, o, i or l anywhere', !/[01oil]/.test(noLookalike), noLookalike);const mk = async (name) => { const v = (await emit('appCreate', [{ name, origins: [SITE] }], a.cookie)).value; return { key: v.app.apiKey, secret: v.secret }; };const A = await mk('Chat'), B = await mk('Shop');const inA = await appLogin(a.cookie, A, def.id), inB = await appLogin(a.cookie, B, def.id);check('the same identity gets the SAME id in two apps, and it is its short id', inA === inB && inA === def.shortId, J([inA, inB, def.shortId]));const inAwork = await appLogin(a.cookie, A, work.id);check('another identity → another id', inAwork === work.shortId && inAwork !== inA, inAwork);const edit = (await emit('editIdentity', [def.id, { nickname: 'Al' }], a.cookie)).value;check('editing the identity does not change its id', edit.identities.find(i => i.id === def.id).shortId === def.shortId);const bIn = await appLogin(b.cookie, A, b.ids[0].id);check('bob in app A: his own short id, no email or names in the answer', bIn === b.ids[0].shortId);// notifications by the short idconst note = (app, identity) => api('/api/notify', { key: app.key, secret: app.secret, identity, name: 'New comment', text: 'hello' });let n = await note(A, def.shortId);check('notify by the short id → 200', n.status === 200 && n.j.id, n.t);n = await note(A, def.shortId.toUpperCase());check('capital letters do not matter', n.status === 200, n.t);n = await note(B, work.shortId);check('an identity that never logged in to this app → 404', n.status === 404, n.t);n = await note(A, 'zzzzz');check('an unknown id → 404', n.status === 404, n.t);n = await note(A, 'a0l1o');check('look-alike characters are no id → 404', n.status === 404, n.t);// the mapping endpointlet m = await api('/api/migrate-ids', { key: A.key, secret: A.secret });check('migrate-ids on a new store: nothing to map', m.status === 200 && J(m.j.ids) === '{}' && m.j.finished === false, m.t);m = await api('/api/migrate-ids', { key: A.key, secret: 'sk_' + '0'.repeat(48) });check('migrate-ids with a wrong secret → 401', m.status === 401, m.t);m = await api('/api/migrate-ids', { key: A.key, secret: A.secret, extra: 1 });check('migrate-ids with an unknown field → 400 naming it', m.status === 400 && /extra/.test(m.t), m.t);const g = await fetch(ID + '/api/migrate-ids');check('migrate-ids is POST only → 405', g.status === 405);} catch (err) {failed++; console.log(' FAIL (aborted) ' + err.stack);} finally {p.kill(); await sleep(300);writeFileSync(join(G, 'ident.log'), log);}console.log(`\n${passed} passed, ${failed} failed`);process.exit(failed ? 1 : 0);
Branches
- mainmain branch
Latest commits
- 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
- 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