2026-08-28 11:12:41 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// 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);
|
2026-08-28 12:08:19 +02:00
|
|
|
toets(
|
|
|
|
|
'en houdt het moment waarop hij zich meldde, niet dat van de klik',
|
|
|
|
|
alsnog.state.owners[0].firstSeen === NU,
|
|
|
|
|
);
|
2026-08-28 11:12:41 +02:00
|
|
|
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);
|