| 1 | // Ook de directe verzenders gaan langs de volgorde per inbox (shaer-xzab.3).
|
|---|
| 2 | //
|
|---|
| 3 | // Drie plekken verstuurden zelf met deliver() en zetten pas bij een mislukking
|
|---|
| 4 | // in de rij: een antwoord (deliverReply), het wissen daarvan
|
|---|
| 5 | // (deliverOutboxDelete) en een direct bericht (deliverDirectNote, ook de
|
|---|
| 6 | // hulpvraag). Zo kon een Delete aankomen vóór de Create die nog in de rij
|
|---|
| 7 | // wachtte -- en stond het antwoord daarna weer op die server.
|
|---|
| 8 | //
|
|---|
| 9 | // Tegen echte HTTP-servers op 127.0.0.1, toegelaten via AP_ALLOW_HOSTS.
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 | import http from 'http';
|
|---|
| 13 |
|
|---|
| 14 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 15 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 16 |
|
|---|
| 17 | const ontvangen = []; // { pad, type }
|
|---|
| 18 | let status = 202;
|
|---|
| 19 | const ander = http.createServer((req, res) => {
|
|---|
| 20 | let body = '';
|
|---|
| 21 | req.on('data', (c) => { body += c; });
|
|---|
| 22 | req.on('end', () => {
|
|---|
| 23 | const host = `http://${req.headers.host}`;
|
|---|
| 24 | if (req.method === 'GET' && req.url === '/users/oma') {
|
|---|
| 25 | res.setHeader('Content-Type', 'application/activity+json');
|
|---|
| 26 | return res.end(JSON.stringify({ id: `${host}/users/oma`, type: 'Person', preferredUsername: 'oma', inbox: `${host}/users/oma/inbox` }));
|
|---|
| 27 | }
|
|---|
| 28 | try { const a = JSON.parse(body); ontvangen.push({ pad: req.url, type: a.type }); } catch { /* geen json */ }
|
|---|
| 29 | res.statusCode = status; res.end();
|
|---|
| 30 | });
|
|---|
| 31 | });
|
|---|
| 32 | await new Promise((r) => ander.listen(0, '127.0.0.1', r));
|
|---|
| 33 | const host = `127.0.0.1:${ander.address().port}`;
|
|---|
| 34 | const inbox = `http://${host}/inbox`;
|
|---|
| 35 | process.env.AP_ALLOW_HOSTS = host;
|
|---|
| 36 | test.after(() => ander.close());
|
|---|
| 37 |
|
|---|
| 38 | const dbMod = await import('../src/config/database.js');
|
|---|
| 39 | const db = dbMod.default;
|
|---|
| 40 | { const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
|
|---|
| 41 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 42 |
|
|---|
| 43 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'robin', 'r@test', 'x', 'god');
|
|---|
| 44 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 45 | const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('me');
|
|---|
| 46 | db.prepare('INSERT INTO ap_followers (slug, actor_uri, inbox) VALUES (?,?,?)').run('me', `http://${host}/users/volger`, inbox);
|
|---|
| 47 | const parent = {
|
|---|
| 48 | id: 1, post_id: '', actor_uri: 'https://unresolvable.invalid/u/ness',
|
|---|
| 49 | actor_url: 'https://unresolvable.invalid/@ness', actor_handle: '@ness@unresolvable.invalid',
|
|---|
| 50 | object_uri: 'https://unresolvable.invalid/notes/9',
|
|---|
| 51 | };
|
|---|
| 52 |
|
|---|
| 53 | const rij = () => db.prepare('SELECT * FROM ap_delivery ORDER BY id').all();
|
|---|
| 54 | const typen = () => rij().map((r) => JSON.parse(r.body).type);
|
|---|
| 55 | const leeg = () => { db.prepare('DELETE FROM ap_delivery').run(); ontvangen.length = 0; status = 202; };
|
|---|
| 56 | const stil = (f) => async () => { const l = console.log, w = console.warn; console.log = () => {}; console.warn = () => {}; try { await f(); } finally { console.log = l; console.warn = w; } };
|
|---|
| 57 |
|
|---|
| 58 | test('een gewist antwoord dat nog wachtte, komt nooit meer aan', stil(async () => {
|
|---|
| 59 | // Robins vraag (26-10): een Delete van een Create die nog in de rij staat,
|
|---|
| 60 | // hoort intern opgelost te worden. Eerst zorgde de volgorde per inbox er
|
|---|
| 61 | // alleen voor dat de Delete NA de Create kwam -- maar de Create kwam dan nog.
|
|---|
| 62 | leeg();
|
|---|
| 63 | status = 503; // de server van de volger hapert
|
|---|
| 64 | const r = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'eerst' });
|
|---|
| 65 | assert.equal(r.delivered, 0);
|
|---|
| 66 | assert.deepEqual(typen(), ['Create'], 'het antwoord wacht in de rij');
|
|---|
| 67 |
|
|---|
| 68 | status = 202; // hij is er weer, de rij is nog niet geweest
|
|---|
| 69 | ontvangen.length = 0;
|
|---|
| 70 | await AP.deliverOutboxDelete(site, r.id);
|
|---|
| 71 | await AP.processDeliveryQueue();
|
|---|
| 72 | assert.ok(!ontvangen.some((o) => o.type === 'Create'), 'het gewiste antwoord gaat niet meer de deur uit');
|
|---|
| 73 | // Hij is wel één keer geprobeerd (de 503), en dan KAN hij aangekomen zijn:
|
|---|
| 74 | // dan gaat de Delete toch, voor de zekerheid.
|
|---|
| 75 | assert.deepEqual(ontvangen.map((o) => o.type), ['Delete']);
|
|---|
| 76 | assert.deepEqual(rij(), []);
|
|---|
| 77 | }));
|
|---|
| 78 |
|
|---|
| 79 | test('nooit geprobeerd en gewist: er gaat helemaal niets heen', stil(async () => {
|
|---|
| 80 | leeg();
|
|---|
| 81 | // De server zit in zijn backoff, dus het antwoord gaat meteen de rij in,
|
|---|
| 82 | // zonder poging; daarna wordt het gewist.
|
|---|
| 83 | AP.enqueueDelivery('me', `http://${host}/ander-pad`, { type: 'Create', id: 'https://klonkt.test/iets' });
|
|---|
| 84 | db.prepare("UPDATE ap_delivery SET next_at = datetime('now', '+30 minutes')").run();
|
|---|
| 85 | const r = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nooit verstuurd' });
|
|---|
| 86 | assert.equal(rij().filter((x) => x.inbox === inbox).length, 1, 'het antwoord wacht, ongeprobeerd');
|
|---|
| 87 | await AP.deliverOutboxDelete(site, r.id);
|
|---|
| 88 | assert.deepEqual(rij().filter((x) => x.inbox === inbox), [], 'Create weg, en geen Delete erachteraan');
|
|---|
| 89 | assert.deepEqual(ontvangen, []);
|
|---|
| 90 | assert.equal(rij().length, 1, 'wat er voor een andere inbox wachtte, blijft');
|
|---|
| 91 | }));
|
|---|
| 92 |
|
|---|
| 93 | test('alleen de Create en Update van DAT object, en alleen voor die inbox', () => {
|
|---|
| 94 | leeg();
|
|---|
| 95 | const obj = 'https://klonkt.test/ap/notes/x';
|
|---|
| 96 | AP.enqueueDelivery('me', inbox, { type: 'Create', id: `${obj}#create`, object: { id: obj } }, { tried: false });
|
|---|
| 97 | AP.enqueueDelivery('me', inbox, { type: 'Update', id: `${obj}#update`, object: { id: obj } }, { tried: false });
|
|---|
| 98 | AP.enqueueDelivery('me', inbox, { type: 'Create', id: 'y#create', object: { id: 'https://klonkt.test/ap/notes/y' } }, { tried: false });
|
|---|
| 99 | AP.enqueueDelivery('me', `http://${host}/elders`, { type: 'Create', id: `${obj}#create`, object: { id: obj } }, { tried: false });
|
|---|
| 100 | // Bewust ALLEEN wat de inhoud draagt. Een Announce of Add van een gewist
|
|---|
| 101 | // object is onschuldig: de ontvanger haalt het op en krijgt een 404.
|
|---|
| 102 | AP.enqueueDelivery('me', inbox, { type: 'Announce', id: `${obj}#boost`, object: obj }, { tried: false });
|
|---|
| 103 | const r = AP.cancelUndelivered(inbox, obj);
|
|---|
| 104 | assert.deepEqual(r, { cancelled: 2, mayHaveArrived: false });
|
|---|
| 105 | assert.deepEqual(rij().map((x) => [x.inbox.endsWith('/inbox') ? 'deze' : 'andere', JSON.parse(x.body).type, String(JSON.parse(x.body).object.id || JSON.parse(x.body).object).slice(-1)]),
|
|---|
| 106 | [['deze', 'Create', 'y'], ['andere', 'Create', 'x'], ['deze', 'Announce', 'x']]);
|
|---|
| 107 | });
|
|---|
| 108 |
|
|---|
| 109 | test('een bewerking gaat niet voorbij het antwoord dat nog wacht', stil(async () => {
|
|---|
| 110 | leeg();
|
|---|
| 111 | status = 503;
|
|---|
| 112 | const r = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'origineel' });
|
|---|
| 113 | status = 202; ontvangen.length = 0;
|
|---|
| 114 | await AP.deliverOutboxUpdate(site, r.id, 'bewerkt');
|
|---|
| 115 | assert.deepEqual(ontvangen, [], 'de Update gaat niet voor: dan negeert de ander hem, en brengt de Create daarna de oude tekst');
|
|---|
| 116 | assert.deepEqual(typen(), ['Create', 'Update']);
|
|---|
| 117 | }));
|
|---|
| 118 |
|
|---|
| 119 | test('een guardian-activiteit gaat niet voorbij wat er voor die inbox wacht', stil(async () => {
|
|---|
| 120 | leeg();
|
|---|
| 121 | const omaInbox = `http://${host}/users/oma/inbox`;
|
|---|
| 122 | AP.enqueueDelivery('me', omaInbox, { type: 'Create', id: 'https://klonkt.test/eerder' });
|
|---|
| 123 | await AP.deliverToActor(site, `http://${host}/users/oma`, { type: 'Offer', id: 'https://klonkt.test/offer/1' });
|
|---|
| 124 | assert.deepEqual(ontvangen, []);
|
|---|
| 125 | assert.equal(rij().filter((x) => x.inbox === omaInbox).length, 2);
|
|---|
| 126 | }));
|
|---|
| 127 |
|
|---|
| 128 | test('een antwoord gaat niet voorbij wat er voor die inbox al wacht', stil(async () => {
|
|---|
| 129 | leeg();
|
|---|
| 130 | AP.enqueueDelivery('me', inbox, { type: 'Create', id: 'https://klonkt.test/eerder' });
|
|---|
| 131 | const r = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'later' });
|
|---|
| 132 | assert.equal(r.delivered, 0, 'niet als verstuurd geteld');
|
|---|
| 133 | assert.deepEqual(ontvangen, []);
|
|---|
| 134 | assert.equal(rij().length, 2);
|
|---|
| 135 | }));
|
|---|
| 136 |
|
|---|
| 137 | test('zonder wachtrij gaat een antwoord gewoon meteen, en telt', stil(async () => {
|
|---|
| 138 | leeg();
|
|---|
| 139 | const r = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'meteen' });
|
|---|
| 140 | assert.equal(r.delivered, 1);
|
|---|
| 141 | assert.deepEqual(ontvangen.map((o) => o.type), ['Create']);
|
|---|
| 142 | assert.deepEqual(rij(), []);
|
|---|
| 143 | }));
|
|---|
| 144 |
|
|---|
| 145 | test('een direct bericht gaat niet voorbij wat er voor die inbox al wacht', stil(async () => {
|
|---|
| 146 | leeg();
|
|---|
| 147 | const omaInbox = `http://${host}/users/oma/inbox`;
|
|---|
| 148 | AP.enqueueDelivery('me', omaInbox, { type: 'Create', id: 'https://klonkt.test/eerder' });
|
|---|
| 149 | const r = await AP.deliverDirectNote(site, { recipients: [`http://${host}/users/oma`], text: 'hoi oma' });
|
|---|
| 150 | assert.ok(r, 'het bericht is gemaakt');
|
|---|
| 151 | assert.equal(r.delivered, 0);
|
|---|
| 152 | assert.deepEqual(ontvangen, [], 'niets voorbij de wachtende');
|
|---|
| 153 | assert.deepEqual(rij().map((x) => x.inbox), [omaInbox, omaInbox]);
|
|---|
| 154 | }));
|
|---|
| 155 |
|
|---|
| 156 | test('zonder wachtrij gaat een direct bericht meteen, en telt', stil(async () => {
|
|---|
| 157 | leeg();
|
|---|
| 158 | const r = await AP.deliverDirectNote(site, { recipients: [`http://${host}/users/oma`], text: 'hoi oma, nu' });
|
|---|
| 159 | assert.equal(r.delivered, 1);
|
|---|
| 160 | assert.deepEqual(ontvangen.map((o) => o.type), ['Create']);
|
|---|
| 161 | }));
|
|---|