source: Klonkt/test/gates-family.test.js@ 8a474ab

main
Last change on this file since 8a474ab was 8a474ab, checked in by Robin <roboburr@…>, 7 hours ago

Poorten per soort: ook bij het versturen, en film heeft er nu een

Twee gaten die naast elkaar stonden en elkaar versterkten.

gate_images en gate_music golden alleen bij het SERVEREN (shaer-qc9o). Wat een
ward niet te zien kreeg mocht hij wel plaatsen, dus het stond bij iedereen
behalve bij hemzelf. Dat is geen poort meer maar een filter op zijn eigen
scherm, en dat is niet wat een guardian dichtzet. De innamekant weigert nu met
gated_images / gated_music / gated_video, in dezelfde vorm als compose en
messages.

Weigeren en niet wegknippen: stilletjes de bijlage verwijderen publiceert een
bericht dat het kind niet geschreven heeft.

En film had helemaal geen poort (shaer-mxh2), dus de zwaarste soort van de drie
was de enige die altijd door mocht. Nu een gewone rij in de catalogus met een
eigen kolom, aan beide kanten afgedwongen: gateAttachments knipt video weg als
hij dicht staat (ook als hij als AS2-Video zonder mediaType binnenkomt) en de
outbox neemt hem niet aan. shaer:video staat in de caps, zodat de app het
vooraf weet in plaats van het bij de eerste weigering te ontdekken.

De reddingsboei gaat door alle drie heen, zoals door elke andere dichte deur.
Een hulpvraag draagt juist vaak een schermafdruk.

uploadMedia zelf blijft open, en dat is met opzet: de boei gebruikt dezelfde
poot en de server weet daar nog niet waar een upload voor bedoeld is. Een
bijlage die blijft liggen kost een bestand; een boei die vastloopt kost meer.
De weigering valt bij het innemen van de post, en daar is het wel bekend.

Getoetst in test/gates-family.test.js, naast de poorten die er al stonden.
Tegenproef: met de video-regel en de innamelus eruit vallen precies de twee
nieuwe toetsen om.

  • Property mode set to 100644
File size: 13.7 KB
RevLine 
[3b43e4c]1// De gate-familie functioneel (shaer-ahy.1, Barts opdracht 8-8): niet alleen
2// een toets in het paneel, maar een poort die echt dichtkan -- bij de
3// AFLEVERING (wat dicht is wordt nooit geserialiseerd) of bij de INNAME (wat
4// de ward niet mag versturen wordt aan de outbox geweigerd).
5//
6// De twee dingen die hier stil kunnen breken: een poort die dicht lijkt maar
7// niets tegenhoudt (een guardian waant het kind beschermd), en een poort die
8// het hulpkanaal mee afsluit (de boei MOET door elke dichte deur heen).
9//
10// In-memory SQLite. Run: npm test
11import { test } from 'node:test';
12import assert from 'node:assert/strict';
13
14process.env.DATABASE_PATH = ':memory:';
15process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
16
17const dbMod = await import('../src/config/database.js');
18const db = dbMod.default;
19dbMod.initializeDatabase();
20const AP = await import('../src/services/ActivityPubService.js');
21const Guardianship = await import('../src/services/guardianship/index.js');
22
23db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
24 .run('u1', 'u1', 'u1@test', 'x', 'god');
25db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'kind', 'Kind', 'u1');
26const site = () => db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
27const user = { id: 'u1', username: 'u1' };
28
29test('elke beschikbare gate in de catalogus heeft een kolom', () => {
30 // De bewaker van de bedrading: een catalogusrij zonder kolom is een toets
31 // die nergens op aangesloten is, en dat merkt pas iemand die erop drukt.
32 for (const g of Guardianship.gated.GATE_CATALOGUE.filter((x) => x.available !== false)) {
33 if (g.fixed) continue; // shaer:follows is een stroom, geen stand
34 assert.ok(Guardianship.gated.featureColumn(g.feature), `${g.feature} mist een kolom`);
35 }
36});
37
38test('de generieke regel: expliciet wint, de automatiek sluit voor een ward', () => {
39 assert.equal(Guardianship.wardGateAllowed(null, false), true);
40 assert.equal(Guardianship.wardGateAllowed(null, true), false);
41 assert.equal(Guardianship.wardGateAllowed(1, true), true); // guardians openden hem
42 assert.equal(Guardianship.wardGateAllowed(0, false), false); // expliciete 0 wint ook zonder wardstatus
43});
44
45test('gateAttachments: beeld en muziek dicht laten de rest staan, leeg wordt undefined', () => {
46 const atts = [
47 { type: 'Document', mediaType: 'image/webp', url: 'https://x/p.webp' },
48 { type: 'Audio', mediaType: 'audio/mpeg', url: 'https://x/a.mp3' },
49 { type: 'Document', mediaType: 'video/mp4', url: 'https://x/v.mp4' },
50 ];
51 const zonderBeeld = AP.gateAttachments(atts, { images: false });
52 assert.deepEqual(zonderBeeld.map((a) => a.mediaType), ['audio/mpeg', 'video/mp4']);
53 const zonderMuziek = AP.gateAttachments(atts, { audio: false });
54 assert.deepEqual(zonderMuziek.map((a) => a.mediaType), ['image/webp', 'video/mp4']);
55 // Alles weg is undefined, zoals de serialisatie een leeg veld overal weglaat.
56 assert.equal(AP.gateAttachments([atts[0]], { images: false }), undefined);
57 // En de poort open verandert niets.
58 assert.equal(AP.gateAttachments(atts, {}).length, 3);
59});
60
61test('stripEmojiTags haalt alleen de Emoji-tags weg', () => {
62 const tags = [
63 { type: 'Emoji', name: ':hartje:', icon: { url: 'https://x/h.png' } },
64 { type: 'Mention', href: 'https://x/u', name: '@u' },
65 { type: 'Hashtag', name: '#muziek' },
66 ];
67 const uit = AP.stripEmojiTags(tags);
68 assert.deepEqual(uit.map((t) => t.type), ['Mention', 'Hashtag']);
69 assert.equal(AP.stripEmojiTags([tags[0]]), undefined);
70});
71
72test('compose dicht weigert een eigen post aan de outbox, maar geen antwoord', async () => {
73 db.prepare("UPDATE sites SET gate_compose = 0 WHERE slug = 'kind'").run();
74 const post = { type: 'Create', object: { type: 'Note', content: '<p>hoi</p>', to: ['https://www.w3.org/ns/activitystreams#Public'] } };
75 const uit = await AP.ingestOutboxActivity(site(), user, post);
76 assert.equal(uit.status, 403);
77 assert.equal(uit.error, 'gated_compose');
78 // Een antwoord is meedoen aan een gesprek, geen eigen podium: dat valt
79 // onder messages/het gesprek, niet onder compose.
80 const reply = { type: 'Create', object: { type: 'Note', content: '<p>ja!</p>', inReplyTo: 'https://elders/x', to: ['https://www.w3.org/ns/activitystreams#Public'] } };
81 const uit2 = await AP.ingestOutboxActivity(site(), user, reply);
82 assert.notEqual(uit2.error, 'gated_compose');
83 db.prepare("UPDATE sites SET gate_compose = NULL WHERE slug = 'kind'").run();
84});
85
[ffb371c]86// ── Antwoorden: een eigen poort (shaer-r4c) ─────────────────────────────
87//
88// Hier stond de aanname dat een antwoord meedoen is en geen eigen podium, en
89// dus onder compose door mocht. Bart heeft die teruggedraaid: meedoen aan een
90// gesprek valt ook onder een poort. Wat deze toetsen bewaken is dat het een
91// EIGEN poort is en geen aanhangsel van compose -- anders is hij in het paneel
92// wel te zien maar niet los te zetten.
93
94test('replies dicht weigert een antwoord', async () => {
95 db.prepare("UPDATE sites SET gate_replies = 0 WHERE slug = 'kind'").run();
96 const reply = { type: 'Create', object: { type: 'Note', content: '<p>ja!</p>', inReplyTo: 'https://elders/x', to: ['https://www.w3.org/ns/activitystreams#Public'] } };
97 const uit = await AP.ingestOutboxActivity(site(), user, reply);
98 assert.equal(uit.status, 403);
99 assert.equal(uit.error, 'gated_replies');
100 db.prepare("UPDATE sites SET gate_replies = NULL WHERE slug = 'kind'").run();
101});
102
103test('replies dicht laat een EIGEN post staan', async () => {
104 // Los van compose, en dat is de hele reden dat het een eigen rij is: je kunt
105 // willen dat een kind een podium heeft zonder in vreemde draadjes te duiken.
106 db.prepare("UPDATE sites SET gate_replies = 0 WHERE slug = 'kind'").run();
107 const post = { type: 'Create', object: { type: 'Note', content: '<p>hoi</p>', to: ['https://www.w3.org/ns/activitystreams#Public'] } };
108 const uit = await AP.ingestOutboxActivity(site(), user, post);
109 assert.notEqual(uit.error, 'gated_replies');
110 assert.notEqual(uit.error, 'gated_compose');
111 db.prepare("UPDATE sites SET gate_replies = NULL WHERE slug = 'kind'").run();
112});
113
114test('compose dicht laat een antwoord staan zolang replies open is', async () => {
115 // De andere richting van dezelfde onafhankelijkheid. Zou compose ook
116 // antwoorden dichtzetten, dan is de nieuwe rij een knop die niets doet.
117 db.prepare("UPDATE sites SET gate_compose = 0, gate_replies = 1 WHERE slug = 'kind'").run();
118 const reply = { type: 'Create', object: { type: 'Note', content: '<p>ja!</p>', inReplyTo: 'https://elders/x', to: ['https://www.w3.org/ns/activitystreams#Public'] } };
119 const uit = await AP.ingestOutboxActivity(site(), user, reply);
120 assert.notEqual(uit.error, 'gated_compose');
121 assert.notEqual(uit.error, 'gated_replies');
122 db.prepare("UPDATE sites SET gate_compose = NULL, gate_replies = NULL WHERE slug = 'kind'").run();
123});
124
125test('replies dicht houdt ook een DIRECT antwoord tegen', async () => {
126 // Een prive-antwoord is allebei: meedoen aan een gesprek en een bericht. Dan
127 // mag allebei hem tegenhouden. Zou alleen de messages-poort gelden, dan is
128 // dat het gat waar een dichte replies-poort omheen loopt.
129 db.prepare("UPDATE sites SET gate_replies = 0, gate_messages = 1 WHERE slug = 'kind'").run();
130 const dm = { type: 'Create', object: { type: 'Note', content: '<p>psst</p>', inReplyTo: 'https://elders/x', to: ['https://elders/ap/users/vreemde'] } };
131 const uit = await AP.ingestOutboxActivity(site(), user, dm);
132 assert.equal(uit.error, 'gated_replies');
133 db.prepare("UPDATE sites SET gate_replies = NULL, gate_messages = NULL WHERE slug = 'kind'").run();
134});
135
136test('replies dicht laat de REDDINGSBOEI door, ook als die een antwoord is', async () => {
137 // Het gevaarlijkste dat deze poort kan doen. Een kind dat om hulp vraagt in
138 // een draadje waar het misgaat, is precies het geval waarvoor de boei bestaat.
139 db.prepare("UPDATE sites SET gate_replies = 0 WHERE slug = 'kind'").run();
140 const hulp = { type: 'Create', object: { type: 'Note', content: '<p>🛟</p>', 'shaer:helpRequest': true, inReplyTo: 'https://elders/x', to: ['https://elders/ap/users/oma'] } };
141 const uit = await AP.ingestOutboxActivity(site(), user, hulp);
142 assert.notEqual(uit.error, 'gated_replies');
143 db.prepare("UPDATE sites SET gate_replies = NULL WHERE slug = 'kind'").run();
144});
145
[3b43e4c]146test('messages dicht weigert een direct bericht, maar NOOIT de reddingsboei', async () => {
147 db.prepare("UPDATE sites SET gate_messages = 0 WHERE slug = 'kind'").run();
148 const dm = { type: 'Create', object: { type: 'Note', content: '<p>psst</p>', to: ['https://elders/ap/users/vreemde'] } };
149 const uit = await AP.ingestOutboxActivity(site(), user, dm);
150 assert.equal(uit.status, 403);
151 assert.equal(uit.error, 'gated_messages');
152 // De boei gaat door elke dichte deur heen: een poort die het hulpkanaal
153 // afsnijdt beschermt niemand.
154 const hulp = { type: 'Create', object: { type: 'Note', content: '<p>🛟</p>', 'shaer:helpRequest': true, to: ['https://elders/ap/users/oma'] } };
155 const uit2 = await AP.ingestOutboxActivity(site(), user, hulp);
156 assert.notEqual(uit2.error, 'gated_messages');
157 db.prepare("UPDATE sites SET gate_messages = NULL WHERE slug = 'kind'").run();
158});
159
160test('accountMove dicht weigert de verhuizing voordat er iets vertrekt', async () => {
161 db.prepare("UPDATE sites SET gate_account_move = 0 WHERE slug = 'kind'").run();
162 const uit = await AP.moveAccount(site(), '@iemand@elders.example');
163 assert.equal(uit.error, 'guarded_account');
164 db.prepare("UPDATE sites SET gate_account_move = NULL WHERE slug = 'kind'").run();
165});
[ffb371c]166
167test('replies dicht houdt OOK het webpad tegen, niet alleen de app', async () => {
168 // routes/posts.js roept deliverReply rechtstreeks aan en gaat nooit langs
169 // ingestOutboxActivity. Een poort die alleen in C2S staat heeft een deur
170 // ernaast -- en die deur is de eigen webinterface van het kind.
171 db.prepare("UPDATE sites SET gate_replies = 0 WHERE slug = 'kind'").run();
172 const parent = { id: 'https://elders/x', attributedTo: 'https://elders/ap/users/vreemde' };
173 const uit = await AP.deliverReply(site(), { postId: '', postSlug: null, parent, text: 'ja!' });
174 assert.equal(uit, null);
175 db.prepare("UPDATE sites SET gate_replies = NULL WHERE slug = 'kind'").run();
176});
[8a474ab]177
178// ── De soort-poorten: ook bij het VERSTUREN, en film heeft er nu een ──────
179//
180// Twee gaten die naast elkaar stonden en elkaar versterkten (shaer-qc9o en
181// shaer-mxh2). gate_images en gate_music golden alleen bij het SERVEREN: wat
182// een ward niet te zien kreeg mocht hij wel plaatsen, dus het stond bij
183// iedereen behalve bij hemzelf. En film had helemaal geen poort, dus de
184// zwaarste soort van de drie was de enige die altijd door mocht.
185//
186// Samen betekende dat: een kind met plaatjes en muziek dicht kon een half uur
187// video de wereld in sturen, en het paneel van de guardian zei dat alles wat
188// er te sluiten viel gesloten was.
189
190const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
191const beeld = { type: 'Document', mediaType: 'image/jpeg', url: 'https://klonkt.test/media/x.jpg' };
192const film = { type: 'Document', mediaType: 'video/mp4', url: 'https://klonkt.test/media/x.mp4' };
193const geluid = { type: 'Audio', mediaType: 'audio/mpeg', url: 'https://klonkt.test/media/x.mp3' };
194
195/** Alle poorten open, behalve die je noemt. Zo toetst elke zaak zijn eigen
196 * weigering en niet een die van een vorige toets bleef staan. */
197function poorten(dicht = {}) {
198 const kolommen = ['gate_compose', 'gate_replies', 'gate_messages', 'gate_images', 'gate_music', 'gate_video'];
199 const zet = kolommen.map((k) => `${k} = ${dicht[k] === 0 ? 0 : 1}`).join(', ');
200 db.prepare(`UPDATE sites SET ${zet} WHERE slug = 'kind'`).run();
201}
202
203test('film wordt bij het serveren weggeknipt als de video-poort dicht is', () => {
204 const atts = [beeld, geluid, film];
205 assert.deepEqual(AP.gateAttachments(atts, { video: false }).map((a) => a.mediaType), ['image/jpeg', 'audio/mpeg']);
206 // Ook als hij als AS2-Video binnenkomt zonder mediaType: de soort staat dan
207 // in `type`, en een poort die alleen naar mediaType kijkt mist hem.
208 assert.equal(AP.gateAttachments([{ type: 'Video', url: 'https://x/v' }], { video: false }), undefined);
209});
210
211test('een bijlage van een dichte soort wordt aan de outbox geweigerd', async () => {
212 for (const [kolom, bijlage, fout] of [
213 ['gate_images', beeld, 'gated_images'],
214 ['gate_music', geluid, 'gated_music'],
215 ['gate_video', film, 'gated_video'],
216 ]) {
217 poorten({ [kolom]: 0 });
218 const post = { type: 'Create', object: { type: 'Note', content: '<p>kijk</p>', to: [PUBLIC], attachment: [bijlage] } };
219 const uit = await AP.ingestOutboxActivity(site(), user, post);
220 assert.equal(uit.status, 403, `${kolom} dicht hoort te weigeren`);
221 assert.equal(uit.error, fout);
222 }
223});
224
225test('een dichte poort raakt alleen zijn eigen soort', async () => {
226 poorten({ gate_images: 0 });
227 const post = { type: 'Create', object: { type: 'Note', content: '<p>luister</p>', to: [PUBLIC], attachment: [geluid] } };
228 const uit = await AP.ingestOutboxActivity(site(), user, post);
229 assert.notEqual(uit.error, 'gated_images', 'muziek hoort niet op de beeldpoort te stuiten');
230});
231
232test('de reddingsboei gaat door een dichte beeldpoort heen', async () => {
233 // Een hulpvraag draagt vaak juist een schermafdruk: dat is het bewijs van
234 // waar het kind van schrikt. Een dichte beeldpoort mag precies het kanaal
235 // niet sluiten dat hem veilig houdt.
236 poorten({ gate_images: 0, gate_video: 0, gate_music: 0 });
237 const boei = {
238 type: 'Create',
239 object: {
240 type: 'Note', content: '<p>help</p>', 'shaer:helpRequest': true,
241 to: ['https://elders.test/u/oma'], attachment: [beeld],
242 },
243 };
244 const uit = await AP.ingestOutboxActivity(site(), user, boei);
245 assert.notEqual(uit.error, 'gated_images', 'de boei hoort door elke dichte deur heen te gaan');
246});
Note: See TracBrowser for help on using the repository browser.