// ═══════════════════════════════════════════════════════════════════════════════ // Toetst het toegangsbeleid van de relay: tools/evolu-relay/src/policy.js // // Draaien: node tests/test_limiter.mjs // // Geen testrunner en geen afhankelijkheden, net als de Python-tests in deze map. // Geeft 0 terug als alles goed is en 1 bij de eerste fout in de telling. // // Waarom dit bestand er is: dit beleid is de enige plek die bepaalt wie er op de // relay mag schrijven. De duurste fout zit hier niet in een berekening maar in // een guard, en dat is precies wat hieronder getoetst wordt: een geblokkeerde // eigenaar mag niet alsnog binnenkomen doordat de leerstand aanstaat. // ═══════════════════════════════════════════════════════════════════════════════ import { MAX_OWNER_ID_LENGTH, MAX_REJECTED, applyCommand, createEmptyState, decideOwner, normalizeState, } from '../tools/evolu-relay/src/policy.js'; let goed = 0; let fout = 0; const toets = (omschrijving, voorwaarde) => { if (voorwaarde) { goed += 1; return; } fout += 1; console.log(`FOUT: ${omschrijving}`); }; const NU = '2026-08-28T12:00:00.000Z'; const LATER = '2026-08-28T13:00:00.000Z'; const EIGEN = 'owner-van-de-gebruiker'; const VREEMD = 'owner-van-een-ander'; // ── De leerstand ────────────────────────────────────────────────────────────── { const vers = createEmptyState(); toets('een verse staat staat in de leerstand', vers.learning === true); const eerste = decideOwner(vers, EIGEN, NU); toets('de eerste eigenaar wordt toegelaten', eerste.allowed === true); toets('en onthouden als toegelaten', eerste.state.owners.length === 1); toets('met de reden dat hij geleerd is', eerste.reason === 'learned'); const nogEens = decideOwner(eerste.state, EIGEN, LATER); toets('dezelfde eigenaar blijft toegelaten', nogEens.allowed === true); toets('en komt er niet dubbel in', nogEens.state.owners.length === 1); toets('de reden is nu dat hij bekend is', nogEens.reason === 'known-allowed'); } // ── De leerstand uit ────────────────────────────────────────────────────────── { const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state; const dicht = applyCommand(geleerd, { action: 'set-learning', value: false }, NU).state; const eigen = decideOwner(dicht, EIGEN, LATER); toets('met de leerstand uit mag de bekende eigenaar er nog steeds in', eigen.allowed === true); const vreemd = decideOwner(dicht, VREEMD, LATER); toets('maar een onbekende wordt geweigerd', vreemd.allowed === false); toets('de weigering wordt onthouden voor de pagina', vreemd.state.rejected.length === 1); toets('en hij belandt niet in de allowlist', vreemd.state.owners.length === 1); } // ── De guard die er het meest toe doet ──────────────────────────────────────── { const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state; const geblokkeerd = applyCommand(geleerd, { action: 'block', ownerId: EIGEN }, NU).state; toets('de leerstand staat hier nog aan', geblokkeerd.learning === true); const opnieuw = decideOwner(geblokkeerd, EIGEN, LATER); toets( 'een geblokkeerde eigenaar komt er niet alsnog in via de leerstand', opnieuw.allowed === false, ); toets('en de reden zegt dat hij geblokkeerd is', opnieuw.reason === 'known-blocked'); } // ── Onbruikbare invoer ──────────────────────────────────────────────────────── { const vers = createEmptyState(); for (const rommel of ['', null, undefined, 42, {}, 'x'.repeat(MAX_OWNER_ID_LENGTH + 1)]) { const uitkomst = decideOwner(vers, rommel, NU); toets(`een onbruikbaar id (${typeof rommel}) wordt geweigerd`, uitkomst.allowed === false); toets('en wordt niet onthouden', uitkomst.state.owners.length === 0); toets('ook niet als geweigerde poging', uitkomst.state.rejected.length === 0); } } // ── De lijst met weigeringen loopt niet vol ─────────────────────────────────── { let staat = { ...createEmptyState(), learning: false }; for (let i = 0; i < MAX_REJECTED + 5; i += 1) { staat = decideOwner(staat, `indringer-${i}`, NU).state; } toets('de lijst met weigeringen wordt afgekapt', staat.rejected.length === MAX_REJECTED); toets('en de nieuwste staat vooraan', staat.rejected[0].id === `indringer-${MAX_REJECTED + 4}`); const herhaling = decideOwner(staat, `indringer-${MAX_REJECTED + 4}`, LATER); toets('een herhaalde poging telt op', herhaling.state.rejected[0].attempts === 2); toets('en voegt geen tweede regel toe', herhaling.state.rejected.length === MAX_REJECTED); } // ── De opdrachten van de pagina ─────────────────────────────────────────────── { const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state; const uit = applyCommand(geleerd, { action: 'set-learning', value: false }, NU); toets('de leerstand gaat uit', uit.state.learning === false); toets('en dat is een wijziging', uit.changed === true); const nogEensUit = applyCommand(uit.state, { action: 'set-learning', value: false }, NU); toets('nog een keer uitzetten verandert niets', nogEensUit.changed === false); toets('en is geen fout', nogEensUit.error === null); const vergeten = applyCommand(geleerd, { action: 'forget', ownerId: EIGEN }, NU); toets('vergeten haalt de eigenaar uit de lijst', vergeten.state.owners.length === 0); const onbekend = applyCommand(geleerd, { action: 'forget', ownerId: VREEMD }, NU); toets('een onbekende vergeten is een fout', onbekend.error === 'unknown-owner'); toets('en verandert niets', onbekend.changed === false); const blokkeerOnbekend = applyCommand(geleerd, { action: 'block', ownerId: VREEMD }, NU); toets('een onbekende blokkeren is een fout', blokkeerOnbekend.error === 'unknown-owner'); const dicht = { ...createEmptyState(), learning: false }; const geweigerd = decideOwner(dicht, VREEMD, NU).state; const alsnog = applyCommand(geweigerd, { action: 'allow', ownerId: VREEMD }, LATER); toets('een geweigerde poging kan alsnog toegelaten worden', alsnog.state.owners.length === 1); toets('en verdwijnt dan uit de weigeringen', alsnog.state.rejected.length === 0); toets( 'en houdt het moment waarop hij zich meldde, niet dat van de klik', alsnog.state.owners[0].firstSeen === NU, ); toets( 'en mag er daarna in, ook met de leerstand uit', decideOwner(alsnog.state, VREEMD, LATER).allowed === true, ); toets( 'een onbekende actie is een fout', applyCommand(geleerd, { action: 'sudo' }, NU).error === 'unknown-action', ); toets( 'een opdracht zonder vorm is een fout', applyCommand(geleerd, 'zet alles open', NU).error === 'malformed-command', ); toets( 'een leerstand-opdracht zonder boolean is een fout', applyCommand(geleerd, { action: 'set-learning', value: 'ja' }, NU).error === 'malformed-command', ); } // ── Lezen van schijf ────────────────────────────────────────────────────────── { const goedeStaat = decideOwner(createEmptyState(), EIGEN, NU).state; const rondgang = normalizeState(JSON.parse(JSON.stringify(goedeStaat))); toets('een eigen staat overleeft de rondgang door JSON', rondgang !== null); toets('met de eigenaar er nog in', rondgang.owners[0].id === EIGEN); const rommel = [ null, 'tekst', [], { version: 999, learning: true, owners: [], rejected: [] }, { version: 1, learning: 'ja', owners: [], rejected: [] }, { version: 1, learning: true, owners: {}, rejected: [] }, { version: 1, learning: true, owners: [{ id: '', allowed: true }], rejected: [] }, { version: 1, learning: true, owners: [{ id: EIGEN }], rejected: [] }, ]; for (const invoer of rommel) { toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null); } } console.log(''); console.log(`${goed} goed, ${fout} fout`); process.exit(fout === 0 ? 0 : 1);