| 1 | // shaer-e9g: onthoud de URI van antwoorden die we al ontvangen.
|
|---|
| 2 | //
|
|---|
| 3 | // Antwoorden van accounts die je volgt komen gewoon binnen, ondertekend door de
|
|---|
| 4 | // schrijver zelf. Ze horen niet in de Krant (belongsInTimeline weigert alles met
|
|---|
| 5 | // een inReplyTo) en werden daarna nergens bewaard. Kwam er later een
|
|---|
| 6 | // DOORGESTUURD antwoord op zo'n bericht, dan kende Klonkt de ouder niet en wees
|
|---|
| 7 | // het af -- terwijl het die ouder wel degelijk had gehad. Op boiert ging daar 72%
|
|---|
| 8 | // van het doorstuurverkeer op stuk.
|
|---|
| 9 | //
|
|---|
| 10 | // Wat hier bewaakt wordt is vooral de grens: onthouden gebeurt ALLEEN voor
|
|---|
| 11 | // schrijvers die je zelf volgt. Zou dat voor iedereen gelden, dan kan een vreemde
|
|---|
| 12 | // eerst een bericht neerleggen en daarna met een antwoord daarop de dereference
|
|---|
| 13 | // naar een adres van zijn keuze sturen.
|
|---|
| 14 | //
|
|---|
| 15 | // Run: npm test
|
|---|
| 16 |
|
|---|
| 17 | import { test } from 'node:test';
|
|---|
| 18 | import assert from 'node:assert/strict';
|
|---|
| 19 |
|
|---|
| 20 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 21 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 22 |
|
|---|
| 23 | const dbMod = await import('../src/config/database.js');
|
|---|
| 24 | const db = dbMod.default;
|
|---|
| 25 | dbMod.initializeDatabase();
|
|---|
| 26 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 27 |
|
|---|
| 28 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 29 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 30 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 31 |
|
|---|
| 32 | // IP-literals: safeFetch slaat de DNS-lookup over, dus offline.
|
|---|
| 33 | const GEVOLGD = 'https://203.0.113.10/users/catsalad'; // volgen we
|
|---|
| 34 | const VREEMDE = 'https://203.0.113.90/users/onbekend'; // volgen we niet
|
|---|
| 35 | const DOORSTUURDER = 'https://203.0.113.20/users/relay';
|
|---|
| 36 |
|
|---|
| 37 | db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, status) VALUES (?,?,?,?)`)
|
|---|
| 38 | .run('me', GEVOLGD, '@catsalad@203.0.113.10', 'accepted');
|
|---|
| 39 |
|
|---|
| 40 | let bron = null;
|
|---|
| 41 | const echteFetch = globalThis.fetch;
|
|---|
| 42 | globalThis.fetch = async (url) => {
|
|---|
| 43 | const u = String(url);
|
|---|
| 44 | if (bron && bron.id === u) {
|
|---|
| 45 | return new Response(JSON.stringify(bron), { status: 200, headers: { 'content-type': 'application/activity+json' } });
|
|---|
| 46 | }
|
|---|
| 47 | return new Response('not found', { status: 404 });
|
|---|
| 48 | };
|
|---|
| 49 |
|
|---|
| 50 | const req = (body) => ({ body, headers: { signature: 'keyId="x",signature="y"' }, ip: '203.0.113.9' });
|
|---|
| 51 | const gezien = (uri) => !!db.prepare('SELECT 1 FROM ap_seen_notes WHERE uri = ?').get(uri);
|
|---|
| 52 |
|
|---|
| 53 | /** Een gewoon bezorgd antwoord: ondertekend door de schrijver zelf. */
|
|---|
| 54 | function bezorgdAntwoord(auteur, id, inReplyTo) {
|
|---|
| 55 | return {
|
|---|
| 56 | type: 'Create',
|
|---|
| 57 | actor: auteur,
|
|---|
| 58 | object: {
|
|---|
| 59 | id, type: 'Note', attributedTo: auteur, inReplyTo,
|
|---|
| 60 | content: '<p>een antwoord midden in een draad</p>',
|
|---|
| 61 | to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|---|
| 62 | },
|
|---|
| 63 | };
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | test('een antwoord van iemand die we volgen wordt onthouden', async () => {
|
|---|
| 67 | const uri = 'https://203.0.113.10/notes/catsalad-1';
|
|---|
| 68 | const status = await AP.handleInbox(req(bezorgdAntwoord(GEVOLGD, uri, 'https://203.0.113.10/notes/draad-start')), 'me', { id: GEVOLGD });
|
|---|
| 69 | assert.equal(status, 202);
|
|---|
| 70 | assert.ok(gezien(uri), 'de URI hoort bewaard te zijn');
|
|---|
| 71 | });
|
|---|
| 72 |
|
|---|
| 73 | test('maar het verschijnt NIET in de Krant', () => {
|
|---|
| 74 | // Alleen de URI. Er verandert niets aan wat je te zien krijgt -- een antwoord
|
|---|
| 75 | // hoort in zijn draad, niet in de tijdlijn, en dat blijft zo.
|
|---|
| 76 | assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline').get().n, 0);
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | test('een antwoord van een VREEMDE wordt niet onthouden', async () => {
|
|---|
| 80 | const uri = 'https://203.0.113.90/notes/vreemde-1';
|
|---|
| 81 | await AP.handleInbox(req(bezorgdAntwoord(VREEMDE, uri, 'https://203.0.113.90/notes/iets')), 'me', { id: VREEMDE });
|
|---|
| 82 | assert.equal(gezien(uri), false, 'anders kan een vreemde de poort voor zichzelf openzetten');
|
|---|
| 83 | });
|
|---|
| 84 |
|
|---|
| 85 | test('een gewone POST van iemand die we volgen wordt niet als gezien-antwoord bewaard', async () => {
|
|---|
| 86 | // Die gaat naar de Krant en staat daarmee al in ap_timeline; een tweede
|
|---|
| 87 | // administratie zou alleen maar uiteen kunnen lopen.
|
|---|
| 88 | const uri = 'https://203.0.113.10/notes/gewone-post';
|
|---|
| 89 | const act = bezorgdAntwoord(GEVOLGD, uri, null);
|
|---|
| 90 | delete act.object.inReplyTo;
|
|---|
| 91 | await AP.handleInbox(req(act), 'me', { id: GEVOLGD });
|
|---|
| 92 | assert.equal(gezien(uri), false);
|
|---|
| 93 | assert.ok(db.prepare('SELECT 1 FROM ap_timeline WHERE id = ?').get(uri), 'die hoort juist wel in de Krant');
|
|---|
| 94 | });
|
|---|
| 95 |
|
|---|
| 96 | test('en dan komt het doorgestuurde antwoord OP dat bericht wel binnen', async () => {
|
|---|
| 97 | // De hele reden van deze bead. Zonder het onthouden was dit een afwijzing met
|
|---|
| 98 | // "skipped (unknown inReplyTo)".
|
|---|
| 99 | const ouder = 'https://203.0.113.10/notes/catsalad-1'; // hierboven onthouden
|
|---|
| 100 | const antwoord = 'https://203.0.113.30/notes/drinkcoaster-1';
|
|---|
| 101 | const schrijver = 'https://203.0.113.30/users/drinkcoaster';
|
|---|
| 102 | bron = {
|
|---|
| 103 | id: antwoord, type: 'Note', attributedTo: schrijver, inReplyTo: ouder,
|
|---|
| 104 | content: '<p>ECHTE versie</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|---|
| 105 | };
|
|---|
| 106 | const act = { type: 'Create', actor: schrijver, object: { ...bron, content: '<p>BEZORGDE versie</p>' } };
|
|---|
| 107 | const status = await AP.handleInbox(req(act), 'me', { id: DOORSTUURDER });
|
|---|
| 108 | assert.equal(status, 202, 'de doorgestuurde Create hoort geaccepteerd te worden');
|
|---|
| 109 | });
|
|---|
| 110 |
|
|---|
| 111 | test('een doorgestuurd antwoord op een bericht dat we NIET kenden blijft geweigerd', async () => {
|
|---|
| 112 | const antwoord = 'https://203.0.113.30/notes/drinkcoaster-2';
|
|---|
| 113 | const schrijver = 'https://203.0.113.30/users/drinkcoaster';
|
|---|
| 114 | bron = {
|
|---|
| 115 | id: antwoord, type: 'Note', attributedTo: schrijver, inReplyTo: 'https://203.0.113.90/notes/nooit-gezien',
|
|---|
| 116 | content: '<p>x</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|---|
| 117 | };
|
|---|
| 118 | const act = { type: 'Create', actor: schrijver, object: bron };
|
|---|
| 119 | assert.equal(await AP.handleInbox(req(act), 'me', { id: DOORSTUURDER }), 401);
|
|---|
| 120 | });
|
|---|
| 121 |
|
|---|
| 122 | test.after(() => { globalThis.fetch = echteFetch; });
|
|---|