source: Klonkt/test/ap-collection-row-fields.test.js

main
Last change on this file was 4425cc2, checked in by Robin <roboburr@…>, 4 days ago

Featured: de kolommen die buildNote nodig heeft (shaer-6oth, laatste punt)

De bead vroeg nog na te gaan of dezelfde kolommen ergens anders ontbraken
waar buildNote op een DB-rij wordt losgelaten. Alle acht aanroeppunten
langsgelopen: zes zijn schoon (twee lezen SELECT *, backfillNewFollower is
15-8 al bijgewerkt, en de drie handgebouwde rijen in de webroutes geven de
velden mee). De featured-collectie was het gat.

De gok in de bead was half raak. Featured filtert wel op fan_only --
daarom leek hij goed -- maar selecteert ap_visibility, paid en excerpt
niet, en filtert daar ook niet op. Twee gevolgen, op een route die
publiek en onbetekend opvraagbaar is:

een vastgezette BETAALDE post lekte zijn volledige tekst, want
buildNote slaat zijn redactie over zodra post.paid undefined is. Precies
het mechanisme van Barts melding van 15-8; toen zijn outboxSlice en
backfillNewFollower bijgewerkt en is deze route gemist.

een vastgezette QUIET-post kreeg to: as:Public in plaats van zijn
volgers -- luider dan de schrijver koos, dezelfde vorm als shaer-6oth.

excerpt hoort erbij, anders valt de teaser na de reparatie terug op de
eerste alinea van precies de tekst die verborgen moet blijven. Het
visibility-filter erbij: niet-publiek hoort niet in een publieke
collectie, ook niet vastgezet.

De toets is een LUS over de publieke collecties die posts dragen, niet
een toets op deze ene query -- dezelfde reden die ap-collection-paging
al noemt: het gat ontstond doordat de ene plek het wel had en de andere
niet, en een toets per plek vraagt om dezelfde vergeetachtigheid. Over
HTTP, want de query zit in de route.

Tegenbewijs: met de kolommen er weer uit vallen precies de twee
featured-toetsen (het lek is dus gemeten, niet beredeneerd), blijft de
outbox groen omdat die al gerepareerd was, en blijft de vrienden-toets
groen -- wat bevestigt dat het fan_only-filter er altijd al was. Eerste
poging tot dat tegenbewijs sloeg niet aan doordat de mutatie niet
matchte; die is overgedaan met controle dat hij er echt in zat.

Volle suite 1245 groen.

  • Property mode set to 100644
File size: 4.9 KB
Line 
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.
17import { test } from 'node:test';
18import assert from 'node:assert/strict';
19
20process.env.DATABASE_PATH = ':memory:';
21process.env.PUBLIC_BASE_URL = 'https://test.example';
22
23const dbMod = await import('../src/config/database.js');
24const db = dbMod.default;
25{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
26const express = (await import('express')).default;
27const routes = (await import('../src/routes/activitypub.js')).default;
28
29db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
30 .run('u1', 'u1', 'u1@t', 'x', 'god');
31db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
32 .run('s1', 'band', 'De Band', 'u1');
33
34const GEHEIM = 'DIT IS DE BETAALDE TEKST';
35const 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.
38insPost.run('p-paid', 's1', 'u1', 'betaald', 'Betaald', `<p>${GEHEIM}</p>`, 'De teaser', '2026-08-01T00:00:00Z', 0, 'public', 1, 500, 3);
39insPost.run('p-quiet', 's1', 'u1', 'stil', 'Stil', '<p>stil</p>', '', '2026-08-02T00:00:00Z', 0, 'quiet', 0, null, 2);
40insPost.run('p-fan', 's1', 'u1', 'vrienden', 'Vrienden', '<p>alleen vrienden</p>', '', '2026-08-03T00:00:00Z', 1, 'friends', 0, null, 1);
41
42const app = express(); app.use(routes);
43const server = app.listen(0);
44await new Promise((r) => server.once('listening', r));
45const base = `http://127.0.0.1:${server.address().port}`;
46test.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.
50const 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). */
53async 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
61const NOTE = (id) => `https://test.example/ap/notes/${id}`;
62
63for (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}
Note: See TracBrowser for help on using the repository browser.