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
|
2026-09-09 15:45:55 +02:00
|
|
|
// een guard, en dat is precies wat hieronder de meeste aandacht krijgt.
|
|
|
|
|
//
|
|
|
|
|
// **Sinds 0.7.0 is er één weg naar binnen: de wachtlijst.** De getimede leerstand
|
|
|
|
|
// is eruit. Twee toetsen hieronder bewaken dat die niet stiekem terugkomt: een
|
|
|
|
|
// onbekende eigenaar wordt geweigerd ook als er `learning: true` in het
|
|
|
|
|
// staatsbestand staat, en `set-learning` is een onbekende actie.
|
2026-08-28 11:12:41 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const NU = '2026-09-09T12:00:00.000Z';
|
|
|
|
|
const LATER = '2026-09-09T13:00:00.000Z';
|
2026-08-28 11:12:41 +02:00
|
|
|
const EIGEN = 'owner-van-de-gebruiker';
|
|
|
|
|
const VREEMD = 'owner-van-een-ander';
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
/** Een staat met deze eigenaar erin, toegelaten of niet. */
|
|
|
|
|
const metEigenaar = (id, allowed, extra = {}) => ({
|
|
|
|
|
...createEmptyState(),
|
|
|
|
|
owners: [{ id, allowed, firstSeen: NU, lastSeen: NU, ...extra }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Een verse installatie laat niemand binnen ─────────────────────────────────
|
2026-08-28 11:12:41 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
const leeg = createEmptyState();
|
|
|
|
|
toets('een verse staat heeft geen eigenaars', leeg.owners.length === 0);
|
|
|
|
|
toets('en een lege wachtlijst', leeg.rejected.length === 0);
|
|
|
|
|
toets('en geen leerstand meer', !('learning' in leeg) && !('learningUntil' in leeg));
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const uitkomst = decideOwner(leeg, VREEMD, NU);
|
|
|
|
|
toets('een onbekende eigenaar wordt geweigerd', uitkomst.allowed === false);
|
|
|
|
|
toets('met reden "waiting"', uitkomst.reason === 'waiting');
|
|
|
|
|
toets('en belandt op de wachtlijst', uitkomst.state.rejected.length === 1);
|
|
|
|
|
toets('met zijn id erbij', uitkomst.state.rejected[0].id === VREEMD);
|
|
|
|
|
toets('en één poging', uitkomst.state.rejected[0].attempts === 1);
|
|
|
|
|
toets('en de staat is veranderd', uitkomst.changed === true);
|
|
|
|
|
toets('en hij staat niet stiekem bij de eigenaars', uitkomst.state.owners.length === 0);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// ── De leerstand is er echt uit ───────────────────────────────────────────────
|
2026-08-28 11:12:41 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
// Dit is de toets die de verwijdering van 0.7.0 vasthoudt. Een staatsbestand
|
|
|
|
|
// van 0.6.0 heeft deze velden nog, en ze horen geen enkel effect te hebben.
|
|
|
|
|
const metOudeVelden = {
|
|
|
|
|
...createEmptyState(),
|
|
|
|
|
learning: true,
|
|
|
|
|
learningUntil: null,
|
|
|
|
|
};
|
|
|
|
|
const uitkomst = decideOwner(metOudeVelden, VREEMD, NU);
|
|
|
|
|
toets('`learning: true` in het bestand laat niemand meer binnen', uitkomst.allowed === false);
|
|
|
|
|
toets('en zet hem op de wachtlijst', uitkomst.state.rejected[0]?.id === VREEMD);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const metVenster = {
|
|
|
|
|
...createEmptyState(),
|
|
|
|
|
learning: true,
|
|
|
|
|
learningUntil: LATER,
|
|
|
|
|
};
|
|
|
|
|
toets(
|
|
|
|
|
'ook een venster dat nog niet verlopen is doet niets',
|
|
|
|
|
decideOwner(metVenster, VREEMD, NU).allowed === false,
|
|
|
|
|
);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const opdracht = applyCommand(createEmptyState(), { action: 'set-learning', value: true }, NU);
|
|
|
|
|
toets('set-learning is een onbekende actie geworden', opdracht.error === 'unknown-action');
|
|
|
|
|
toets('en verandert niets', opdracht.changed === false);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── De guard die er het meest toe doet ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
// Een geblokkeerde eigenaar mag er nooit in. Dat was tot 0.6.0 het gevaar dat
|
|
|
|
|
// de leerstand hem alsnog toeliet; die is er nu uit, maar de guard blijft de
|
|
|
|
|
// belangrijkste regel van dit bestand.
|
|
|
|
|
const geblokkeerd = metEigenaar(EIGEN, false);
|
|
|
|
|
const uitkomst = decideOwner(geblokkeerd, EIGEN, LATER);
|
|
|
|
|
toets('een geblokkeerde eigenaar blijft buiten', uitkomst.allowed === false);
|
|
|
|
|
toets('met reden "known-blocked"', uitkomst.reason === 'known-blocked');
|
|
|
|
|
toets('en komt niet op de wachtlijst', uitkomst.state.rejected.length === 0);
|
|
|
|
|
toets('hij blijft geblokkeerd staan', uitkomst.state.owners[0].allowed === false);
|
|
|
|
|
toets('en "laatst gezien" wordt bijgewerkt', uitkomst.state.owners[0].lastSeen === LATER);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const toegelaten = decideOwner(metEigenaar(EIGEN, true), EIGEN, LATER);
|
|
|
|
|
toets('een toegelaten eigenaar mag erin', toegelaten.allowed === true);
|
|
|
|
|
toets('met reden "known-allowed"', toegelaten.reason === 'known-allowed');
|
|
|
|
|
toets('en "laatst gezien" wordt bijgewerkt', toegelaten.state.owners[0].lastSeen === LATER);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const zelfdeMoment = decideOwner(metEigenaar(EIGEN, true), EIGEN, NU);
|
|
|
|
|
toets('twee keer op hetzelfde moment verandert niets', zelfdeMoment.changed === false);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Onbruikbare invoer ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
const leeg = createEmptyState();
|
|
|
|
|
for (const onzin of ['', null, 42, {}, [], 'x'.repeat(MAX_OWNER_ID_LENGTH + 1)]) {
|
|
|
|
|
const uitkomst = decideOwner(leeg, onzin, NU);
|
|
|
|
|
toets(`${JSON.stringify(onzin)} wordt geweigerd`, uitkomst.allowed === false);
|
|
|
|
|
// Onthouden zou hier juist de weg openzetten om het bestand vol te schrijven.
|
|
|
|
|
toets(`${JSON.stringify(onzin)} wordt niet onthouden`, uitkomst.changed === false);
|
|
|
|
|
toets(`${JSON.stringify(onzin)} geeft reden invalid-owner-id`, uitkomst.reason === 'invalid-owner-id');
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
2026-09-09 15:45:55 +02:00
|
|
|
toets(
|
|
|
|
|
`een id van precies ${MAX_OWNER_ID_LENGTH} tekens mag wel`,
|
|
|
|
|
decideOwner(leeg, 'x'.repeat(MAX_OWNER_ID_LENGTH), NU).changed === true,
|
|
|
|
|
);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// ── De wachtlijst loopt niet vol ──────────────────────────────────────────────
|
2026-08-28 11:12:41 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
let staat = createEmptyState();
|
|
|
|
|
for (let i = 0; i < MAX_REJECTED + 10; i += 1) {
|
|
|
|
|
staat = decideOwner(staat, `vreemde-${i}`, NU).state;
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
2026-09-09 15:45:55 +02:00
|
|
|
toets('de wachtlijst wordt afgekapt', staat.rejected.length === MAX_REJECTED);
|
|
|
|
|
// Nieuwste vooraan: een aanvaller die blijft proberen duwt zijn eigen eerdere
|
|
|
|
|
// pogingen weg en niet de eigenaar die er net bij kwam.
|
|
|
|
|
toets('de nieuwste staat vooraan', staat.rejected[0].id === `vreemde-${MAX_REJECTED + 9}`);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const nogmaals = decideOwner(staat, `vreemde-${MAX_REJECTED + 9}`, LATER);
|
|
|
|
|
toets('een tweede poging telt op', nogmaals.state.rejected[0].attempts === 2);
|
|
|
|
|
toets('en zet geen tweede regel neer', nogmaals.state.rejected.length === MAX_REJECTED);
|
|
|
|
|
toets('en houdt het eerste moment vast', nogmaals.state.rejected[0].firstSeen === NU);
|
|
|
|
|
toets('en werkt het laatste bij', nogmaals.state.rejected[0].lastSeen === LATER);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// ── Toelaten vanaf de wachtlijst ──────────────────────────────────────────────
|
2026-08-28 11:12:41 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
// De weg die er sinds 0.7.0 als enige is: iemand staat op de wachtlijst en jij
|
|
|
|
|
// laat hem toe.
|
|
|
|
|
const gewacht = decideOwner(createEmptyState(), VREEMD, NU).state;
|
|
|
|
|
const toegelaten = applyCommand(gewacht, { action: 'allow', ownerId: VREEMD }, LATER);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
toets('toelaten lukt', toegelaten.error === null);
|
|
|
|
|
toets('en hij staat bij de eigenaars', toegelaten.state.owners[0]?.id === VREEMD);
|
|
|
|
|
toets('als toegelaten', toegelaten.state.owners[0]?.allowed === true);
|
|
|
|
|
// De geschiedenis gaat mee. Ging dat mis, dan meldt de pagina "first seen" op
|
|
|
|
|
// het moment van de klik terwijl die eigenaar al twintig minuten aan de deur
|
|
|
|
|
// stond, en dat is juist bij een eigenaar die je alsnog toelaat wat je wilt zien.
|
|
|
|
|
toets('met het eerste moment van de wachtlijst', toegelaten.state.owners[0]?.firstSeen === NU);
|
|
|
|
|
// En dit is de klacht van de gebruiker van 09-09-2026, die met de leerstand mee
|
|
|
|
|
// verdween: hij mag niet in beide lijsten tegelijk staan.
|
|
|
|
|
toets('en hij is van de wachtlijst af', toegelaten.state.rejected.length === 0);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
toets('en daarna mag hij erin', decideOwner(toegelaten.state, VREEMD, LATER).allowed === true);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// ── De overige opdrachten van de pagina ───────────────────────────────────────
|
2026-08-30 12:26:56 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
const staat = metEigenaar(EIGEN, true);
|
2026-08-30 12:26:56 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const geblokkeerd = applyCommand(staat, { action: 'block', ownerId: EIGEN }, LATER);
|
|
|
|
|
toets('blokkeren lukt', geblokkeerd.error === null);
|
|
|
|
|
toets('en zet allowed op false', geblokkeerd.state.owners[0].allowed === false);
|
2026-08-30 12:26:56 +02:00
|
|
|
|
|
|
|
|
toets(
|
2026-09-09 15:45:55 +02:00
|
|
|
'blokkeren van een onbekende wordt geweigerd',
|
|
|
|
|
applyCommand(staat, { action: 'block', ownerId: VREEMD }, LATER).error === 'unknown-owner',
|
2026-08-30 12:26:56 +02:00
|
|
|
);
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const vergeten = applyCommand(staat, { action: 'forget', ownerId: EIGEN }, LATER);
|
|
|
|
|
toets('vergeten lukt', vergeten.error === null);
|
|
|
|
|
toets('en de eigenaar is weg', vergeten.state.owners.length === 0);
|
|
|
|
|
toets(
|
|
|
|
|
'vergeten van een onbekende wordt geweigerd',
|
|
|
|
|
applyCommand(staat, { action: 'forget', ownerId: VREEMD }, LATER).error === 'unknown-owner',
|
2026-08-30 12:26:56 +02:00
|
|
|
);
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
const gewacht = decideOwner(createEmptyState(), VREEMD, NU).state;
|
|
|
|
|
const weg = applyCommand(gewacht, { action: 'forget', ownerId: VREEMD }, LATER);
|
|
|
|
|
toets('iemand van de wachtlijst vergeten lukt ook', weg.error === null);
|
|
|
|
|
toets('en dan is de wachtlijst leeg', weg.state.rejected.length === 0);
|
2026-08-30 12:26:56 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
for (const onzin of [null, 42, 'tekst', {}, { action: 'iets' }]) {
|
|
|
|
|
const uitkomst = applyCommand(staat, onzin, NU);
|
|
|
|
|
toets(`${JSON.stringify(onzin)} als opdracht verandert niets`, uitkomst.changed === false);
|
|
|
|
|
toets(`${JSON.stringify(onzin)} als opdracht geeft een fout`, uitkomst.error !== null);
|
|
|
|
|
}
|
2026-08-30 12:26:56 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
for (const onzin of ['', null, 42, 'x'.repeat(MAX_OWNER_ID_LENGTH + 1)]) {
|
2026-08-30 12:26:56 +02:00
|
|
|
toets(
|
2026-09-09 15:45:55 +02:00
|
|
|
`block met ownerId ${JSON.stringify(onzin)} wordt geweigerd`,
|
|
|
|
|
applyCommand(staat, { action: 'block', ownerId: onzin }, NU).error === 'malformed-command',
|
2026-08-30 12:26:56 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// ── Wat er van schijf komt ────────────────────────────────────────────────────
|
2026-08-28 11:12:41 +02:00
|
|
|
|
|
|
|
|
{
|
2026-09-09 15:45:55 +02:00
|
|
|
toets('null is geen staat', normalizeState(null) === null);
|
|
|
|
|
toets('een array is geen staat', normalizeState([]) === null);
|
|
|
|
|
toets('een verkeerde versie is geen staat', normalizeState({ version: 2, owners: [], rejected: [] }) === null);
|
|
|
|
|
toets('zonder owners is het geen staat', normalizeState({ version: 1, rejected: [] }) === null);
|
|
|
|
|
toets('zonder rejected is het geen staat', normalizeState({ version: 1, owners: [] }) === null);
|
2026-08-28 11:12:41 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
// Dit is de toets die de installatie van de gebruiker beschermt: een bestand
|
|
|
|
|
// van 0.6.0 heeft `learning` en `learningUntil` en moet gewoon lezen. Zou dat
|
|
|
|
|
// afgewezen worden, dan schuift store.js de allowlist opzij en gaat de deur
|
|
|
|
|
// dicht voor eigenaars die er al in stonden.
|
|
|
|
|
const oud = {
|
|
|
|
|
version: 1,
|
|
|
|
|
learning: true,
|
|
|
|
|
learningUntil: '2026-09-09T12:02:00.000Z',
|
|
|
|
|
owners: [{ id: EIGEN, allowed: true, firstSeen: NU, lastSeen: NU }],
|
|
|
|
|
rejected: [{ id: VREEMD, firstSeen: NU, lastSeen: NU, attempts: 3 }],
|
|
|
|
|
};
|
2026-08-30 12:26:56 +02:00
|
|
|
const gelezen = normalizeState(oud);
|
2026-09-09 15:45:55 +02:00
|
|
|
toets('een bestand van 0.6.0 leest gewoon', gelezen !== null);
|
|
|
|
|
toets('met de eigenaar erin', gelezen.owners[0].id === EIGEN);
|
|
|
|
|
toets('en de wachtlijst erin', gelezen.rejected[0].attempts === 3);
|
|
|
|
|
toets('en zonder de oude velden', !('learning' in gelezen) && !('learningUntil' in gelezen));
|
2026-08-30 12:26:56 +02:00
|
|
|
|
2026-09-09 15:45:55 +02:00
|
|
|
toets(
|
|
|
|
|
'een eigenaar zonder allowed is geen staat',
|
|
|
|
|
normalizeState({ version: 1, owners: [{ id: EIGEN }], rejected: [] }) === null,
|
|
|
|
|
);
|
|
|
|
|
toets(
|
|
|
|
|
'een eigenaar met een leeg id is geen staat',
|
|
|
|
|
normalizeState({ version: 1, owners: [{ id: '', allowed: true }], rejected: [] }) === null,
|
|
|
|
|
);
|
|
|
|
|
toets(
|
|
|
|
|
'een wachtende zonder id is geen staat',
|
|
|
|
|
normalizeState({ version: 1, owners: [], rejected: [{ attempts: 1 }] }) === null,
|
|
|
|
|
);
|
|
|
|
|
toets(
|
|
|
|
|
'een onleesbaar aantal pogingen wordt 1 en geen weigering',
|
|
|
|
|
normalizeState({ version: 1, owners: [], rejected: [{ id: VREEMD, attempts: -3 }] })
|
|
|
|
|
?.rejected[0].attempts === 1,
|
|
|
|
|
);
|
2026-08-28 11:12:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log(`${goed} goed, ${fout} fout`);
|
|
|
|
|
process.exit(fout === 0 ? 0 : 1);
|