source: Klonkt/test/c2s-reply-mentions.test.js@ 0d6d266

main
Last change on this file since 0d6d266 was 0d6d266, checked in by Robin <roboburr@…>, 13 days ago

C2S-antwoord: alle mentions van de client, niet alleen de ouder-auteur

Robins bugreport, 26-8: er gingen er drie de deur uit, er stond er een
gepubliceerd.

Geen filter, maar een schakelaar die nooit werd omgezet. deliverReply
kent de mentions-balk van het web: een ARRAY betekent "precies deze
mensen", undefined betekent "oud gedrag -- noem de auteur van de ouder".
De C2S-inname gaf dat veld helemaal niet mee, dus elk antwoord uit een
app viel terug op dat oude gedrag. Dezelfde schakelaar stuurt drie
dingen tegelijk aan, en ze gingen dus alle drie mee: de ankers vooraan
de inhoud, de Mention-tags (die worden uit die ankers afgeleid) en de
inboxen waar bezorgd wordt.

De tags zijn de bron en niet to/cc: daar staan ook de volgerscollectie
en Public in, en dat zijn geen mensen. Ontdubbeld op actor, want de
ouder-auteur zit meestal ook in de tags. Zonder tags blijft het bij het
oude gedrag -- een lege lijst zou "niemand noemen" betekenen, en dat is
een keuze die een client die geen tags kent nooit gemaakt heeft.

Drie toetsen, met tegenbewijs: de twee over het nieuwe gedrag vallen
tegen de code van hiervoor, en die over het oude gedrag slaagt in
allebei -- dat laatste is het bewijs dat het bestaande pad niet
verlegd is. Volle suite 1234 groen.

  • Property mode set to 100644
File size: 4.0 KB
Line 
1// Een C2S-antwoord houdt ALLE mentions die de client stuurde (Robins melding,
2// 26-8). Dat deed het niet: de inname gaf deliverReply geen `mentions` mee, en
3// dat betekent daar "oud gedrag -- noem alleen de auteur van de ouder". Wie er
4// drie stuurde zag er dus een gepubliceerd worden.
5//
6// Getoetst op wat er de deur uit gaat: de Mention-tags op de note, want dat is
7// wat de ontvangende kant leest.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
17const APmod = await import('../src/services/ActivityPubService.js');
18const AP = APmod.default;
19
20db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
21 .run('u1', 'robin', 'u1@t', 'x', 'god');
22db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
23const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('kid');
24const user = db.prepare('SELECT * FROM users WHERE id = ?').get('u1');
25
26// De ouder waarop geantwoord wordt: een eigen post, zodat er geen netwerk aan
27// te pas komt. resolveRemoteNote leest die rechtstreeks uit de databank.
28db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, created_at, updated_at, published_at)
29 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`)
30 .run('p1', 's1', 'de-post', 'u1', '', '<p>de ouder</p>', '', 'published', 'post', 'nl',
31 '2026-08-26T09:00:00Z', '2026-08-26T09:00:00Z', '2026-08-26T09:00:00Z');
32
33const OUDER = 'https://test.example/ap/notes/p1';
34const BART = 'https://pruts.nl/ap/users/bart';
35const ESMEE = 'https://boiert.eu/users/esmee';
36
37async function antwoord(tags) {
38 const r = await AP.ingestOutboxActivity(site, user, {
39 type: 'Create',
40 object: {
41 type: 'Note',
42 inReplyTo: OUDER,
43 content: '<p>ja, mee eens</p>',
44 source: { content: 'ja, mee eens', mediaType: 'text/plain' },
45 to: [`https://test.example/ap/users/kid/followers`],
46 cc: ['https://www.w3.org/ns/activitystreams#Public'],
47 ...(tags ? { tag: tags } : {}),
48 },
49 });
50 assert.equal(r.status, 201, `het antwoord hoort te landen, kreeg ${r.status} ${r.error || ''}`);
51 const rij = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
52 return AP.buildNote('https://test.example', site, rij, { isReply: true });
53}
54
55const mentionsVan = (note) => (note.tag || []).filter((t) => t.type === 'Mention').map((t) => t.href).sort();
56
57test('drie meegestuurde mentions worden er drie, niet een', async () => {
58 const note = await antwoord([
59 { type: 'Mention', href: AP.actorId('https://test.example', 'kid'), name: '@kid@test.example' },
60 { type: 'Mention', href: BART, name: '@bart@pruts.nl' },
61 { type: 'Mention', href: ESMEE, name: '@esmee@boiert.eu' },
62 ]);
63 assert.deepEqual(mentionsVan(note), [ESMEE, BART, AP.actorId('https://test.example', 'kid')].sort(),
64 'alle drie de genoemden staan als Mention op de note');
65});
66
67test('een dubbele mention telt een keer, en een raar adres telt niet mee', async () => {
68 const note = await antwoord([
69 { type: 'Mention', href: BART, name: '@bart@pruts.nl' },
70 { type: 'Mention', href: BART, name: '@bart@pruts.nl' }, // dezelfde
71 { type: 'Mention', href: 'javascript:alert(1)', name: '@stout' },
72 { type: 'Hashtag', href: 'https://pruts.nl/tags/muziek', name: '#muziek' },
73 ]);
74 assert.deepEqual(mentionsVan(note), [BART], 'ontdubbeld, en alleen echte Mentions met een http-adres');
75});
76
77test('zonder tags blijft het oude gedrag: de auteur van de ouder', async () => {
78 // Een client die geen tags kent hoort niet stil zonder mentions te eindigen.
79 const note = await antwoord(null);
80 assert.deepEqual(mentionsVan(note), [AP.actorId('https://test.example', 'kid')],
81 'de ouder-auteur wordt genoemd, zoals altijd');
82});
Note: See TracBrowser for help on using the repository browser.