gitoriaLog in with ident

notes

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain2149e902notes mission 002 (4/4): code order — README file map + same-output test, STATUS, LOG, report; tests/letcount.py, tests/realdata-baseline.mjs, tests/realdata-compare.pymremain/tests/realdata-baseline.mjs

13.6 KB

  1. // tests/realdata-baseline.mjs (notes mission 002, code order; after gitoria's): every read the app answers + a fixed write
  2. // sequence, on a COPY of the LIVE storage, saved for diffing two code trees (a cleanup must answer the same). Compare two
  3. // outputs with tests/realdata-compare.py. README "Test" → "Same output".
  4. // node tests/realdata-baseline.mjs <tree> <port> <outdir> (kills only the server it started)
  5. // The live copy: NOTES_REALDATA (default .scratch/realdata) must hold storage/mpackdb, e.g. from Byrodin:
  6. // tar -C /CONTAINERS/projects/notes.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C <repo>/.scratch/realdata -xf -
  7. // Each run works on a fresh copy of it (<outdir>.run, removed after). Signed in as the creator (identity az5b2 → users @id
  8. // 0muh6o5a3kkd) through a session file written before the start; every page and API read is fetched signed in AND out.
  9. // ident is a FAKE on <port>+5 (POST /api/exchange: code abcdef01 → az5b2, abcdef02 → an invalid id, abcdef03 → 403,
  10. // else 400); the login routes and faces answer what they answer for that. The public URL is the same for every port
  11. // (http://notes.test), so two runs on two ports can be compared.
  12. import { spawn } from 'node:child_process';
  13. import { request, createServer } from 'node:http';
  14. import { writeFileSync, mkdirSync, rmSync, cpSync } from 'node:fs';
  15. import { createHash, randomBytes } from 'node:crypto';
  16. import { join, resolve } from 'node:path';
  17. const [tree, portArg, outArg] = process.argv.slice(2);
  18. const PORT = Number(portArg);
  19. const W = resolve(process.env.NOTES_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';
  20. const USER = '0muh6o5a3kkd';
  21. rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });
  22. mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(OUT, { recursive: true });
  23. cpSync(join(W, 'storage'), join(RUN, 'storage'), { recursive: true });
  24. // ---- the sessions: signed in, and a signed-out one (for the failed-login reason) -----------------------------------
  25. const now = Date.now();
  26. const session = (user, tag) => {
  27. const sid = randomBytes(16).toString('hex');
  28. writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),
  29. JSON.stringify({ created: now, data: { tag }, handled: [], id: sid, seen: now, user }), { mode: 0o600 });
  30. return 'notessid=' + sid;
  31. };
  32. const COOKIE = session({ id: USER }, 'rdtag1'), C2 = session(null, 'rdtag2');
  33. const sleep = ms => new Promise(r => setTimeout(r, ms));
  34. const HOST = `notes.test:${PORT}`;
  35. const IDENT = `http://127.0.0.1:${PORT + 5}`;
  36. const fakeIdent = createServer((req, res) => {
  37. let body = ''; req.on('data', d => body += d); req.on('end', () => {
  38. let j = {}; try { j = JSON.parse(body); } catch {}
  39. const answer = (status, v) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(v)); };
  40. if (req.method !== 'POST' || req.url !== '/api/exchange') return answer(404, { error: 'fake ident: not here' });
  41. if (j.key !== 'pk_rd' || j.secret !== 'sk_rd') return answer(403, { error: 'bad key' });
  42. if (j.code === 'abcdef01') return answer(200, { identity: 'az5b2' });
  43. if (j.code === 'abcdef02') return answer(200, { identity: 'Not Valid!' });
  44. if (j.code === 'abcdef03') return answer(403, { error: 'code expired' });
  45. return answer(400, { error: 'unknown code' });
  46. });
  47. });
  48. fakeIdent.listen(PORT + 5, '127.0.0.1');
  49. const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],
  50. env: { ...process.env, NOTES_PORT: String(PORT), NOTES_PUBLIC_URL: 'http://notes.test', NOTES_STORAGE: join(RUN, 'storage/mpackdb'),
  51. NOTES_SESSIONS: join(RUN, 'sessions') + '/', NOTES_WATCH: '0', IDENT_URL: 'http://ident.test', IDENT_EXCHANGE_URL: IDENT,
  52. IDENT_API_KEY: 'pk_rd', IDENT_API_SECRET: 'sk_rd' } });
  53. let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);
  54. const hreq = (method, path, headers = {}, body = null) => new Promise((res) => {
  55. const rq = request({ host: '127.0.0.1', port: PORT, path, method, headers: { host: HOST, ...headers } }, (r) => {
  56. const parts = []; r.on('data', d => parts.push(d)); r.on('end', () => res({ status: r.statusCode, headers: r.headers, body: Buffer.concat(parts).toString('utf8') }));
  57. });
  58. rq.on('error', (e) => res({ status: 0, headers: {}, body: 'ERROR ' + e.message }));
  59. if (body != null) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); }
  60. rq.end();
  61. });
  62. let n = 0;
  63. const file = (name, text) => writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), text);
  64. const save = async (name, method, path, headers = {}, body = null) => {
  65. const r = await hreq(method, path, headers, body);
  66. const head = [r.status, r.headers['content-type'] || '', r.headers['location'] || '', r.headers['cache-control'] || '', r.headers['set-cookie'] ? 'set-cookie' : ''].join(' | ');
  67. file(name, head + '\n' + r.body);
  68. return r;
  69. };
  70. let ei = 0;
  71. const emitF = async (name, event, payload, cookie = COOKIE) => {
  72. const r = await hreq('POST', '/__hl/emit', { 'content-type': 'application/json', cookie }, JSON.stringify({ t: 'emit', i: ++ei, event, payload }));
  73. file('w-face-' + name, r.status + '\n' + r.body);
  74. try { return JSON.parse(r.body).value; } catch { return null; }
  75. };
  76. const JH = { 'content-type': 'application/json', cookie: COOKIE };
  77. const post = async (name, body, cookie = COOKIE) => {
  78. const r = await save(name, 'POST', '/api/notes', { 'content-type': 'application/json', cookie }, typeof body === 'string' ? body : JSON.stringify(body));
  79. try { return JSON.parse(r.body); } catch { return {}; }
  80. };
  81. try {
  82. for (let i = 0; i < 240; i++) { if ((await hreq('GET', '/api/me')).status === 200) break; await sleep(500); }
  83. const list = JSON.parse((await save('api-notes', 'GET', '/api/notes', { cookie: COOKIE })).body).notes;
  84. // ---- READS ------------------------------------------------------------------------------------------------------
  85. const pages = ['/', '/note/nosuch', '/note/', '/login/failed', '/nope', '/login.js', '/sw.js', '/notes-offline.js', '/md-editor.js',
  86. '/favicon.ico', '/icons/icon-192.png', '/icons/icon-512.png', '/icons/apple-touch-icon.png', '/icons/favicon.svg',
  87. '/__hl/manifest.webmanifest', '/__hl/sw.js'];
  88. for (const x of list) pages.push('/note/' + x.id);
  89. const modules = new Set();
  90. for (const p of pages) {
  91. const r = await save('in-' + p, 'GET', p, { cookie: COOKIE }); await save('out-' + p, 'GET', p);
  92. for (const m of r.body.matchAll(/(\/(?:components|__hl)\/[\w./-]+\.(?:hl|js|css))\?v=[0-9a-f]+/g)) modules.add(m[0]);
  93. }
  94. for (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', m);
  95. const apis = ['/api/me', '/api/notes', '/api/notes?id=nosuch', '/api/notes?id='];
  96. for (const x of list) apis.push('/api/notes?id=' + x.id);
  97. for (const u of apis) { await save('api-in' + u, 'GET', u, { cookie: COOKIE }); await save('api-out' + u, 'GET', u); }
  98. // the refusals of POST /api/notes
  99. for (const [name, body] of [['empty', ''], ['badjson', '{"op":'], ['surrogate', '{"op":"save","text":"\\ud83d\\ude00"}'], ['array', '[1,2]'],
  100. ['string', '"x"'], ['null', 'null'], ['noop', '{}'], ['badop', '{"op":"rename"}'],
  101. ['save-nofields', '{"op":"save"}'], ['save-numid', '{"op":"save","id":1,"local":"","text":"","base":0,"at":0}'],
  102. ['save-negbase', '{"op":"save","id":"","local":"","text":"","base":-1,"at":0}'],
  103. ['save-longid', JSON.stringify({ op: 'save', id: 'x'.repeat(65), local: '', text: 'a', base: 0, at: 0 })],
  104. ['save-longlocal', JSON.stringify({ op: 'save', id: '', local: 'l'.repeat(65), text: 'a', base: 0, at: 0 })],
  105. ['save-toolong', JSON.stringify({ op: 'save', id: '', local: 'lrdlong', text: 'x'.repeat(200001), base: 0, at: 0 })],
  106. ['delete-nofields', '{"op":"delete"}'], ['delete-strbase', '{"op":"delete","id":"x","base":"1"}']])
  107. await post('post-' + name, body);
  108. await save('post-out', 'POST', '/api/notes', { 'content-type': 'application/json' }, '{"op":"save"}');
  109. await save('put', 'PUT', '/api/notes', JH, '{}');
  110. await save('delete-method', 'DELETE', '/api/notes', JH);
  111. await save('me-post', 'POST', '/api/me', JH, '{}');
  112. // the login's function route (the fake ident)
  113. for (const q of ['', '?ident_code=zz', '?ident_code=abc123', '?ident_code=abcdef02', '?ident_code=abcdef03', '?ident_code=abc&next=/note/x']) await save('callback' + q, 'GET', '/login/callback' + q);
  114. await save('callback-post', 'POST', '/login/callback', {}, 'x');
  115. await save('page-loginfailed-after', 'GET', '/login/failed');
  116. await save('callback-cookie', 'GET', '/login/callback?ident_code=abc123', { cookie: C2 });
  117. await save('page-loginfailed-cookie', 'GET', '/login/failed', { cookie: C2 });
  118. await save('callback-cookie-nocode', 'GET', '/login/callback', { cookie: C2 });
  119. await save('page-loginfailed-cookie2', 'GET', '/login/failed', { cookie: C2 });
  120. await save('callback-cookie-get-only', 'POST', '/login/callback', { cookie: C2 }, 'x');
  121. await save('page-loginfailed-cookie3', 'GET', '/login/failed', { cookie: C2 });
  122. await save('callback-cookie-expired', 'GET', '/login/callback?ident_code=abcdef03', { cookie: C2 });
  123. await save('page-loginfailed-cookie4', 'GET', '/login/failed', { cookie: C2 });
  124. // a login that works (the fake ident knows abcdef01 = az5b2): fresh browser → a new session; `next` → back to that path
  125. for (const q of ['', '&next=/note/x', '&next=//evil', '&next=/login/failed', '&next=/a%20b', '&next=/a<b', '&next=x']) {
  126. const r = await save('w-callback-ok' + q, 'GET', '/login/callback?ident_code=abcdef01' + q);
  127. const ck = (r.headers['set-cookie'] || [''])[0].split(';')[0];
  128. if (q === '') { await save('w-callback-ok-page', 'GET', '/', { cookie: ck }); await save('w-callback-ok-me', 'GET', '/api/me', { cookie: ck }); }
  129. }
  130. // the same in a browser that has a signed-out session: that session is signed in (no new cookie)
  131. await save('w-callback-ok-cookie', 'GET', '/login/callback?ident_code=abcdef01&next=/note/y', { cookie: C2 });
  132. await save('w-callback-ok-cookie-me', 'GET', '/api/me', { cookie: C2 });
  133. await save('w-callback-ok-cookie-failed', 'GET', '/login/failed', { cookie: C2 });
  134. // ---- WRITES ('w-' files; realdata-compare.py masks the new ids and the times) ---------------------------------------
  135. const a = await post('w-save-new', { op: 'save', id: '', local: 'lrd1', text: '# rd one\n\nbody', base: 0, at: 1700000000000 });
  136. await post('w-save-retry', { op: 'save', id: '', local: 'lrd1', text: '# rd one\n\nbody', base: 0, at: 1700000000000 });
  137. await post('w-save-same', { op: 'save', id: a.id, local: '', text: '# rd one\n\nbody', base: a.edited, at: 1700000001000 });
  138. const b = await post('w-save-edit', { op: 'save', id: a.id, local: '', text: '# rd one\n\nbody 2', base: a.edited, at: 1700000002000 });
  139. // conflict: base is old; the push is LATER than the server's version → the push wins, the server's version is a copy
  140. const c = await post('w-save-conflict-later', { op: 'save', id: a.id, local: '', text: 'rd later', base: a.edited, at: Date.now() - 1000 });
  141. // conflict: the push is OLDER → the server keeps its text, the push becomes a copy
  142. await post('w-save-conflict-older', { op: 'save', id: a.id, local: '', text: 'rd older', base: a.edited, at: 1600000000000 });
  143. await post('w-save-unknownid', { op: 'save', id: 'nosuch', local: '', text: 'rd unknown id', base: 5, at: 0 });
  144. await post('w-save-real', { op: 'save', id: list.find(x => !x.gone)?.id || 'none', local: '', text: 'rd overwrite?', base: 1, at: 1 });
  145. await post('w-save-out', { op: 'save', id: '', local: 'lrd2', text: 'x', base: 0, at: 0 }, 'notessid=none');
  146. await post('w-delete-wrongbase', { op: 'delete', id: a.id, base: 1 });
  147. await post('w-delete', { op: 'delete', id: a.id, base: c.edited ?? b.edited });
  148. await post('w-delete-again', { op: 'delete', id: a.id, base: 0 });
  149. await post('w-delete-nosuch', { op: 'delete', id: 'nosuch', base: 0 });
  150. await post('w-delete-empty', { op: 'delete', id: '', base: 0 });
  151. await post('w-save-after-delete', { op: 'save', id: a.id, local: '', text: 'rd back', base: 0, at: 0 });
  152. await post('w-save-oldlocal-again', { op: 'save', id: '', local: 'lrd1', text: 'rd local again', base: 0, at: 0 });
  153. await save('w-api-notes', 'GET', '/api/notes', { cookie: COOKIE });
  154. await save('w-api-one', 'GET', '/api/notes?id=' + a.id, { cookie: COOKIE });
  155. await save('w-page-main', 'GET', '/', { cookie: COOKIE });
  156. await save('w-page-note', 'GET', '/note/' + a.id, { cookie: COOKIE });
  157. // the faces: login with a code (ident down), logout, then the pages again
  158. await emitF('login-bad', 'notesLogin', ['zz']);
  159. await emitF('login-hex', 'notesLogin', ['abc123']);
  160. await emitF('login-invalid', 'notesLogin', ['abcdef02']);
  161. await emitF('login-out', 'notesLogin', ['abc123'], 'notessid=none');
  162. const C3 = session(null, 'rdtag3');
  163. await emitF('login-ok', 'notesLogin', ['abcdef01'], C3);
  164. await save('w-login-ok-me', 'GET', '/api/me', { cookie: C3 });
  165. await save('w-login-ok-page', 'GET', '/', { cookie: C3 });
  166. await emitF('logout', 'notesLogOut', []);
  167. await save('w-page-main-loggedout', 'GET', '/', { cookie: COOKIE });
  168. await save('w-api-me-loggedout', 'GET', '/api/me', { cookie: COOKIE });
  169. await save('w-api-notes-loggedout', 'GET', '/api/notes', { cookie: COOKIE });
  170. console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${apis.length} api urls x2, + writes)`);
  171. } finally {
  172. fakeIdent.close();
  173. srv.kill('SIGTERM'); await sleep(1000); try { process.kill(srv.pid, 'SIGKILL'); } catch {}
  174. }
  175. writeFileSync(join(OUT, '0000-server-log'), log.split('\n').filter(l => !/listening|watch/i.test(l)).join('\n'));
  176. rmSync(RUN, { recursive: true, force: true });

Branches

  • mainmain branch

Latest commits

  • 2149e902notes mission 002 (4/4): code order — README file map + same-output test, STATUS, LOG, report; tests/letcount.py, tests/realdata-baseline.mjs, tests/realdata-compare.pymre
  • 47db4fc1notes mission 002 (3/4): code order — let only where reassigned (58 dropped; 34 left: 19 reassigned, 15 loop-bound); gates 50/0 + 18/0, live-data run = step 2mre
  • 3f8cb383notes mission 002 (2/4): code order — topics, map, thin wrappers: lib/util.hl, lib/notes(-helpers).hl, lib/users.hl (+userOfLoginCode, tagOf), lib/api(-helpers).hl; project.hl = map; login routes take &req/&sessions (failed-login reason now kept); dead notes#1 functions removed; gates 50/0 + 18/0mre
  • 8cda5412notes mission 002 (1/4): code order — files moved: lib/notes.hl, lib/users.hl, lib/jsoncheck.hl, components/styles.hl (imports only); gates 50/0 + 18/0, live-data run identicalmre
  • 47dad68bnotes: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 50/0 + 18/0mre
  • a4a2b2aeantcolony#40: tracker missions moved too — references to them in missions/reports/LOG.md updatedmre
  • 3dea0ef2notes: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 50/0mre
  • e27c7d71notes: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy; audit: nothing to fix; gate 50/0)mre
  • 124613b6antcolony#40: mission references point to the moved missionsmre
  • 1ca2f34dantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 2bebebdanotes: Hybriel master ff51cf46 (re-vendor round)mre
  • 3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre
  • 9883c540deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
  • c8904061State of 2026-09-27, before the move to gitoriamre