| 1 | // Elke rij die bij buildNote aankomt draagt de velden waar buildNote OP BESLIST
|
|---|
| 2 | // (shaer-6oth, en Barts melding van 15-8).
|
|---|
| 3 | //
|
|---|
| 4 | // Deze fout heeft een vorm, en die vorm herhaalt zich: een route of dienst
|
|---|
| 5 | // haalt een KOLOMMENLIJST op in plaats van SELECT *, buildNote leest een veld
|
|---|
| 6 | // dat er niet bij zat, en `undefined` levert stilletjes het RUIMSTE gedrag op.
|
|---|
| 7 | // Zonder fan_only/ap_visibility krijgt een besloten post `to: as:Public`; zonder
|
|---|
| 8 | // paid slaat de redactie over en gaat de volledige tekst van een betaalde post
|
|---|
| 9 | // mee. Er is niets dat waarschuwt: de query klopt, de bouwer klopt, en samen
|
|---|
| 10 | // lekken ze.
|
|---|
| 11 | //
|
|---|
| 12 | // Daarom een LUS over de publieke collecties die posts serveren, en geen toets
|
|---|
| 13 | // op een enkele query -- precies de reden die ap-collection-paging.test.js ook
|
|---|
| 14 | // noemt: het gat ontstond doordat de ene plek het wel had en de andere niet, en
|
|---|
| 15 | // een toets per plek vraagt om dezelfde vergeetachtigheid. Over HTTP, want de
|
|---|
| 16 | // query zit in de route en die is het onderwerp.
|
|---|
| 17 | import { test } from 'node:test';
|
|---|
| 18 | import assert from 'node:assert/strict';
|
|---|
| 19 |
|
|---|
| 20 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 21 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 22 |
|
|---|
| 23 | const dbMod = await import('../src/config/database.js');
|
|---|
| 24 | const db = dbMod.default;
|
|---|
| 25 | { const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
|
|---|
| 26 | const express = (await import('express')).default;
|
|---|
| 27 | const routes = (await import('../src/routes/activitypub.js')).default;
|
|---|
| 28 |
|
|---|
| 29 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 30 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 31 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
|
|---|
| 32 | .run('s1', 'band', 'De Band', 'u1');
|
|---|
| 33 |
|
|---|
| 34 | const GEHEIM = 'DIT IS DE BETAALDE TEKST';
|
|---|
| 35 | const insPost = db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, excerpt, status, published_at, fan_only, ap_visibility, paid, paid_min_cents, pinned)
|
|---|
| 36 | VALUES (?,?,?,?,?,?,?,'published',?,?,?,?,?,?)`);
|
|---|
| 37 | // Alle drie VASTGEZET, zodat ze ook in de featured-collectie terechtkomen.
|
|---|
| 38 | insPost.run('p-paid', 's1', 'u1', 'betaald', 'Betaald', `<p>${GEHEIM}</p>`, 'De teaser', '2026-08-01T00:00:00Z', 0, 'public', 1, 500, 3);
|
|---|
| 39 | insPost.run('p-quiet', 's1', 'u1', 'stil', 'Stil', '<p>stil</p>', '', '2026-08-02T00:00:00Z', 0, 'quiet', 0, null, 2);
|
|---|
| 40 | insPost.run('p-fan', 's1', 'u1', 'vrienden', 'Vrienden', '<p>alleen vrienden</p>', '', '2026-08-03T00:00:00Z', 1, 'friends', 0, null, 1);
|
|---|
| 41 |
|
|---|
| 42 | const app = express(); app.use(routes);
|
|---|
| 43 | const server = app.listen(0);
|
|---|
| 44 | await new Promise((r) => server.once('listening', r));
|
|---|
| 45 | const base = `http://127.0.0.1:${server.address().port}`;
|
|---|
| 46 | test.after(() => server.close());
|
|---|
| 47 |
|
|---|
| 48 | // De publieke collecties die POSTS naar buiten dragen. Wie er een toevoegt die
|
|---|
| 49 | // zijn rijen met een kolommenlijst ophaalt, hoort hem hier ook bij te zetten.
|
|---|
| 50 | const COLLECTIES = ['/ap/users/band/outbox', '/ap/users/band/featured'];
|
|---|
| 51 |
|
|---|
| 52 | /** Elke Note uit een collectie, hoe hij ook verpakt zit (Create-wrapper of kaal). */
|
|---|
| 53 | async function notes(pad) {
|
|---|
| 54 | const r = await fetch(base + pad, { headers: { Accept: 'application/activity+json' } });
|
|---|
| 55 | assert.equal(r.status, 200, `${pad} hoort 200 te geven`);
|
|---|
| 56 | const body = await r.json();
|
|---|
| 57 | const items = body.orderedItems || body.items || [];
|
|---|
| 58 | return items.map((i) => (i && typeof i.object === 'object' ? i.object : i)).filter((n) => n && n.id);
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | const NOTE = (id) => `https://test.example/ap/notes/${id}`;
|
|---|
| 62 |
|
|---|
| 63 | for (const pad of COLLECTIES) {
|
|---|
| 64 | test(`${pad}: een betaalde post gaat als teaser de deur uit, nooit als tekst`, async () => {
|
|---|
| 65 | const alles = await notes(pad);
|
|---|
| 66 | const n = alles.find((x) => x.id === NOTE('p-paid'));
|
|---|
| 67 | assert.ok(n, 'de betaalde post hoort hier te staan -- hij is publiek, alleen zijn tekst niet');
|
|---|
| 68 | assert.ok(!String(n.content).includes(GEHEIM),
|
|---|
| 69 | 'zonder de kolom `paid` slaat buildNote zijn redactie over en staat de volledige tekst erin');
|
|---|
| 70 | assert.match(String(n.content), /supporters/i, 'in plaats daarvan de teaser met de leeslink');
|
|---|
| 71 | assert.match(String(n.content), /De teaser/, 'en de teaser komt uit excerpt, niet uit de verborgen tekst');
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | test(`${pad}: een stille post wordt niet luid geadresseerd`, async () => {
|
|---|
| 75 | const n = (await notes(pad)).find((x) => x.id === NOTE('p-quiet'));
|
|---|
| 76 | assert.ok(n, 'een quiet-post hoort in een publieke collectie te staan');
|
|---|
| 77 | const to = [].concat(n.to || []);
|
|---|
| 78 | assert.ok(!to.includes('https://www.w3.org/ns/activitystreams#Public'),
|
|---|
| 79 | 'zonder ap_visibility zet buildNote as:Public in `to` en is stil ineens luid');
|
|---|
| 80 | assert.ok(to.some((a) => /\/followers$/.test(a)), 'stil = aan je volgers gericht');
|
|---|
| 81 | });
|
|---|
| 82 |
|
|---|
| 83 | test(`${pad}: een vrienden-post staat er helemaal niet in`, async () => {
|
|---|
| 84 | const n = (await notes(pad)).find((x) => x.id === NOTE('p-fan'));
|
|---|
| 85 | assert.equal(n, undefined, 'besloten hoort niet in een publiek opvraagbare collectie');
|
|---|
| 86 | });
|
|---|
| 87 | }
|
|---|