source: Klonkt/test/c2s-direct.test.js@ 407353e

main
Last change on this file since 407353e was 407353e, checked in by Bart <bart@…>, 2 hours ago

Een foto zonder woorden is een bericht (502 direct_failed)

deliverDirectNote begon met !String(text).trim() -> return null, en de inname
maakte van dat null een 502 direct_failed. Sinds de inname een bijlage-only
note doorlaat waren die twee lagen het oneens over wat een bericht is: wie een
foto op een gesprek liet vallen en niets typte kwam precies daar vast te
zitten. Het viel niet op in de tests, want dit was de enige plek waar de regel
stond en hij stond midden in een functie die netwerk doet.

Nu is het een eigen, pure regel -- text, html of bijlage -- met tests eromheen,
zoals de rest van deze laag het doet. Leeg blijft geweigerd: geen tekst, geen
opmaak en geen bijlage is niets.

En String(text) werd String(text || ''): zonder tekst stond er anders
letterlijk "undefined" in de body zodra een aanroeper het veld wegliet.

Co-Authored-By: Claude Opus 5 <claude@…>

  • Property mode set to 100644
File size: 6.9 KB
Line 
1// Direct notes (private mentions, shaer-tqc): never Public, never boostable.
2import { test } from 'node:test';
3import assert from 'node:assert/strict';
4
5process.env.DATABASE_PATH = ':memory:';
6process.env.PUBLIC_BASE_URL = 'https://test.example';
7
8const dbMod = await import('../src/config/database.js');
9const db = dbMod.default;
10dbMod.initializeDatabase();
11const AP = (await import('../src/services/ActivityPubService.js')).default;
12
13const PUB = 'https://www.w3.org/ns/activitystreams#Public';
14db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
15db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'me', 'Me', 'u1', 1);
16const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
17const user = { id: 'u1', username: 'u1' };
18
19test('a direct outbox row addresses only its recipients, no Public, no cc', () => {
20 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, visibility, to_actors, created_at)
21 VALUES ('d1','me','',NULL,NULL,'https://r.test/u/g','@g@r.test','<p>help</p>','direct','["https://r.test/u/g","https://q.test/u/h"]',CURRENT_TIMESTAMP)`).run();
22 const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get('d1');
23 const note = AP.buildReplyNote('https://test.example', site, row);
24 assert.deepEqual(note.to, ['https://r.test/u/g', 'https://q.test/u/h']);
25 assert.deepEqual(note.cc, []);
26 assert.ok(!JSON.stringify(note.to).includes(PUB) && !JSON.stringify(note.cc).includes(PUB));
27});
28
29test('a direct note carries its attachments (help-buoy capture)', () => {
30 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, visibility, to_actors, attachments, created_at)
31 VALUES ('d2','me','',NULL,NULL,'https://r.test/u/g','@g@r.test','<p>kijk</p>','direct','["https://r.test/u/g"]','[{"url":"/media/reply-media/x.png","mediaType":"image/png","name":"capture"}]',CURRENT_TIMESTAMP)`).run();
32 const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get('d2');
33 const note = AP.buildReplyNote('https://test.example', site, row);
34 assert.equal(note.attachment.length, 1);
35 assert.equal(note.attachment[0].type, 'Image');
36 assert.ok(note.attachment[0].url.endsWith('/media/reply-media/x.png'));
37 assert.deepEqual(note.cc, []); // still direct
38});
39
40test('a help request emits shaer:helpRequest (FEP-633c 5.2.1)', () => {
41 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, visibility, to_actors, help_request, created_at)
42 VALUES ('d3','me','',NULL,NULL,'https://r.test/u/g','@g@r.test','<p>help</p>','direct','["https://r.test/u/g"]',1,CURRENT_TIMESTAMP)`).run();
43 const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get('d3');
44 const note = AP.buildReplyNote('https://test.example', site, row);
45 assert.equal(note['shaer:helpRequest'], true);
46 assert.deepEqual(note.cc, []); // still strictly direct
47 // and the namespace is declared, so the term is valid JSON-LD
48 const ctx = AP.AP_CONTEXT.find((p) => p && typeof p === 'object');
49 assert.equal(ctx.shaer, 'https://ns.klonkt.com/shaer#');
50 // a plain direct note (no flag) does NOT carry the term
51 const plainRow = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get('d1');
52 assert.equal(AP.buildReplyNote('https://test.example', site, plainRow)['shaer:helpRequest'], undefined);
53});
54
55test('direct without any real recipient is refused (400 no_recipients)', async () => {
56 const r = await AP.ingestOutboxActivity(site, user, {
57 type: 'Note', content: '<p>x</p>',
58 to: ['https://test.example/ap/users/me/followers'], cc: [], // friends-shaped? no: followers in to = friends
59 });
60 // followers-only reads as friends, so force the direct shape: bare unknown string
61 const r2 = await AP.ingestOutboxActivity(site, user, { type: 'Note', content: '<p>x</p>', to: [], cc: [] });
62 // empty addressing = legacy public; the real no-recipient direct case:
63 const r3 = await AP.ingestOutboxActivity(site, user, { type: 'Note', content: '<p>x</p>', to: ['not-a-uri'], cc: [] });
64 assert.equal(r3.status, 400);
65 assert.equal(r3.error, 'no_recipients');
66 assert.ok(r && r2); // shapes above answered too (not the point of this test)
67});
68
69test('C2S Announce/Like of a non-public local post is refused (403)', async () => {
70 db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, type, fan_only, ap_visibility, created_at, updated_at, published_at)
71 VALUES ('pf','s1','geheim','u1','','<p>prive</p>','published','post',1,'friends',datetime('now'),datetime('now'),datetime('now'))`).run();
72 const noteUrl = 'https://test.example/ap/notes/pf';
73 const boost = await AP.ingestOutboxActivity(site, user, { type: 'Announce', object: noteUrl });
74 assert.equal(boost.status, 403);
75 assert.equal(boost.error, 'not_public');
76 const like = await AP.ingestOutboxActivity(site, user, { type: 'Like', object: noteUrl });
77 assert.equal(like.status, 403);
78});
79
80// Een bijlage KAN het hele bericht zijn (shaer-6lcc).
81//
82// Twee lagen waren het oneens over wat een bericht is: de inname liet een
83// note met alleen een bijlage door, en deliverDirectNote weigerde hem een
84// laag lager op `!text.trim()`. Wie een foto op een gesprek liet vallen en
85// niets typte kreeg daardoor "502 direct_failed".
86test('een direct bericht met alleen een bijlage telt als inhoud', async () => {
87 const { directNoteHasContent } = await import('../src/services/guardianship/delivery.js');
88
89 assert.equal(directNoteHasContent({ text: 'hallo' }), true);
90 assert.equal(directNoteHasContent({ html: '<p>hallo</p>' }), true);
91 assert.equal(directNoteHasContent({
92 text: '',
93 attachments: [{ url: '/media/reply-media/x.png', mediaType: 'image/png' }],
94 }), true, 'een foto zonder woorden is een bericht');
95 assert.equal(directNoteHasContent({
96 text: ' \n ',
97 attachments: [{ url: '/media/reply-media/x.png', mediaType: 'image/png' }],
98 }), true, 'witruimte is geen tekst, maar de bijlage draagt hem');
99
100 // En leeg blijft leeg: dit mag de deur niet uit.
101 assert.equal(directNoteHasContent({}), false);
102 assert.equal(directNoteHasContent({ text: ' ', html: '', attachments: [] }), false);
103 assert.equal(directNoteHasContent(), false);
104});
105
106// De inname en de bezorging horen dezelfde grens te trekken. Loopt er eentje
107// weg, dan is dat precies de 502 hierboven, en die valt buiten de tests om.
108test('inname en bezorging zijn het eens over een leeg bericht', async () => {
109 const { directNoteHasContent } = await import('../src/services/guardianship/delivery.js');
110 const src = (await import('fs')).readFileSync('src/services/ap-c2s.js', 'utf8');
111 assert.ok(src.includes("error: 'empty_note'"), 'de inname heeft nog een lege-bericht-controle');
112 // De inname laat een bijlage-only note door; de bezorging hoort dat ook te doen.
113 assert.equal(directNoteHasContent({ text: '', attachments: [{ url: '/media/x.png' }] }), true);
114});
Note: See TracBrowser for help on using the repository browser.