source: Klonkt/test/reply-visibility.test.js

main
Last change on this file was 3778ddb, checked in by Robin <roboburr@…>, 8 weeks ago

Fix: private replies (followers-only/DM) no longer render on the public post page

handleInbox ignored a reply's to/cc, and getInteractions rendered every stored
reply, so a followers-only or direct Note replying to a post was shown publicly.
Incoming interactions now record their AP addressing (new ap_interactions.visibility:
public/unlisted/followers/direct, derived via noteVisibility() from to/cc, also in
crawlThread); the public thread filters to public/unlisted (legacy rows count as
public). Likes/boosts stay count-only. Private replies are NOT shown on the post
page at all (no admin badge, per design decision): they reach the owner via
notifications, which already carry post context (slug+title) and the reference.
Covered by test/reply-visibility.test.js; verified in a browser that a seeded DM
reply no longer renders while a public one does. Beads: klonkt-demo-jct.

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

  • Property mode set to 100644
File size: 4.6 KB
Line 
1// Privacy: private replies (followers-only / direct) mogen NIET in de publieke
2// thread op de post-pagina verschijnen; ze horen bij notifications/Messages.
3// Dekt noteVisibility() (to/cc parsing) + het getInteractions-filter af.
4//
5// Run: npm test (= node --test)
6
7import { test } from 'node:test';
8import assert from 'node:assert/strict';
9
10// Isoleer van de echte DB: ':memory:' MOET gezet zijn vóór de eerste import van
11// config/database.js (die maakt de singleton-connectie op basis van deze env).
12process.env.DATABASE_PATH = ':memory:';
13process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
14
15const dbMod = await import('../src/config/database.js');
16const db = dbMod.default;
17const AP = await import('../src/services/ActivityPubService.js');
18
19dbMod.initializeDatabase();
20
21const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
22
23// ── noteVisibility: to/cc → visibility ──────────────────────────────────
24test('Public in to = public', () => {
25 assert.equal(AP.noteVisibility({ to: [PUBLIC], cc: ['https://a/followers'] }), 'public');
26});
27test('Public in cc = unlisted', () => {
28 assert.equal(AP.noteVisibility({ to: ['https://a/followers'], cc: [PUBLIC] }), 'unlisted');
29});
30test('as:Public / Public shorthands tellen ook', () => {
31 assert.equal(AP.noteVisibility({ to: ['as:Public'] }), 'public');
32 assert.equal(AP.noteVisibility({ cc: ['Public'] }), 'unlisted');
33});
34test('followers-collectie zonder Public = followers', () => {
35 assert.equal(AP.noteVisibility({ to: ['https://mastodon.social/users/a/followers'] }), 'followers');
36});
37test('alleen personen geadresseerd = direct (DM)', () => {
38 assert.equal(AP.noteVisibility({ to: ['https://klonkt.test/ap/users/me'] }), 'direct');
39});
40test('string ipv array en ontbrekende velden crashen niet', () => {
41 assert.equal(AP.noteVisibility({ to: PUBLIC }), 'public');
42 assert.equal(AP.noteVisibility({}), 'direct');
43 assert.equal(AP.noteVisibility(null), 'direct');
44});
45
46// ── getInteractions: private replies uit de publieke thread ─────────────
47db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
48 .run('u1', 'u1', 'u1@test', 'x', 'god');
49db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
50 .run('s1', 'me', 'Me', 'u1');
51db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, status, type, created_at, updated_at, published_at)
52 VALUES ('p1','s1','post','u1','Post','<p>x</p>','published','post',datetime('now'),datetime('now'),datetime('now'))`).run();
53
54function addReply(objectUri, visibility, content) {
55 db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, content, visibility)
56 VALUES ('reply','p1',?,?,?,?,?)`).run(objectUri, 'https://remote.test/users/r', 'R', content, visibility);
57}
58addReply('https://remote.test/n/1', 'public', '<p>publieke reply</p>');
59addReply('https://remote.test/n/2', 'unlisted', '<p>unlisted reply</p>');
60addReply('https://remote.test/n/3', 'followers', '<p>followers-only reply</p>');
61addReply('https://remote.test/n/4', 'direct', '<p>DM reply</p>');
62// legacy rij zonder visibility (pre-migratie) → telt als public
63db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, content, visibility)
64 VALUES ('reply','p1','https://remote.test/n/5','https://remote.test/users/r','R','<p>legacy</p>',NULL)`).run();
65// een followers-only like blijft gewoon meetellen (count-only, geen content)
66db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, visibility)
67 VALUES ('like','p1','','https://remote.test/users/r','followers')`).run();
68
69test('publieke thread bevat public/unlisted/legacy, maar geen followers/direct', () => {
70 const view = AP.getInteractions('p1', 'https://klonkt.test', { slug: 'me', title: 'Me' });
71 const html = JSON.stringify(view);
72 assert.ok(html.includes('publieke reply'));
73 assert.ok(html.includes('unlisted reply'));
74 assert.ok(html.includes('legacy'));
75 assert.ok(!html.includes('followers-only reply'), 'followers-only reply lekte naar de publieke thread');
76 assert.ok(!html.includes('DM reply'), 'DM lekte naar de publieke thread');
77 assert.equal(view.likeCount, 1, 'like hoort te blijven meetellen');
78});
79
80test('private reply blijft zichtbaar voor de eigenaar in notifications (met post-context)', () => {
81 const notes = AP.getNotifications('me', 50);
82 const dm = notes.find((n) => n.content && n.content.includes('DM reply'));
83 assert.ok(dm, 'DM-reply ontbreekt in notifications');
84 assert.equal(dm.post_slug, 'post');
85 assert.equal(dm.post_title, 'Post');
86});
Note: See TracBrowser for help on using the repository browser.