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

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

Een verhuisd account gaat op slot aan de uitgaande kant

Robins vraag: "nu kan je er nog alles mee". Klopt. moved_to deed maar twee dingen,
de actor-wegwijzer en een banner op de homepage. Posten, volgen, liken, boosten en
reageren kon gewoon door, en dat federeerde de wereld in.

Drie dingen gingen daar mis. Nieuwe posts kregen een object-URI op een adres dat je
hebt opgezegd, en die URI's overleven het domein niet. Je volgers waren al verhuisd,
dus je postte in het niets terwijl het lijkt of je post. En een server die je
movedTo ziet én tegelijk verse activiteit van dat adres krijgt, krijgt
tegenstrijdige signalen over de verhuizing.

DE POORT STAAT IN DE SERVICE, NIET OP DE KNOPPEN. Een C2S-client (Shaer) praat
rechtstreeks met deze functies en zou langs een verborgen knop lopen. De UI volgt
de poort.

Dicht: posten, reageren, volgen, liken, boosten, stemmen, en een tweede verhuizing.
Open: de actor met movedTo, webfinger, bestaande posts, de outbox, alles inkomend
(reacties op oude posts blijven binnenkomen en leesbaar), je eigen beheer (archief
en volglijst exporteren), ontvolgen, en rapporteren. Dat laatste is een
veiligheidsklep, geen inhoud maken.

Omkeerbaar: moved_to leegmaken heft het slot op. Een verhuizing kan mislukken.

setReaction is APART gepoort, want die schrijft alleen de lokale vlag; het
versturen gebeurt elders. Zonder die tweede poort zag je een like staan die nooit
de deur uit was: dezelfde halve toestand die vandaag al twee keer opdook.

Changed files:
src/services/ActivityPubService.js

  • movedLock() en movedRefusal(), met de scheidslijn in het commentaar
  • poorten in deliverCreate, deliverReply, followActor, sendInteraction, voteOnRemotePoll, setReaction en moveAccount

src/routes/posts.js

  • 'moved' krijgt een eigen melding: het is een weigering met een reden, niet een mislukking, en "Volgen mislukt" laat je zoeken naar een storing die er niet is
  • connect krijgt movedTo mee

src/views/pages/connect.ejs

  • banner in plaats van het volgformulier; het volglijst-blok BLIJFT, want je archief meenemen moet je juist kunnen op een verhuisd account

src/services/i18n.js

  • drie teksten in nl, en en de

New file:
test/moved-lockdown.test.js

  • 11 tests die per soort vastleggen wat dicht gaat en wat open blijft
  • inclusief: een half ingevuld moved_to zet niets op slot, ontvolgen mag nog, en de wegwijzer plus de inbox blijven staan

remarks: tegenproef gedaan. Zonder de poort vallen 8 van de 11 om en blijven de
drie "blijft open"-tests groen. Suite 949 in UTC en Europe/Amsterdam. De pagina in
beide standen gerenderd: formulier weg, banner er, exportblok blijft, geen
onvertaalde sleutels.

Doorsturen van reacties op oude posts is bewust NIET gebouwd. Zo'n reply wijst naar
een object-URI met de oude base, en findThreadTarget matcht alleen op onze eigen
base: hij zou als wees binnenkomen. Om het te laten werken zou de nieuwe instance
objecten moeten claimen op een domein dat hij niet beheert, en dat is precies waar
de sleutelbinding van vanmiddag tegen beschermt. Antwoorden doe je vanaf het nieuwe
account via de bestaande bladwijzerroute; dan blijft elke URI bij wie hem hoort.

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

  • Property mode set to 100644
File size: 5.7 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});
Note: See TracBrowser for help on using the repository browser.