UmbrelApps: community app store whatsnext
Electrum Gate, Evolu Relay en Atreides Assets, met de image-recepten in tools/ en de tests in tests/. Begonnen zonder voorgeschiedenis op 09-10-2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
85d85a1295
63 files changed
+13987
No files matched your search
@@ -0,0 +1,267 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 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 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.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
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-09-09T12:00:00.000Z';
|
||||
const LATER = '2026-09-09T13:00:00.000Z';
|
||||
const EIGEN = 'owner-van-de-gebruiker';
|
||||
const VREEMD = 'owner-van-een-ander';
|
||||
|
||||
/** 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 ─────────────────────────────────
|
||||
|
||||
{
|
||||
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));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ── De leerstand is er echt uit ───────────────────────────────────────────────
|
||||
|
||||
{
|
||||
// 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);
|
||||
|
||||
const metVenster = {
|
||||
...createEmptyState(),
|
||||
learning: true,
|
||||
learningUntil: LATER,
|
||||
};
|
||||
toets(
|
||||
'ook een venster dat nog niet verlopen is doet niets',
|
||||
decideOwner(metVenster, VREEMD, NU).allowed === false,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ── De guard die er het meest toe doet ────────────────────────────────────────
|
||||
|
||||
{
|
||||
// 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);
|
||||
|
||||
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);
|
||||
|
||||
const zelfdeMoment = decideOwner(metEigenaar(EIGEN, true), EIGEN, NU);
|
||||
toets('twee keer op hetzelfde moment verandert niets', zelfdeMoment.changed === false);
|
||||
}
|
||||
|
||||
// ── Onbruikbare invoer ────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
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');
|
||||
}
|
||||
toets(
|
||||
`een id van precies ${MAX_OWNER_ID_LENGTH} tekens mag wel`,
|
||||
decideOwner(leeg, 'x'.repeat(MAX_OWNER_ID_LENGTH), NU).changed === true,
|
||||
);
|
||||
}
|
||||
|
||||
// ── De wachtlijst loopt niet vol ──────────────────────────────────────────────
|
||||
|
||||
{
|
||||
let staat = createEmptyState();
|
||||
for (let i = 0; i < MAX_REJECTED + 10; i += 1) {
|
||||
staat = decideOwner(staat, `vreemde-${i}`, NU).state;
|
||||
}
|
||||
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}`);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Toelaten vanaf de wachtlijst ──────────────────────────────────────────────
|
||||
|
||||
{
|
||||
// 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);
|
||||
|
||||
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);
|
||||
|
||||
toets('en daarna mag hij erin', decideOwner(toegelaten.state, VREEMD, LATER).allowed === true);
|
||||
}
|
||||
|
||||
// ── De overige opdrachten van de pagina ───────────────────────────────────────
|
||||
|
||||
{
|
||||
const staat = metEigenaar(EIGEN, true);
|
||||
|
||||
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);
|
||||
|
||||
toets(
|
||||
'blokkeren van een onbekende wordt geweigerd',
|
||||
applyCommand(staat, { action: 'block', ownerId: VREEMD }, LATER).error === 'unknown-owner',
|
||||
);
|
||||
|
||||
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',
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
for (const onzin of ['', null, 42, 'x'.repeat(MAX_OWNER_ID_LENGTH + 1)]) {
|
||||
toets(
|
||||
`block met ownerId ${JSON.stringify(onzin)} wordt geweigerd`,
|
||||
applyCommand(staat, { action: 'block', ownerId: onzin }, NU).error === 'malformed-command',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wat er van schijf komt ────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
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);
|
||||
|
||||
// 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 }],
|
||||
};
|
||||
const gelezen = normalizeState(oud);
|
||||
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));
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`${goed} goed, ${fout} fout`);
|
||||
process.exit(fout === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user