| 1 | // Een ANTWOORD dat als vermelding/bericht binnenkomt hoort zijn ouder te
|
|---|
| 2 | // houden (Robins melding, 26-8). Dat deed het niet: ap_mentions had geen
|
|---|
| 3 | // kolom voor inReplyTo, dus de ouder viel bij het opslaan op de grond en de
|
|---|
| 4 | // C2S-lezing kon hem niet serveren. Een client kan een gesprek alleen
|
|---|
| 5 | // teruglopen langs inReplyTo, dus elk antwoord kwam aan als het BEGIN van een
|
|---|
| 6 | // gesprek.
|
|---|
| 7 | //
|
|---|
| 8 | // Getoetst op de keten, niet op een kolom: binnen via handleInbox, eruit via
|
|---|
| 9 | // dezelfde leesweg die de app gebruikt.
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 |
|
|---|
| 13 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 14 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 15 |
|
|---|
| 16 | const dbMod = await import('../src/config/database.js');
|
|---|
| 17 | const db = dbMod.default;
|
|---|
| 18 | { const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
|
|---|
| 19 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 20 |
|
|---|
| 21 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 22 | .run('u1', 'robin', 'u1@t', 'x', 'god');
|
|---|
| 23 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
|
|---|
| 24 |
|
|---|
| 25 | const mij = 'https://test.example/ap/users/kid';
|
|---|
| 26 |
|
|---|
| 27 | /** Een binnenkomende directe note, zoals de inbox hem van de andere kant krijgt. */
|
|---|
| 28 | async function binnen(note) {
|
|---|
| 29 | const act = {
|
|---|
| 30 | '@context': 'https://www.w3.org/ns/activitystreams',
|
|---|
| 31 | id: `${note.id}#create`, type: 'Create', actor: note.attributedTo, object: note,
|
|---|
| 32 | };
|
|---|
| 33 | // preVerified: de handtekening zelf is hier niet het onderwerp, en zonder
|
|---|
| 34 | // netwerk valt er niets op te halen.
|
|---|
| 35 | await AP.handleInbox(
|
|---|
| 36 | { body: act, headers: {}, method: 'POST', originalUrl: '/ap/users/kid/inbox', rawBody: Buffer.from(JSON.stringify(act)) },
|
|---|
| 37 | 'kid',
|
|---|
| 38 | { id: note.attributedTo },
|
|---|
| 39 | );
|
|---|
| 40 | }
|
|---|
| 41 |
|
|---|
| 42 | const note = (id, extra = {}) => ({
|
|---|
| 43 | id, type: 'Note', attributedTo: 'https://pruts.nl/ap/users/bart',
|
|---|
| 44 | content: `<p>${id}</p>`, to: [mij],
|
|---|
| 45 | tag: [{ type: 'Mention', href: mij, name: '@kid@test.example' }],
|
|---|
| 46 | published: '2026-08-26T10:00:00Z',
|
|---|
| 47 | ...extra,
|
|---|
| 48 | });
|
|---|
| 49 |
|
|---|
| 50 | test('een binnengekomen antwoord houdt zijn ouder, van opslag tot lezing', async () => {
|
|---|
| 51 | await binnen(note('https://pruts.nl/notes/1'));
|
|---|
| 52 | await binnen(note('https://pruts.nl/notes/2', { inReplyTo: 'https://pruts.nl/notes/1' }));
|
|---|
| 53 |
|
|---|
| 54 | const berichten = AP.getDirectMessages('kid', 20);
|
|---|
| 55 | const kop = berichten.find((m) => m.object_uri === 'https://pruts.nl/notes/2');
|
|---|
| 56 | const eerste = berichten.find((m) => m.object_uri === 'https://pruts.nl/notes/1');
|
|---|
| 57 | assert.ok(kop && eerste, 'allebei de berichten kwamen binnen');
|
|---|
| 58 | assert.equal(kop.in_reply_to, 'https://pruts.nl/notes/1', 'het antwoord draagt zijn ouder');
|
|---|
| 59 | assert.equal(eerste.in_reply_to, null, 'en wie niets beantwoordt heeft er geen');
|
|---|
| 60 |
|
|---|
| 61 | // Dezelfde rij zoals een gesprek hem opvraagt: een keten teruglopen gebeurt
|
|---|
| 62 | // langs deze weg, niet langs de inbox-lijst.
|
|---|
| 63 | const [uitGesprek] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/2']);
|
|---|
| 64 | assert.equal(uitGesprek.in_reply_to, 'https://pruts.nl/notes/1', 'ook via de gesprekslezing');
|
|---|
| 65 | });
|
|---|
| 66 |
|
|---|
| 67 | test('de ouder mag alleen een http(s)-adres zijn, in beide AS2-vormen', async () => {
|
|---|
| 68 | // AS2 staat een string of een object toe. Alleen de string erkennen laat
|
|---|
| 69 | // hetzelfde gat open voor iedereen die de objectvorm stuurt.
|
|---|
| 70 | await binnen(note('https://pruts.nl/notes/3', { inReplyTo: { id: 'https://pruts.nl/notes/1', type: 'Note' } }));
|
|---|
| 71 | const [obj] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/3']);
|
|---|
| 72 | assert.equal(obj.in_reply_to, 'https://pruts.nl/notes/1', 'de objectvorm telt net zo goed');
|
|---|
| 73 |
|
|---|
| 74 | // En een vreemde mag langs dit veld geen ander schema binnensmokkelen.
|
|---|
| 75 | await binnen(note('https://pruts.nl/notes/4', { inReplyTo: 'javascript:alert(1)' }));
|
|---|
| 76 | const [vies] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/4']);
|
|---|
| 77 | assert.equal(vies.in_reply_to, null, 'geen javascript:-adres in de kolom');
|
|---|
| 78 | });
|
|---|