gitoriaLog in with ident

ident

All repositories: gitoria

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

7.4 KB

  1. // tests/shortid.mjs — THE SHORT ID GATE (ident#23): one public id per identity, the same in every app.
  2. // Own ident :8706 (fresh storage .scratch/shortid-gate), no browser: every step is HTTP.
  3. // * every identity (default and added ones) has 5 characters from the alphabet without look-alikes
  4. // * the identity page shows it; the ids are unique; the id never changes (edit, sign out of an app)
  5. // * two apps, one identity → the exchange gives the SAME id; another identity → another id
  6. // * notifications name the identity by it (case does not matter); a wrong id / another app's user → 404
  7. // * migrate-ids: strict body, wrong secret 401, nothing to migrate on a new store
  8. import { spawn } from 'node:child_process';
  9. import { readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
  10. import { dirname, join } from 'node:path';
  11. import { fileURLToPath } from 'node:url';
  12. import { sleep } from './cdp.mjs';
  13. const APP = join(dirname(fileURLToPath(import.meta.url)), '..');
  14. const G = join(APP, '.scratch/shortid-gate');
  15. const ID = 'http://127.0.0.1:8706';
  16. const SITE = 'http://127.0.0.1:8707';
  17. const J = JSON.stringify;
  18. let passed = 0, failed = 0;
  19. const check = (name, ok, detail = '') => {
  20. if (ok) { passed++; console.log(' ok ' + name); } else { failed++; console.log(' FAIL ' + name + (detail ? ' — ' + detail : '')); }
  21. };
  22. rmSync(G, { recursive: true, force: true });
  23. mkdirSync(G, { recursive: true });
  24. const p = spawn(join(APP, 'bin/hybriel'), ['project.hl'], {
  25. cwd: APP, stdio: ['ignore', 'pipe', 'pipe'],
  26. 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' },
  27. });
  28. let log = ''; p.stdout.on('data', d => { log += d; }); p.stderr.on('data', d => { log += d; });
  29. const SHORT = /^[2-9a-hj-km-np-z]{5}$/;
  30. let emitI = 0;
  31. async function emit(event, payload, cookie) {
  32. const r = await fetch(ID + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', ...(cookie ? { cookie } : {}) }, body: J({ t: 'emit', i: ++emitI, event, payload }) });
  33. const j = JSON.parse(await r.text());
  34. return { cookie: (r.headers.get('set-cookie') || '').split(';')[0], value: j.value };
  35. }
  36. async function api(path, body) {
  37. const r = await fetch(ID + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: typeof body === 'string' ? body : J(body) });
  38. const t = await r.text(); let j = null; try { j = JSON.parse(t); } catch {}
  39. return { status: r.status, j, t };
  40. }
  41. const lastCode = (email) => readFileSync(join(G, 'mail.txt'), 'utf8').trim().split('\n').filter(l => l.startsWith(email + ' ')).pop().split(' ')[1];
  42. async function login(email) {
  43. await api('/api/code', { email });
  44. const v = await emit('verifyCode', [email, lastCode(email), 'UTC']);
  45. return { cookie: v.cookie, ids: v.value.identities };
  46. }
  47. async function appLogin(cookie, app, identity) {
  48. const l = await fetch(ID + '/login?key=' + app.key + '&return=' + encodeURIComponent(SITE + '/cb'), { redirect: 'manual' });
  49. const rid = (l.headers.get('location') || '').split('/').pop();
  50. const c = await emit('chooseIdentity', [rid, identity], cookie);
  51. const x = await api('/api/exchange', { key: app.key, secret: app.secret, code: new URL(c.value.url).searchParams.get('ident_code') });
  52. if (x.status !== 200) throw new Error('exchange failed: ' + x.t);
  53. return x.j.identity;
  54. }
  55. try {
  56. for (let i = 0; i < 80; i++) { try { await fetch(ID + '/', { redirect: 'manual' }); break; } catch {} await sleep(250); }
  57. const a = await login('[email protected]');
  58. const def = a.ids[0];
  59. check('the default identity has a short id: 5 characters, no look-alikes', SHORT.test(def.shortId), J(def));
  60. const added = (await emit('addIdentity', [{ identityName: 'Work' }], a.cookie)).value;
  61. const work = added.identities.find(i => i.identityName === 'Work');
  62. check('a new identity gets one too, another one', SHORT.test(work.shortId) && work.shortId !== def.shortId, J(added.identities));
  63. const b = await login('[email protected]');
  64. check('another account: another id', SHORT.test(b.ids[0].shortId) && ![def.shortId, work.shortId].includes(b.ids[0].shortId));
  65. const home = await (await fetch(ID + '/', { headers: { cookie: a.cookie } })).text();
  66. check('the identity page shows the id of each identity', home.includes('>' + def.shortId + '<') && home.includes('>' + work.shortId + '<'));
  67. // many ids: all valid, all different
  68. const many = [];
  69. for (let i = 0; i < 40; i++) many.push((await emit('addIdentity', [{ identityName: 'x' + i }], a.cookie)).value.identities.at(-1).shortId);
  70. 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);
  71. const noLookalike = many.join('') + def.shortId + work.shortId;
  72. check('no 0, 1, o, i or l anywhere', !/[01oil]/.test(noLookalike), noLookalike);
  73. const mk = async (name) => { const v = (await emit('appCreate', [{ name, origins: [SITE] }], a.cookie)).value; return { key: v.app.apiKey, secret: v.secret }; };
  74. const A = await mk('Chat'), B = await mk('Shop');
  75. const inA = await appLogin(a.cookie, A, def.id), inB = await appLogin(a.cookie, B, def.id);
  76. 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]));
  77. const inAwork = await appLogin(a.cookie, A, work.id);
  78. check('another identity → another id', inAwork === work.shortId && inAwork !== inA, inAwork);
  79. const edit = (await emit('editIdentity', [def.id, { nickname: 'Al' }], a.cookie)).value;
  80. check('editing the identity does not change its id', edit.identities.find(i => i.id === def.id).shortId === def.shortId);
  81. const bIn = await appLogin(b.cookie, A, b.ids[0].id);
  82. check('bob in app A: his own short id, no email or names in the answer', bIn === b.ids[0].shortId);
  83. // notifications by the short id
  84. const note = (app, identity) => api('/api/notify', { key: app.key, secret: app.secret, identity, name: 'New comment', text: 'hello' });
  85. let n = await note(A, def.shortId);
  86. check('notify by the short id → 200', n.status === 200 && n.j.id, n.t);
  87. n = await note(A, def.shortId.toUpperCase());
  88. check('capital letters do not matter', n.status === 200, n.t);
  89. n = await note(B, work.shortId);
  90. check('an identity that never logged in to this app → 404', n.status === 404, n.t);
  91. n = await note(A, 'zzzzz');
  92. check('an unknown id → 404', n.status === 404, n.t);
  93. n = await note(A, 'a0l1o');
  94. check('look-alike characters are no id → 404', n.status === 404, n.t);
  95. // the mapping endpoint
  96. let m = await api('/api/migrate-ids', { key: A.key, secret: A.secret });
  97. check('migrate-ids on a new store: nothing to map', m.status === 200 && J(m.j.ids) === '{}' && m.j.finished === false, m.t);
  98. m = await api('/api/migrate-ids', { key: A.key, secret: 'sk_' + '0'.repeat(48) });
  99. check('migrate-ids with a wrong secret → 401', m.status === 401, m.t);
  100. m = await api('/api/migrate-ids', { key: A.key, secret: A.secret, extra: 1 });
  101. check('migrate-ids with an unknown field → 400 naming it', m.status === 400 && /extra/.test(m.t), m.t);
  102. const g = await fetch(ID + '/api/migrate-ids');
  103. check('migrate-ids is POST only → 405', g.status === 405);
  104. } catch (err) {
  105. failed++; console.log(' FAIL (aborted) ' + err.stack);
  106. } finally {
  107. p.kill(); await sleep(300);
  108. writeFileSync(join(G, 'ident.log'), log);
  109. }
  110. console.log(`\n${passed} passed, ${failed} failed`);
  111. process.exit(failed ? 1 : 0);

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