Changeset 952baf3 in Klonkt for test


Ignore:
Timestamp:
08/07/2026 05:15:52 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ba76bf5
Parents:
f85b2c3 (diff), 0d5bd2c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge GitHub-main (1.7.0) met de VPS-lijn

De twee mains waren een dag gedivergeerd en bevatten elk echt werk. GitHub had 66
commits die nooit langs prutfolio.git zijn gekomen, omdat een parallelle sessie
rechtstreeks naar GitHub pushte vanaf een kloon in /tmp op de VPS. De VPS had twee
commits die GitHub niet had. Geen van beide bevatte de ander, en stable had geen van
de twee.

Bewust een merge en geen rebase: dan blijft beide historie intact en wordt er niets
herschreven waar iemand anders al op voortbouwt.

Drie bestanden raakten beide kanten. Alle drie zijn nagekeken, want dat een merge
automatisch slaagt zegt niets over of hij inhoudelijk klopt:

src/services/ActivityPubService.js

  • de sleutelbinding staat nu boven de nieuwe asSlug-aanroep van 1.7.0, dus de controle komt nog steeds voor de handtekeningcontrole

scripts/klonkt-refresh-updater.sh

  • alleen de opzij-aanpak overleefde; systemctl mask staat nergens meer als code

deploy/MULTI-INSTANCE.md

  • spreekt zichzelf niet tegen: beschrijft opzij zetten, met de reden waarom mask weigert

remarks: het gat dat in de review naar boven kwam staat hiermee ook op de 1.7.0-lijn.
De andere bevindingen uit die review staan nog open en zijn niet in deze merge
opgelost; die horen als beads. Ook nog te doen: dezelfde sleutelbinding op stable
als 1.6.1, want daar is het gat nog open bij self-hosters.

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

Location:
test
Files:
14 added
3 edited

Legend:

Unmodified
Added
Removed
  • test/authorized-fetch.test.js

    rf85b2c3 r952baf3  
    1 // FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch.
     1// Authorized fetch (shaer-afq): een instance in Mastodons secure mode geeft zijn
     2// actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND
     3// verzoek. Zonder die handtekening kregen we 401, vonden we geen sleutel, en
     4// wezen we elke correct ondertekende Follow van die instance af. Op boiert.eu
     5// bleven daardoor vier accounts eindeloos hangen.
     6//
     7// De fetch is hier gestubd: het gaat om de vraag OF er ondertekend wordt en of
     8// er wordt teruggevallen, niet om echte HTTP of crypto.
     9//
     10// Run: npm test
     11
    212import { test } from 'node:test';
    313import assert from 'node:assert/strict';
    414
    515process.env.DATABASE_PATH = ':memory:';
    6 process.env.PUBLIC_BASE_URL = 'https://test.example';
     16process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
    717
    818const dbMod = await import('../src/config/database.js');
    919const db = dbMod.default;
    1020dbMod.initializeDatabase();
    11 const AP = (await import('../src/services/ActivityPubService.js')).default;
     21const AP = await import('../src/services/ActivityPubService.js');
    1222
    13 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
    14 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
    15 // Commit a guardian relation: kid (ward) is guarded by mom.
    16 const MOM = 'https://mom.example/ap/users/mom';
    17 db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM);
     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', 'me', 'Me', 'u1');
    1826
    19 test('a committed guardian is recognised; a stranger is not', () => {
    20   assert.equal(AP.isWardGuardian('kid', MOM), true);
    21   assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false);
    22   assert.equal(AP.isWardGuardian('nosuch', MOM), false);
     27// IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline.
     28const OPEN_ACTOR = 'https://203.0.113.30/users/open';       // gewone instance
     29const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch
     30
     31let verzoeken = [];
     32const echteFetch = globalThis.fetch;
     33globalThis.fetch = async (url, opts = {}) => {
     34  const u = String(url);
     35  const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
     36  verzoeken.push({ url: u, ondertekend });
     37  const doc = (id) => new Response(JSON.stringify({
     38    id, type: 'Person', preferredUsername: 'x', inbox: `${id}/inbox`,
     39    publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: '-----BEGIN PUBLIC KEY-----\nx\n-----END PUBLIC KEY-----' },
     40  }), { status: 200, headers: { 'content-type': 'application/activity+json' } });
     41
     42  if (u === OPEN_ACTOR) return doc(OPEN_ACTOR);
     43  // De kern van secure mode: onbetekend is het 401, ondertekend krijg je hem wel.
     44  if (u === SECURE_ACTOR) return ondertekend ? doc(SECURE_ACTOR) : new Response('unauthorized', { status: 401 });
     45  return new Response('not found', { status: 404 });
     46};
     47
     48test('een actor achter authorized fetch wordt nu wél opgehaald', async () => {
     49  verzoeken = [];
     50  const actor = await AP.fetchActor(SECURE_ACTOR, { asSlug: 'me' });
     51  assert.ok(actor, 'de actor hoort binnen te komen');
     52  assert.equal(actor.id, SECURE_ACTOR);
     53  assert.ok(verzoeken.some((v) => v.url === SECURE_ACTOR && v.ondertekend), 'het verzoek hoort ondertekend te zijn');
    2354});
     55
     56test('zonder ondertekenaar blijft dezelfde actor onbereikbaar', async () => {
     57  // Dit is precies het oude gedrag, en het bewijst dat de stub echt onderscheid
     58  // maakt in plaats van altijd mee te werken.
     59  const actor = await AP.fetchActor(SECURE_ACTOR);
     60  assert.equal(actor, null);
     61});
     62
     63test('een gewone instance blijft werken, ondertekend of niet', async () => {
     64  assert.ok(await AP.fetchActor(OPEN_ACTOR), 'onbetekend');
     65  assert.ok(await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }), 'ondertekend');
     66});
     67
     68test('een OPEN instance wordt NIET ondertekend opgehaald', () => {
     69  // De veiligheidskant van shaer-afq: verifyRequest haalt de keyId-URL op
     70  // voordat er iets geverifieerd is, en die URL komt uit een header die iedereen
     71  // mag sturen. Tekenden we standaard, dan kan een vreemde ons een ondertekend
     72  // verzoek naar een adres van zijn keuze laten sturen, met onze identiteit
     73  // eronder. Onbetekend eerst dus, en alleen tekenen als het anders niet lukt.
     74  verzoeken = [];
     75  return AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }).then((actor) => {
     76    assert.ok(actor);
     77    assert.equal(verzoeken.length, 1, 'één poging, geen tweede');
     78    assert.equal(verzoeken[0].ondertekend, false, 'en die was onbetekend');
     79  });
     80});
     81
     82test('een document zonder sleutel telt als mislukt en leidt tot een ondertekende poging', async () => {
     83  // Sommige instances serveren onbetekend wel iets, maar zonder publicKey. Voor
     84  // een verificatie hebben we daar niets aan.
     85  const KAAL = 'https://203.0.113.50/users/kaal';
     86  const stubOrig = globalThis.fetch;
     87  globalThis.fetch = async (url, opts = {}) => {
     88    const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
     89    if (String(url) === KAAL) {
     90      verzoeken.push({ url: String(url), ondertekend });
     91      const body = ondertekend
     92        ? { id: KAAL, type: 'Person', publicKey: { id: `${KAAL}#k`, owner: KAAL, publicKeyPem: 'x' } }
     93        : { id: KAAL, type: 'Person' };   // kaal: geen sleutel
     94      return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/activity+json' } });
     95    }
     96    return stubOrig(url, opts);
     97  };
     98  verzoeken = [];
     99  const actor = await AP.fetchActor(KAAL, { asSlug: 'me' });
     100  globalThis.fetch = stubOrig;
     101  assert.ok(actor.publicKey && actor.publicKey.publicKeyPem, 'de ondertekende poging levert de sleutel');
     102  assert.deepEqual(verzoeken.map((v) => v.ondertekend), [false, true], 'eerst onbetekend, daarna pas ondertekend');
     103});
     104
     105test('een onbekende actor blijft null, ondertekend of niet', async () => {
     106  assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null);
     107});
     108
     109test.after(() => { globalThis.fetch = echteFetch; });
  • test/co-location.test.js

    rf85b2c3 r952baf3  
    227227    // of that job being done.
    228228    wardSlugsOf: 'TODO: gated follows still take the shared-database path for a local guardian',
     229    // Tweede plek van diezelfde sluiproute, aangekomen met Fase 2 (shaer-jdb):
     230    // followsCollection vult de wachtrij voor een ward op DEZE instance uit
     231    // ap_pending_follows. Hoort mee te verdwijnen met wardSlugsOf hierboven --
     232    // niet apart, want het is een sluiproute en geen tweede probleem.
     233    slugOf: 'TODO: idem, nu ook in de follows-wachtrij (shaer-h6u)',
    229234  };
    230   const files = ['src/services/guardianship/handshake.js', 'src/routes/guardian.js'];
     235  // De hele module, niet twee bestanden. Het commentaar hierboven zegt "elke
     236  // plek die vraagt of deze actor van ons is", en dat werd tot nu toe afgemeten
     237  // aan twee namen -- waardoor een nieuwe sluiproute in een DERDE bestand er
     238  // geruisloos doorheen kwam. Dat is precies hoe deze er kwam.
     239  const files = [
     240    ...fs.readdirSync('src/services/guardianship').filter((f) => f.endsWith('.js'))
     241      .map((f) => `src/services/guardianship/${f}`),
     242    'src/routes/guardian.js',
     243  ];
    231244  // Only top-level declarations name a scope; an indented `const base = ...` is
    232245  // a local and would otherwise take the blame for its enclosing function. A
  • test/messages.test.js

    rf85b2c3 r952baf3  
    22// in één stroom, groepeert opeenvolgende likes/boosts op dezelfde post, en geeft
    33// visibility door (voor de privé-badge). Besluit Robin+Bart 2026-07-16.
     4//
     5// Sinds Berichten gesprekken toont vouwen antwoorden, mentions en je eigen
     6// verzonden berichten samen tot draden (zie messages-conversations.test.js voor
     7// de groepeerlogica zelf); likes, boosts en follows blijven losse regels.
    48//
    59// Run: npm test   (= node --test)
     
    4246const msgs = AP.getMessages('me', 50);
    4347
    44 test('eigen outbound reply zit als "sent" in de stroom (nieuwste eerst)', () => {
    45   const sent = msgs.find((m) => m.type === 'sent');
     48test('eigen outbound reply en het ontvangen antwoord staan in EEN draad', () => {
     49  const thread = msgs.find((m) => m.type === 'thread');
     50  assert.ok(thread, 'draad ontbreekt');
     51  assert.equal(msgs[0], thread, 'de draad met het nieuwste bericht hoort bovenaan');
     52  const sent = thread.messages.find((m) => m.type === 'sent');
    4653  assert.ok(sent, 'sent-item ontbreekt');
    4754  assert.equal(sent.outboxId, 'out1');
    4855  assert.equal(sent.to_handle, '@dana@r.test');
    49   assert.equal(msgs[0].type, 'sent', 'nieuwste item hoort bovenaan');
     56  // Hier gaat het om: Dana's antwoord en het jouwe zaten in aparte chips
     57  // (Gesprekken en Verzonden) en staan nu op volgorde in dezelfde draad.
     58  assert.deepEqual(thread.messages.map((m) => m.type), ['reply', 'sent']);
     59  assert.equal(thread.post.slug, 'mijn-post');
    5060});
    5161
     
    5868
    5969test('privé-reply draagt visibility voor de badge en heeft post-context', () => {
    60   const reply = msgs.find((m) => m.type === 'reply');
     70  const thread = msgs.find((m) => m.type === 'thread');
     71  const reply = thread.messages.find((m) => m.type === 'reply');
    6172  assert.equal(reply.visibility, 'direct');
    6273  assert.equal(reply.post_slug, 'mijn-post');
    6374  assert.equal(reply.post_title, 'Mijn post');
     75  // De context hangt ook aan de draad zelf: die voedt de link in de kop.
     76  assert.deepEqual(thread.post, { slug: 'mijn-post', title: 'Mijn post' });
    6477});
    6578
Note: See TracChangeset for help on using the changeset viewer.