| 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 |
|
|---|
| 7 | import { test } from 'node:test';
|
|---|
| 8 | import 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).
|
|---|
| 12 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 13 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 14 |
|
|---|
| 15 | const dbMod = await import('../src/config/database.js');
|
|---|
| 16 | const db = dbMod.default;
|
|---|
| 17 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 18 |
|
|---|
| 19 | dbMod.initializeDatabase();
|
|---|
| 20 |
|
|---|
| 21 | const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|---|
| 22 |
|
|---|
| 23 | // ── noteVisibility: to/cc → visibility ──────────────────────────────────
|
|---|
| 24 | test('Public in to = public', () => {
|
|---|
| 25 | assert.equal(AP.noteVisibility({ to: [PUBLIC], cc: ['https://a/followers'] }), 'public');
|
|---|
| 26 | });
|
|---|
| 27 | test('Public in cc = unlisted', () => {
|
|---|
| 28 | assert.equal(AP.noteVisibility({ to: ['https://a/followers'], cc: [PUBLIC] }), 'unlisted');
|
|---|
| 29 | });
|
|---|
| 30 | test('as:Public / Public shorthands tellen ook', () => {
|
|---|
| 31 | assert.equal(AP.noteVisibility({ to: ['as:Public'] }), 'public');
|
|---|
| 32 | assert.equal(AP.noteVisibility({ cc: ['Public'] }), 'unlisted');
|
|---|
| 33 | });
|
|---|
| 34 | test('followers-collectie zonder Public = followers', () => {
|
|---|
| 35 | assert.equal(AP.noteVisibility({ to: ['https://mastodon.social/users/a/followers'] }), 'followers');
|
|---|
| 36 | });
|
|---|
| 37 | test('alleen personen geadresseerd = direct (DM)', () => {
|
|---|
| 38 | assert.equal(AP.noteVisibility({ to: ['https://klonkt.test/ap/users/me'] }), 'direct');
|
|---|
| 39 | });
|
|---|
| 40 | test('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 ─────────────
|
|---|
| 47 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 48 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 49 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
|
|---|
| 50 | .run('s1', 'me', 'Me', 'u1');
|
|---|
| 51 | db.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 |
|
|---|
| 54 | function 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 | }
|
|---|
| 58 | addReply('https://remote.test/n/1', 'public', '<p>publieke reply</p>');
|
|---|
| 59 | addReply('https://remote.test/n/2', 'unlisted', '<p>unlisted reply</p>');
|
|---|
| 60 | addReply('https://remote.test/n/3', 'followers', '<p>followers-only reply</p>');
|
|---|
| 61 | addReply('https://remote.test/n/4', 'direct', '<p>DM reply</p>');
|
|---|
| 62 | // legacy rij zonder visibility (pre-migratie) → telt als public
|
|---|
| 63 | db.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)
|
|---|
| 66 | db.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 |
|
|---|
| 69 | test('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 |
|
|---|
| 80 | test('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 | });
|
|---|