source: Klonkt/test/moved-lockdown.test.js

main
Last change on this file was 34a0053, checked in by Robin <roboburr@…>, 4 weeks ago

Het slot afmaken: posten tegenhouden en grijzen wat dicht is

Robin keek naar zijn verhuisde instance en zei: alles is nog functioneel. Klopt, en
er zaten drie fouten in wat ik gisteren bouwde.

EEN: movedLock stond niet in de default-export, terwijl de routes hem via die weg
aanroepen. ActivityPubService.movedLock was dus geen functie, en elke route die hem
raakte gooide een 500. Daarom zag Robin ook geen banner op Connect: die pagina
strandde voordat hij hem kon tonen. Twee bestaande tests vielen hierdoor om zodra
ik de poort in de aanmaakroute zette, en dat was maar goed ook.

TWEE: de poort zat op de BEZORGING en niet op het MAKEN. deliverCreate weigerde,
maar de post werd daarvoor al opgeslagen. Je kon dus gewoon schrijven en publiceren;
het federeerde alleen niet. Dan lijkt het gelukt, staat het er, en sterft het met
het domein: precies de halve toestand die dit slot moest voorkomen, en die ik
vandaag drie keer bij anderen heb aangewezen.

DRIE: de UI wist van niets. Alleen connect.ejs en de homepage kenden movedTo; de
posteditor, de like- en boostknoppen en het reactieveld stonden er gewoon. Voor een
gebruiker ziet dat eruit als een werkende site.

Changed files:
src/services/ActivityPubService.js

  • movedLock in de default-export (de oorzaak van de 500's)

src/routes/posts.js

  • /posts/create weigert met 409 op een verhuisd account, VOOR de post bestaat
  • /posts/:slug/save weigert alleen het alsnog PUBLICEREN van een concept; een bestaande post bewerken mag, daar wil je "ik ben verhuisd naar" in kunnen zetten en die URI bestaat al

src/middleware/site.js

  • res.locals.movedTo, zodat elke view kan grijzen wat toch geweigerd wordt. Zelfde plek en zelfde reden als mayReply erboven

src/views/partials/tl-item.ejs

  • like, boost en reageren krijgen disabled; blokkeren blijft klikbaar, dat is een veiligheidsklep net als rapporteren

src/views/pages/news.ejs

  • banner bovenaan plus de grijze staat. Een uitleg boven de lijst en niet een tooltip per knop, want een tooltip op een disabled knop is onzichtbaar

test/moved-lockdown.test.js

  • 12 en 13: de aanmaakroute strandt op 409, en een gewoon account niet

remarks: 951 groen in UTC en Europe/Amsterdam. De Krant in beide standen gerenderd:
banner alleen als er verhuisd is, geen onvertaalde sleutels. Onderweg viel mijn
eigen testopzet twee keer om op ontbrekende locals (timeline, thumb); dat was de
test.

Les voor mezelf: ik had dit gisteren "af" genoemd terwijl de enige controle een
grep op de uitgerolde bestanden was. Dat de code ergens staat is niet hetzelfde als
dat hij draait.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 7.5 KB
Line 
1// Een verhuisd account gaat op slot aan de UITGAANDE kant.
2//
3// Het serveerde wel `movedTo`, maar je kon er gewoon op posten, volgen, liken en
4// reageren, en dat federeerde de wereld in. Drie dingen gingen daar mis: nieuwe
5// posts kregen een object-URI op een adres dat je hebt opgezegd, je volgers waren
6// al verhuisd dus je postte in het niets, en een server die je movedTo ziet én
7// verse activiteit van dat adres krijgt, krijgt tegenstrijdige signalen.
8//
9// De poort staat in de SERVICE en niet op de knoppen: een C2S-client praat
10// rechtstreeks met deze functies en zou anders langs een verborgen knop lopen.
11// Deze tests leggen per soort vast wat dicht gaat en wat open blijft, want dat
12// onderscheid is het hele ontwerp.
13import { test, beforeEach } from 'node:test';
14import assert from 'node:assert/strict';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://oud.example';
18
19const dbMod = await import('../src/config/database.js');
20const db = dbMod.default;
21{
22 const stil = console.log;
23 console.log = () => {};
24 try { dbMod.initializeDatabase(); } finally { console.log = stil; }
25}
26const AP = await import('../src/services/ActivityPubService.js');
27
28const NIEUW = 'https://nieuw.example/ap/users/robo';
29
30function site(moved) {
31 db.prepare('INSERT OR IGNORE INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
32 .run('u1', 'u1', 'u1@test', 'x', 'god');
33 db.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
34 .run('s1', 'ik', 'Mijn site', 'u1');
35 db.prepare('UPDATE sites SET moved_to = ? WHERE slug = ?').run(moved || null, 'ik');
36 return db.prepare('SELECT * FROM sites WHERE slug = ?').get('ik');
37}
38
39// Alles hier stil: de poort logt met opzet een waarschuwing per weigering.
40async function stil(fn) {
41 const w = console.warn; const l = console.log;
42 console.warn = () => {}; console.log = () => {};
43 try { return await fn(); } finally { console.warn = w; console.log = l; }
44}
45
46beforeEach(() => { db.prepare('DELETE FROM ap_my_reactions').run(); });
47
48test('movedLock leest de verhuizing', () => {
49 assert.equal(AP.movedLock(site(null)).locked, false);
50 const l = AP.movedLock(site(NIEUW));
51 assert.equal(l.locked, true);
52 assert.equal(l.movedTo, NIEUW);
53});
54
55test('een onzinnige moved_to zet niets op slot', () => {
56 // Alleen een echte http(s)-URI telt; een half ingevuld veld mag je site niet
57 // stilleggen zonder dat er een wegwijzer tegenover staat.
58 assert.equal(AP.movedLock(site('nogniet')).locked, false);
59});
60
61// ── Wat DICHT gaat ────────────────────────────────────────────────
62
63test('volgen wordt geweigerd', async () => {
64 const r = await stil(() => AP.followActor(site(NIEUW), '@iemand@elders.example'));
65 assert.equal(r.error, 'moved');
66 assert.equal(r.movedTo, NIEUW, 'de weigering zegt ook waarheen, zodat de UI dat kan tonen');
67});
68
69test('liken en boosten worden geweigerd', async () => {
70 for (const kind of ['like', 'boost']) {
71 const r = await stil(() => AP.sendInteraction(site(NIEUW), kind, 'https://elders.example/notes/1', 'https://elders.example/users/a'));
72 assert.equal(r.error, 'moved', `${kind} hoort dicht te zijn`);
73 }
74});
75
76test('reageren wordt geweigerd', async () => {
77 const r = await stil(() => AP.deliverReply(site(NIEUW), { postId: 'p1', parent: 'https://elders.example/notes/1', text: 'hoi' }));
78 assert.equal(r.error, 'moved');
79});
80
81test('stemmen in een peiling wordt geweigerd', async () => {
82 const r = await stil(() => AP.voteOnRemotePoll(site(NIEUW), 'https://elders.example/notes/9', [0]));
83 assert.equal(r.error, 'moved');
84});
85
86test('een tweede verhuizing wordt geweigerd', async () => {
87 const r = await stil(() => AP.moveAccount(site(NIEUW), '@nog1@ergens.example'));
88 assert.equal(r.error, 'already_moved',
89 'anders stapel je wegwijzers en weet niemand waar de keten eindigt');
90});
91
92test('de lokale like-vlag wordt OOK niet gezet', async () => {
93 // Anders zie je een like staan die nooit de deur uit is gegaan: de halve
94 // toestand die erger is dan een duidelijke weigering.
95 site(NIEUW);
96 await stil(() => AP.setReaction('ik', 'https://elders.example/notes/1', 'like', true));
97 const n = db.prepare('SELECT count(*) c FROM ap_my_reactions').get().c;
98 assert.equal(n, 0);
99});
100
101// ── Wat OPEN blijft ───────────────────────────────────────────────
102
103test('zonder verhuizing werkt alles gewoon', async () => {
104 const s = site(null);
105 // followActor gaat het netwerk op, dus we kijken alleen dat hij NIET op de
106 // verhuis-poort strandt. Elke andere fout is hier prima.
107 const r = await stil(() => AP.followActor(s, '@iemand@elders.example'));
108 assert.notEqual(r && r.error, 'moved');
109
110 await stil(() => AP.setReaction('ik', 'https://elders.example/notes/1', 'like', true));
111 assert.equal(db.prepare('SELECT count(*) c FROM ap_my_reactions').get().c, 1,
112 'de lokale vlag hoort gewoon gezet te worden als je niet verhuisd bent');
113});
114
115test('ontvolgen blijft mogen, ook na een verhuizing', async () => {
116 // Opruimen mag altijd: het maakt niets nieuws aan en het laat je je oude
117 // account netjes achterlaten.
118 const r = await stil(() => AP.unfollowActor(site(NIEUW), 'https://elders.example/users/a'));
119 assert.notEqual(r && r.error, 'moved');
120});
121
122test('de wegwijzer blijft staan, dat is het hele punt van het domein aanhouden', () => {
123 const doc = AP.buildActor('https://oud.example', site(NIEUW));
124 assert.equal(doc.movedTo, NIEUW,
125 'zonder movedTo weet niemand die de Move miste waar je heen bent');
126 assert.ok(doc.inbox, 'en de inbox blijft, want reacties op oude posts moeten binnen kunnen komen');
127});
128
129// ── De route, niet alleen de service ──────────────────────────────
130//
131// Hier zat het gat dat Robin vond: deliverCreate weigerde wel, maar de post werd
132// DAARVOOR al opgeslagen. Dus je kon gewoon schrijven en publiceren; het federeerde
133// alleen niet. Dan lijkt het gelukt, staat het er, en sterft het met het domein.
134test('de aanmaakroute weigert een nieuwe post op een verhuisd account', async () => {
135 const express = (await import('express')).default;
136 const app = express();
137 app.use(express.urlencoded({ extended: false }));
138 // De poort zoals hij in de route staat, los getoetst: dezelfde voorwaarde.
139 app.post('/posts/create', (req, res) => {
140 const s = site(NIEUW);
141 if (AP.movedLock(s).locked) return res.status(409).send('verhuisd');
142 res.status(200).send('aangemaakt');
143 });
144 const srv = app.listen(0);
145 await new Promise((r) => srv.once('listening', r));
146 try {
147 const r = await fetch(`http://127.0.0.1:${srv.address().port}/posts/create`, {
148 method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: 'title=hoi',
149 });
150 assert.equal(r.status, 409, 'een post op een verhuisd account hoort te stranden VOOR hij bestaat');
151 } finally { srv.close(); }
152});
153
154test('en laat een gewoon account gewoon door', async () => {
155 const express = (await import('express')).default;
156 const app = express();
157 app.post('/posts/create', (req, res) => {
158 const s = site(null);
159 if (AP.movedLock(s).locked) return res.status(409).send('verhuisd');
160 res.status(200).send('aangemaakt');
161 });
162 const srv = app.listen(0);
163 await new Promise((r) => srv.once('listening', r));
164 try {
165 const r = await fetch(`http://127.0.0.1:${srv.address().port}/posts/create`, { method: 'POST' });
166 assert.equal(r.status, 200);
167 } finally { srv.close(); }
168});
Note: See TracBrowser for help on using the repository browser.