Changeset 0ca7e9a4 in Klonkt for test/move-actor.test.js


Ignore:
Timestamp:
07/31/2026 05:43:16 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
bfa6fa1
Parents:
f434294
Message:

Account-verhuizingen slice 2: de uitgaande Move (FEP-7628)

De vertrekkende helft van shaer-0j2: deze Klonkt als het oude huis. Twee
eisen voordat er iets de deur uit gaat. Een: geen guardians; een warded
account verhuizen zonder de guardianship te hertargeten zou het vangnet
van het kind stil breken, dus tot shaer-tge er is weigert een bewaakt
account met een heldere melding. Twee: het nieuwe profiel claimt dit
adres in alsoKnownAs, dezelfde back-reference die elke ontvangende
server (onze slice 1 incluis) eist; zonder die claim is de Move overal
dood bij aankomst.

De Move gaat duurzaam (retry-queue) naar elke volger-inbox; hun servers
doen de re-follow. moved_to wordt vastgelegd; het serveren ervan op de
actor en het beleid van de oude site zijn slice 3. In Beheer > Sites >
bewerken zit de aankondiging als eigen formulier met bevestiging, nooit
als bijeffect van Opslaan: een verhuizing is een deur die je achter je
dichttrekt.

Changed files:
src/services/ActivityPubService.js

  • moveAccount(site, target, {fetchActorFn, deliverFn}): guardian- weigering, resolutie (URL/handle), back-reference-check, moved_to, Move naar alle volger-inboxen (shared inbox de-dupe)

src/config/database.js

  • sites.moved_to (TEXT)

src/routes/admin-sites.js

  • POST /:slug/move met per-fout een heldere redirect-melding

src/views/pages/admin-site-edit.ejs

  • Verhuizen-sectie (eigen form + confirm), toont moved_to

src/services/i18n.js

  • asite.move* in nl/en/de

test/move-actor.test.js

  • guarded weigert (niets vastgelegd), zonder back-reference weigert, happy path: moved_to + een Move per volger-inbox (shared voorop)

remarks: gebouwd op vps/main (f434294) in een worktree, los van de
lopende boost-toggle-sessie in de hoofd-clone. Suite op 370 groen.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/move-actor.test.js

    rf434294 r0ca7e9a4  
    124124  assert.equal('alsoKnownAs' in buildActor('https://test.example', site), false);
    125125});
     126
     127// ── Slice 2: the OUTGOING half ─────────────────────────────────────
     128// This Klonkt as the old home. A guarded account refuses (shaer-tge), a
     129// target without the alsoKnownAs back-reference refuses, and the happy path
     130// records moved_to and delivers ONE Move to every follower inbox.
     131const { moveAccount } = await import('../src/services/ActivityPubService.js');
     132const ME_RADIO = 'https://test.example/ap/users/radio';
     133const radioSite = () => db.prepare("SELECT * FROM sites WHERE slug = 'radio'").get();
     134
     135test('a guarded account refuses to move (shaer-tge)', async () => {
     136  db.prepare(`INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, status, offer_id)
     137              VALUES ('blog', 'ward', 'https://oma.example/u/oma', 'accepted', 'o9')`).run();
     138  const blogSite = db.prepare("SELECT * FROM sites WHERE slug = 'blog'").get();
     139  const r = await moveAccount(blogSite, 'https://elders.example/users/nieuw', {
     140    fetchActorFn: async () => ({ id: 'https://elders.example/users/nieuw', inbox: 'https://elders.example/inbox', alsoKnownAs: ['https://test.example/ap/users/blog'] }),
     141  });
     142  assert.equal(r.error, 'guarded_account');
     143  assert.equal(db.prepare("SELECT moved_to FROM sites WHERE slug = 'blog'").get().moved_to, null, 'nothing recorded');
     144});
     145
     146test('without the back-reference the move refuses', async () => {
     147  const r = await moveAccount(radioSite(), 'https://elders.example/users/nieuw', {
     148    fetchActorFn: async () => ({ id: 'https://elders.example/users/nieuw', inbox: 'https://elders.example/inbox', alsoKnownAs: ['https://iemand-anders.example/x'] }),
     149  });
     150  assert.equal(r.error, 'no_backreference');
     151  assert.equal(db.prepare("SELECT moved_to FROM sites WHERE slug = 'radio'").get().moved_to, null);
     152});
     153
     154test('the happy path: moved_to recorded, one Move to every follower inbox', async () => {
     155  db.prepare("INSERT INTO ap_followers (slug, actor_uri, inbox) VALUES ('radio', 'https://a.example/u/a', 'https://a.example/inbox')").run();
     156  db.prepare("INSERT INTO ap_followers (slug, actor_uri, inbox, shared_inbox) VALUES ('radio', 'https://b.example/u/b', 'https://b.example/inbox', 'https://b.example/shared')").run();
     157  const delivered = [];
     158  const r = await moveAccount(radioSite(), 'https://elders.example/users/nieuw', {
     159    fetchActorFn: async () => ({ id: 'https://elders.example/users/nieuw', inbox: 'https://elders.example/inbox', alsoKnownAs: [ME_RADIO] }),
     160    deliverFn: async (slug, inbox, activity) => { delivered.push({ inbox, activity }); },
     161  });
     162  assert.equal(r.ok, true);
     163  assert.equal(r.target, 'https://elders.example/users/nieuw');
     164  assert.equal(db.prepare("SELECT moved_to FROM sites WHERE slug = 'radio'").get().moved_to, 'https://elders.example/users/nieuw');
     165  assert.equal(delivered.length, 2, 'both follower inboxes');
     166  for (const d of delivered) {
     167    assert.equal(d.activity.type, 'Move');
     168    assert.equal(d.activity.object, ME_RADIO);
     169    assert.equal(d.activity.target, 'https://elders.example/users/nieuw');
     170  }
     171  assert.ok(delivered.some((d) => d.inbox === 'https://b.example/shared'), 'shared inbox preferred');
     172});
Note: See TracChangeset for help on using the changeset viewer.