- Timestamp:
- 08/07/2026 05:15:52 PM (5 weeks ago)
- Branches:
- main
- Children:
- ba76bf5
- Parents:
- f85b2c3 (diff), 0d5bd2c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the(diff)links above to see all the changes relative to each parent. - Location:
- test
- Files:
-
- 14 added
- 3 edited
-
archive-export.test.js (added)
-
archive-import.test.js (added)
-
archive-recovery.test.js (added)
-
authorized-fetch.test.js (modified) (1 diff)
-
co-location.test.js (modified) (1 diff)
-
feed-wait.test.js (added)
-
forwarded-activities.test.js (added)
-
guardian-follow-queues.test.js (added)
-
guardian-gate-panel.test.js (added)
-
guardian-gate-push.test.js (added)
-
guardian-help-state.test.js (added)
-
interaction-reactions.test.js (added)
-
messages-conversations.test.js (added)
-
messages.test.js (modified) (3 diffs)
-
reactions-characterization.test.js (added)
-
seen-notes.test.js (added)
-
signature-verification.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
test/authorized-fetch.test.js
rf85b2c3 r952baf3 1 // FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch. 1 // Authorized fetch (shaer-afq): een instance in Mastodons secure mode geeft zijn 2 // actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND 3 // verzoek. Zonder die handtekening kregen we 401, vonden we geen sleutel, en 4 // wezen we elke correct ondertekende Follow van die instance af. Op boiert.eu 5 // bleven daardoor vier accounts eindeloos hangen. 6 // 7 // De fetch is hier gestubd: het gaat om de vraag OF er ondertekend wordt en of 8 // er wordt teruggevallen, niet om echte HTTP of crypto. 9 // 10 // Run: npm test 11 2 12 import { test } from 'node:test'; 3 13 import assert from 'node:assert/strict'; 4 14 5 15 process.env.DATABASE_PATH = ':memory:'; 6 process.env.PUBLIC_BASE_URL = 'https:// test.example';16 process.env.PUBLIC_BASE_URL = 'https://klonkt.test'; 7 17 8 18 const dbMod = await import('../src/config/database.js'); 9 19 const db = dbMod.default; 10 20 dbMod.initializeDatabase(); 11 const AP = (await import('../src/services/ActivityPubService.js')).default;21 const AP = await import('../src/services/ActivityPubService.js'); 12 22 13 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god'); 14 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1'); 15 // Commit a guardian relation: kid (ward) is guarded by mom. 16 const MOM = 'https://mom.example/ap/users/mom'; 17 db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM); 23 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)') 24 .run('u1', 'u1', 'u1@test', 'x', 'god'); 25 db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1'); 18 26 19 test('a committed guardian is recognised; a stranger is not', () => { 20 assert.equal(AP.isWardGuardian('kid', MOM), true); 21 assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false); 22 assert.equal(AP.isWardGuardian('nosuch', MOM), false); 27 // IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline. 28 const OPEN_ACTOR = 'https://203.0.113.30/users/open'; // gewone instance 29 const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch 30 31 let verzoeken = []; 32 const echteFetch = globalThis.fetch; 33 globalThis.fetch = async (url, opts = {}) => { 34 const u = String(url); 35 const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature)); 36 verzoeken.push({ url: u, ondertekend }); 37 const doc = (id) => new Response(JSON.stringify({ 38 id, type: 'Person', preferredUsername: 'x', inbox: `${id}/inbox`, 39 publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: '-----BEGIN PUBLIC KEY-----\nx\n-----END PUBLIC KEY-----' }, 40 }), { status: 200, headers: { 'content-type': 'application/activity+json' } }); 41 42 if (u === OPEN_ACTOR) return doc(OPEN_ACTOR); 43 // De kern van secure mode: onbetekend is het 401, ondertekend krijg je hem wel. 44 if (u === SECURE_ACTOR) return ondertekend ? doc(SECURE_ACTOR) : new Response('unauthorized', { status: 401 }); 45 return new Response('not found', { status: 404 }); 46 }; 47 48 test('een actor achter authorized fetch wordt nu wél opgehaald', async () => { 49 verzoeken = []; 50 const actor = await AP.fetchActor(SECURE_ACTOR, { asSlug: 'me' }); 51 assert.ok(actor, 'de actor hoort binnen te komen'); 52 assert.equal(actor.id, SECURE_ACTOR); 53 assert.ok(verzoeken.some((v) => v.url === SECURE_ACTOR && v.ondertekend), 'het verzoek hoort ondertekend te zijn'); 23 54 }); 55 56 test('zonder ondertekenaar blijft dezelfde actor onbereikbaar', async () => { 57 // Dit is precies het oude gedrag, en het bewijst dat de stub echt onderscheid 58 // maakt in plaats van altijd mee te werken. 59 const actor = await AP.fetchActor(SECURE_ACTOR); 60 assert.equal(actor, null); 61 }); 62 63 test('een gewone instance blijft werken, ondertekend of niet', async () => { 64 assert.ok(await AP.fetchActor(OPEN_ACTOR), 'onbetekend'); 65 assert.ok(await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }), 'ondertekend'); 66 }); 67 68 test('een OPEN instance wordt NIET ondertekend opgehaald', () => { 69 // De veiligheidskant van shaer-afq: verifyRequest haalt de keyId-URL op 70 // voordat er iets geverifieerd is, en die URL komt uit een header die iedereen 71 // mag sturen. Tekenden we standaard, dan kan een vreemde ons een ondertekend 72 // verzoek naar een adres van zijn keuze laten sturen, met onze identiteit 73 // eronder. Onbetekend eerst dus, en alleen tekenen als het anders niet lukt. 74 verzoeken = []; 75 return AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }).then((actor) => { 76 assert.ok(actor); 77 assert.equal(verzoeken.length, 1, 'één poging, geen tweede'); 78 assert.equal(verzoeken[0].ondertekend, false, 'en die was onbetekend'); 79 }); 80 }); 81 82 test('een document zonder sleutel telt als mislukt en leidt tot een ondertekende poging', async () => { 83 // Sommige instances serveren onbetekend wel iets, maar zonder publicKey. Voor 84 // een verificatie hebben we daar niets aan. 85 const KAAL = 'https://203.0.113.50/users/kaal'; 86 const stubOrig = globalThis.fetch; 87 globalThis.fetch = async (url, opts = {}) => { 88 const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature)); 89 if (String(url) === KAAL) { 90 verzoeken.push({ url: String(url), ondertekend }); 91 const body = ondertekend 92 ? { id: KAAL, type: 'Person', publicKey: { id: `${KAAL}#k`, owner: KAAL, publicKeyPem: 'x' } } 93 : { id: KAAL, type: 'Person' }; // kaal: geen sleutel 94 return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/activity+json' } }); 95 } 96 return stubOrig(url, opts); 97 }; 98 verzoeken = []; 99 const actor = await AP.fetchActor(KAAL, { asSlug: 'me' }); 100 globalThis.fetch = stubOrig; 101 assert.ok(actor.publicKey && actor.publicKey.publicKeyPem, 'de ondertekende poging levert de sleutel'); 102 assert.deepEqual(verzoeken.map((v) => v.ondertekend), [false, true], 'eerst onbetekend, daarna pas ondertekend'); 103 }); 104 105 test('een onbekende actor blijft null, ondertekend of niet', async () => { 106 assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null); 107 }); 108 109 test.after(() => { globalThis.fetch = echteFetch; }); -
test/co-location.test.js
rf85b2c3 r952baf3 227 227 // of that job being done. 228 228 wardSlugsOf: 'TODO: gated follows still take the shared-database path for a local guardian', 229 // Tweede plek van diezelfde sluiproute, aangekomen met Fase 2 (shaer-jdb): 230 // followsCollection vult de wachtrij voor een ward op DEZE instance uit 231 // ap_pending_follows. Hoort mee te verdwijnen met wardSlugsOf hierboven -- 232 // niet apart, want het is een sluiproute en geen tweede probleem. 233 slugOf: 'TODO: idem, nu ook in de follows-wachtrij (shaer-h6u)', 229 234 }; 230 const files = ['src/services/guardianship/handshake.js', 'src/routes/guardian.js']; 235 // De hele module, niet twee bestanden. Het commentaar hierboven zegt "elke 236 // plek die vraagt of deze actor van ons is", en dat werd tot nu toe afgemeten 237 // aan twee namen -- waardoor een nieuwe sluiproute in een DERDE bestand er 238 // geruisloos doorheen kwam. Dat is precies hoe deze er kwam. 239 const files = [ 240 ...fs.readdirSync('src/services/guardianship').filter((f) => f.endsWith('.js')) 241 .map((f) => `src/services/guardianship/${f}`), 242 'src/routes/guardian.js', 243 ]; 231 244 // Only top-level declarations name a scope; an indented `const base = ...` is 232 245 // a local and would otherwise take the blame for its enclosing function. A -
test/messages.test.js
rf85b2c3 r952baf3 2 2 // in één stroom, groepeert opeenvolgende likes/boosts op dezelfde post, en geeft 3 3 // visibility door (voor de privé-badge). Besluit Robin+Bart 2026-07-16. 4 // 5 // Sinds Berichten gesprekken toont vouwen antwoorden, mentions en je eigen 6 // verzonden berichten samen tot draden (zie messages-conversations.test.js voor 7 // de groepeerlogica zelf); likes, boosts en follows blijven losse regels. 4 8 // 5 9 // Run: npm test (= node --test) … … 42 46 const msgs = AP.getMessages('me', 50); 43 47 44 test('eigen outbound reply zit als "sent" in de stroom (nieuwste eerst)', () => { 45 const sent = msgs.find((m) => m.type === 'sent'); 48 test('eigen outbound reply en het ontvangen antwoord staan in EEN draad', () => { 49 const thread = msgs.find((m) => m.type === 'thread'); 50 assert.ok(thread, 'draad ontbreekt'); 51 assert.equal(msgs[0], thread, 'de draad met het nieuwste bericht hoort bovenaan'); 52 const sent = thread.messages.find((m) => m.type === 'sent'); 46 53 assert.ok(sent, 'sent-item ontbreekt'); 47 54 assert.equal(sent.outboxId, 'out1'); 48 55 assert.equal(sent.to_handle, '@dana@r.test'); 49 assert.equal(msgs[0].type, 'sent', 'nieuwste item hoort bovenaan'); 56 // Hier gaat het om: Dana's antwoord en het jouwe zaten in aparte chips 57 // (Gesprekken en Verzonden) en staan nu op volgorde in dezelfde draad. 58 assert.deepEqual(thread.messages.map((m) => m.type), ['reply', 'sent']); 59 assert.equal(thread.post.slug, 'mijn-post'); 50 60 }); 51 61 … … 58 68 59 69 test('privé-reply draagt visibility voor de badge en heeft post-context', () => { 60 const reply = msgs.find((m) => m.type === 'reply'); 70 const thread = msgs.find((m) => m.type === 'thread'); 71 const reply = thread.messages.find((m) => m.type === 'reply'); 61 72 assert.equal(reply.visibility, 'direct'); 62 73 assert.equal(reply.post_slug, 'mijn-post'); 63 74 assert.equal(reply.post_title, 'Mijn post'); 75 // De context hangt ook aan de draad zelf: die voedt de link in de kop. 76 assert.deepEqual(thread.post, { slug: 'mijn-post', title: 'Mijn post' }); 64 77 }); 65 78
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)