Changeset 61d52ed in Klonkt for test/thread.test.js


Ignore:
Timestamp:
10/14/2026 04:25:21 PM (6 hours ago)
Author:
Bart <bart@…>
Branches:
main
Parents:
6e9bc70
git-author:
Bart <bart@…> (10/14/2026 04:23:29 PM)
git-committer:
Bart <bart@…> (10/14/2026 04:25:21 PM)
Message:

Eén note ophalen, voor wie een gesprek naar boven volgt

De thread-route (shaer-tqz) geeft wat ONDER een post hangt. Een lezer die een
antwoord kreeg en de ouder niet, kon die ouder bij een server in secure mode
niet halen: daar telt alleen een ondertekend verzoek, en de sleutel woont hier.
R9999 haalt ouders voortaan op zodra een antwoord binnenkomt, en heeft daar
deze route voor nodig -- zonder anonieme terugval, om dezelfde reden als bij de
thread.

GET /ap/users/:slug/object?uri= -- bearer-only en niets bewaren, dezelfde
weigering-of-storing (404/502) en dezelfde poorten als de thread. Een
geblokkeerde auteur is er niet. Voor een ward met een dichte kring is een note
van buiten de kring een 404 'outside the circle', met shaer:hidden erbij in
plaats van verzwegen.

inReplyTo alleen als de bron hem gaf. De thread vult een ontbrekende inReplyTo
aan met de post waaronder het antwoord in de replies-collectie stond, en daar is
dat bewijs. Voor een losse note is er geen bewijs, en een verzonnen ouder is
erger dan geen.

De vorm van één note (readerNote) en de poorten (readerGates) zijn uit de
thread getild, zodat de twee niet uit elkaar kunnen lopen. De thread-route
gedraagt zich hetzelfde.

Tests: getObject (de ouder van de bron, geen verzonnen ouder voor een wortel,
blokkade, weigering) en de route (zonder bearer, andermans bearer, alleen
https, weigering is 404).

Co-Authored-By: Claude Opus 5.5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/thread.test.js

    r6e9bc70 r61d52ed  
    240240  assert.equal(broken.body.error, 'source unreachable');
    241241});
     242
     243// ── Eén note, naar boven (de object-route) ───────────────────────────
     244//
     245// De thread geeft alleen wat onder een post hangt. Een lezer die een antwoord
     246// kreeg en de ouder niet, vraagt de ouder hier. Wat hier stil kan breken: een
     247// ouder die verzonnen wordt (de thread-fallback die meelekt), een geblokkeerde
     248// die toch zichtbaar wordt, en een route zonder bearer-check.
     249test('getObject geeft de note zelf, met alleen de ouder die de bron noemde', async (t) => {
     250  const vorige = globalThis.fetch;
     251  t.after(() => { globalThis.fetch = vorige; });
     252  const KIND_REPLY = 'https://203.0.113.60/notes/antwoord';
     253  const WORTEL = 'https://203.0.113.60/notes/wortel';
     254  globalThis.fetch = async (url) => {
     255    const u = String(url);
     256    const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/activity+json' } });
     257    if (u === KIND_REPLY) return json({ id: KIND_REPLY, type: 'Note', attributedTo: TANTE, inReplyTo: WORTEL, content: '<p>ja <script>x</script></p>', published: '2026-09-07T13:42:26Z' });
     258    if (u === WORTEL) return json({ id: WORTEL, type: 'Note', attributedTo: TANTE, content: '<p>begin</p>' });
     259    if (u === TANTE) return json(actorDoc(TANTE, 'tante'));
     260    return new Response('not found', { status: 404 });
     261  };
     262
     263  const antwoord = await AP.getObject('kind', KIND_REPLY);
     264  assert.equal(antwoord.found, true);
     265  assert.equal(antwoord.note.inReplyTo, WORTEL, 'de ouder die de bron gaf');
     266  assert.equal(antwoord.note.attributedTo.name, 'tante');
     267  assert.ok(!antwoord.note.content.includes('<script'), 'geschoond');
     268
     269  const wortel = await AP.getObject('kind', WORTEL);
     270  assert.equal(wortel.found, true);
     271  assert.equal(wortel.note.inReplyTo, undefined, 'geen ouder verzonnen voor een wortel');
     272});
     273
     274test('getObject: een geblokkeerde auteur is er niet, een weigering is een weigering', async (t) => {
     275  const vorige = globalThis.fetch;
     276  t.after(() => { globalThis.fetch = vorige; });
     277  const VAN_NAAR = 'https://203.0.113.40/notes/van-naar';
     278  globalThis.fetch = async (url) => (String(url) === VAN_NAAR
     279    ? new Response(JSON.stringify({ id: VAN_NAAR, type: 'Note', attributedTo: GEBLOKT, content: 'x' }), { status: 200 })
     280    : new Response('nope', { status: 410 }));
     281
     282  const geblokt = await AP.getObject('kind', VAN_NAAR);
     283  assert.equal(geblokt.found, false);
     284  assert.equal(geblokt.note, null);
     285
     286  const weg = await AP.getObject('kind', 'https://203.0.113.61/notes/weg');
     287  assert.equal(weg.found, false);
     288  assert.equal(weg.sourceStatus, 410);
     289});
     290
     291test('de object-route: alleen met de eigen bearer, en een weigering is 404', async (t) => {
     292  const crypto = await import('crypto');
     293  const express = (await import('express')).default;
     294  const routes = (await import('../src/routes/activitypub.js')).default;
     295
     296  // Een eigen site zonder guardians, zodat de poorten van de tests hierboven
     297  // (de ward met de dichte emoji-poort) hier niet meetellen.
     298  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s-lezer', 'lezer', 'Lezer', 'u1');
     299  const bearer = 'test-token-' + 'e'.repeat(24);
     300  db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
     301    .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'lezer', 'read write');
     302
     303  const app = express();
     304  app.use(routes);
     305  const server = app.listen(0);
     306  t.after(() => server.close());
     307  await new Promise((r) => server.once('listening', r));
     308
     309  const vorige = globalThis.fetch;
     310  t.after(() => { globalThis.fetch = vorige; });
     311  const OUDER = 'https://203.0.113.62/notes/ouder';
     312  globalThis.fetch = async (url) => {
     313    const u = String(url);
     314    if (u === OUDER) return new Response(JSON.stringify({ id: OUDER, type: 'Note', attributedTo: VREEMDE, content: '<p>de ouder</p>', inReplyTo: BRON }), { status: 200 });
     315    if (u === VREEMDE) return new Response(JSON.stringify(actorDoc(VREEMDE, 'vreemde')), { status: 200 });
     316    return new Response('nope', { status: 404 });
     317  };
     318  const ask = async (slug, uri, auth = `Bearer ${bearer}`) => {
     319    const r = await realFetch(
     320      `http://127.0.0.1:${server.address().port}/ap/users/${slug}/object?uri=${encodeURIComponent(uri)}`,
     321      { headers: auth ? { Authorization: auth } : {} });
     322    return { status: r.status, body: r.status === 403 ? null : await r.json() };
     323  };
     324
     325  assert.equal((await ask('lezer', OUDER, null)).status, 403, 'zonder bearer niets');
     326  assert.equal((await ask('kind', OUDER)).status, 403, 'niet met de bearer van een ander');
     327  assert.equal((await ask('lezer', 'http://203.0.113.62/notes/ouder')).status, 400, 'alleen https');
     328
     329  const ok = await ask('lezer', OUDER);
     330  assert.equal(ok.status, 200);
     331  assert.equal(ok.body.id, OUDER);
     332  assert.equal(ok.body.inReplyTo, BRON);
     333  assert.equal(ok.body.attributedTo.name, 'vreemde');
     334
     335  const dicht = await ask('lezer', 'https://203.0.113.63/notes/dicht');
     336  assert.equal(dicht.status, 404);
     337  assert.equal(dicht.body.error, 'not shared by source');
     338});
Note: See TracChangeset for help on using the changeset viewer.