| 1 | // Een betaalde post lekt zijn inhoud nergens (klonkt-demo-aki).
|
|---|
| 2 | //
|
|---|
| 3 | // Aanleiding (Bart, 15-8): https://boiert.eu/we-should-explore-at-least-something
|
|---|
| 4 | // stond met 4555 tekens VOLLEDIG in de publieke outbox -- onbetekend op te
|
|---|
| 5 | // halen. Die post bleek niet als `paid` gemarkeerd: "PAID" stond alleen in de
|
|---|
| 6 | // waarschuwingstekst, en dat is tekst, geen poort.
|
|---|
| 7 | //
|
|---|
| 8 | // Dat de redactie op EEN plek hangt (de vroege return in buildNote) maakt hem
|
|---|
| 9 | // kwetsbaar: elk pad dat post.content serveert zonder buildNote lekt meteen, en
|
|---|
| 10 | // zonder foutmelding. Deze tests leggen vast dat geen enkele weg dat doet.
|
|---|
| 11 | import { test } from 'node:test';
|
|---|
| 12 | import assert from 'node:assert/strict';
|
|---|
| 13 |
|
|---|
| 14 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 15 | process.env.PUBLIC_BASE_URL = 'https://ons.test';
|
|---|
| 16 |
|
|---|
| 17 | const dbMod = await import('../src/config/database.js');
|
|---|
| 18 | const db = dbMod.default;
|
|---|
| 19 | dbMod.initializeDatabase();
|
|---|
| 20 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 21 |
|
|---|
| 22 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 23 | .run('u1', 'u1', 'u1@t', 'x', 'god');
|
|---|
| 24 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public) VALUES (?,?,?,?,1)')
|
|---|
| 25 | .run('s1', 'dev', 'Dev', 'u1');
|
|---|
| 26 | const site = () => db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
|
|---|
| 27 |
|
|---|
| 28 | const GEHEIM = 'DIT IS DE BETAALDE INHOUD DIE NERGENS MAG STAAN';
|
|---|
| 29 |
|
|---|
| 30 | function maak({ id, slug, paid = 0, fan = 0, nsfw = 0, cw = null }) {
|
|---|
| 31 | db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, excerpt, content,
|
|---|
| 32 | status, published_at, paid, fan_only, nsfw, content_warning)
|
|---|
| 33 | VALUES (?,?,?,?,?,?,?,'published',?,?,?,?,?)`)
|
|---|
| 34 | .run(id, 's1', 'u1', slug, 'Titel', 'Een teaser.',
|
|---|
| 35 | `<p>${GEHEIM}</p><p>en nog meer</p>`, '2026-08-15T12:00:00Z', paid, fan, nsfw, cw);
|
|---|
| 36 | return db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
|
|---|
| 37 | }
|
|---|
| 38 |
|
|---|
| 39 | const alleTekst = (o) => JSON.stringify(o);
|
|---|
| 40 |
|
|---|
| 41 | test('een betaalde post geeft een teaser, nooit de inhoud', () => {
|
|---|
| 42 | const note = AP.buildNote('https://ons.test', site(), maak({ id: 'p1', slug: 'betaald', paid: 1 }));
|
|---|
| 43 | assert.ok(!alleTekst(note).includes(GEHEIM), 'de inhoud staat nergens in het object');
|
|---|
| 44 | assert.match(note.content, /Een teaser/);
|
|---|
| 45 | });
|
|---|
| 46 |
|
|---|
| 47 | test('ook niet via de OUTBOX, het pad waar het misging', () => {
|
|---|
| 48 | // Bart zag hem juist daar: de outbox serveert Create-activiteiten en die
|
|---|
| 49 | // dragen het Note-object. Als de redactie daar niet meekomt, lekt alles.
|
|---|
| 50 | const p = maak({ id: 'p2', slug: 'betaald-2', paid: 1 });
|
|---|
| 51 | const ob = AP.buildOutbox('https://ons.test', site(), [p], []);
|
|---|
| 52 | assert.ok(!alleTekst(ob).includes(GEHEIM), 'ook de outbox draagt hem niet');
|
|---|
| 53 | });
|
|---|
| 54 |
|
|---|
| 55 | test('en niet voor een VRIEND, want de redactie kent geen publiek', () => {
|
|---|
| 56 | // outboxAudience geeft 'friend' aan iedere volger; fan_only laat die posts dan
|
|---|
| 57 | // mee. Een betaalde post mag dat NIET volgen -- betalen is iets anders dan
|
|---|
| 58 | // volgen.
|
|---|
| 59 | const p = maak({ id: 'p3', slug: 'betaald-vrienden', paid: 1, fan: 1 });
|
|---|
| 60 | const ob = AP.buildOutbox('https://ons.test', site(), [p], []);
|
|---|
| 61 | assert.ok(!alleTekst(ob).includes(GEHEIM));
|
|---|
| 62 | });
|
|---|
| 63 |
|
|---|
| 64 | test('een SENSITIVE betaalde post houdt zijn waarschuwing', () => {
|
|---|
| 65 | // Hier zat de tweede fout: de vroege return liet `sensitive` en `summary`
|
|---|
| 66 | // vallen, dus een betaalde post met een waarschuwing kwam ZONDER die
|
|---|
| 67 | // waarschuwing de deur uit. De teaser is publiek; dan hoort de waarschuwing
|
|---|
| 68 | // dat ook te zijn.
|
|---|
| 69 | const p = maak({ id: 'p4', slug: 'betaald-nsfw', paid: 1, nsfw: 1, cw: 'Smut' });
|
|---|
| 70 | const note = AP.buildNote('https://ons.test', site(), p);
|
|---|
| 71 | assert.ok(!alleTekst(note).includes(GEHEIM), 'inhoud nog steeds weg');
|
|---|
| 72 | assert.equal(note.sensitive, true, 'de vlag reist mee');
|
|---|
| 73 | assert.equal(note.summary, 'Smut', 'en de waarschuwingstekst ook');
|
|---|
| 74 | });
|
|---|
| 75 |
|
|---|
| 76 | test('een NIET-betaalde post lekt niets extra: die hoort gewoon leesbaar', () => {
|
|---|
| 77 | // De tegenproef, zodat de test niet slaagt door alles dicht te gooien.
|
|---|
| 78 | const note = AP.buildNote('https://ons.test', site(), maak({ id: 'p5', slug: 'gewoon' }));
|
|---|
| 79 | assert.ok(alleTekst(note).includes(GEHEIM), 'een gewone post draagt zijn inhoud wel');
|
|---|
| 80 | });
|
|---|