source: Klonkt/test/direct-sends-queue.test.js

main
Last change on this file was 7ccdced, checked in by Robin <roboburr@…>, 26 hours ago

Een Delete van wat nog in de rij wacht: intern oplossen

Robins vraag (26-10): "Shouldn't a Delete on an undelivered Create resolve
internally for all unfederated ones?" Ja. bfa3ba3 zorgde er alleen voor dat de
Delete NA de wachtende Create aankwam -- maar de Create kwam dan nog steeds,
en wat gewist was bereikte zo alsnog een server die het nooit had gehad.

Nu haalt een Delete per inbox de wachtende Create en Update van datzelfde
object uit de rij (cancelUndelivered). Bewust alleen wat de inhoud draagt: een
Announce of Add van een gewist object is onschuldig, de ontvanger krijgt een
404 als hij het ophaalt.

Of de Delete zelf dan nog moet, hangt af van of er iets aangekomen KAN zijn.
Daarvoor de nieuwe kolom ap_delivery.tried: een rij die in de rij ging zonder
poging (de inbox wachtte al, of de server zat in zijn backoff) is nooit de deur
uit geweest; dan gaat er voor die inbox helemaal niets ('cancelled'). Is hij
wel geprobeerd, dan kan een time-out na verwerking hem hebben laten aankomen,
en gaat de Delete toch -- onschuldig als ze hem niet kenden. Rijen van voor de
kolom tellen als geprobeerd: dat is de veilige kant.

Daarbij twee directe verzenders die ik in bfa3ba3 had gemist, nu ook langs
deliverNowOrQueue: het bewerken van een antwoord (deliverOutboxUpdate -- een
Update vóór de wachtende Create wordt genegeerd, waarna de Create de oude tekst
brengt) en guardian-activiteiten (deliverToActor).

test/direct-sends-queue.test.js, nu negen toetsen: het gewiste antwoord dat
wachtte komt nooit aan (en de Delete gaat wel, want het was geprobeerd), een
nooit geprobeerd en gewist antwoord laat niets vertrekken, alleen Create en
Update van dat object en die inbox, en de bewerking en de guardian-activiteit
wachten. Acht mutaties apart gedraaid, elk gevangen; één (ook andere soorten
weghalen) eerst niet, waarop de bedoelde grens in de toets is vastgelegd.
Volledige suite 1380 groen.

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

  • Property mode set to 100644
File size: 8.7 KB
RevLine 
[bfa3ba3]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.
10import { test } from 'node:test';
11import assert from 'node:assert/strict';
12import http from 'http';
13
14process.env.DATABASE_PATH = ':memory:';
15process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
16
17const ontvangen = []; // { pad, type }
18let status = 202;
19const 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});
32await new Promise((r) => ander.listen(0, '127.0.0.1', r));
33const host = `127.0.0.1:${ander.address().port}`;
34const inbox = `http://${host}/inbox`;
35process.env.AP_ALLOW_HOSTS = host;
36test.after(() => ander.close());
37
38const dbMod = await import('../src/config/database.js');
39const db = dbMod.default;
40{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
41const AP = await import('../src/services/ActivityPubService.js');
42
43db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'robin', 'r@test', 'x', 'god');
44db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
45const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('me');
46db.prepare('INSERT INTO ap_followers (slug, actor_uri, inbox) VALUES (?,?,?)').run('me', `http://${host}/users/volger`, inbox);
47const 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
53const rij = () => db.prepare('SELECT * FROM ap_delivery ORDER BY id').all();
54const typen = () => rij().map((r) => JSON.parse(r.body).type);
55const leeg = () => { db.prepare('DELETE FROM ap_delivery').run(); ontvangen.length = 0; status = 202; };
56const 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
[7ccdced]58test('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.
[bfa3ba3]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();
[7ccdced]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
79test('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
93test('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
109test('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
119test('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);
[bfa3ba3]126}));
127
128test('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
137test('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
145test('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
156test('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}));
Note: See TracBrowser for help on using the repository browser.