// ═══════════════════════════════════════════════════════════════════════════════ // Toetst het register, de opdrachtregel en de instellingen van de testclient. // // Draaien: node tests/test_client_register.mjs // // Géén afhankelijkheden nodig, anders dan test_client_lokaal.mjs: register.js, // argumenten.js en config.js raken Evolu niet aan. Dat is met opzet zo gebouwd // en het is dezelfde scheiding die policy.js aan de relay-kant heeft: alles wat // te toetsen valt zonder netwerk, staat los van alles wat dat niet is. // // Waar dit op let: het register bevat mnemonics. De duurste fout zit hier niet // in een berekening maar in stil overschrijven of stil vervangen, en dat is dus // wat hieronder het meeste aandacht krijgt. // ═══════════════════════════════════════════════════════════════════════════════ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { isBruikbareNaam, normaliseer, openRegister } from '../tools/relay-client/src/register.js'; import { leesArgumenten, OPDRACHTEN } from '../tools/relay-client/src/argumenten.js'; import { leesConfig, leesEnvBestand, naarRelayUrl } from '../tools/relay-client/src/config.js'; let goed = 0; let fout = 0; const toets = (omschrijving, voorwaarde) => { if (voorwaarde) { goed += 1; return; } fout += 1; console.log(`FOUT: ${omschrijving}`); }; const gooit = (aan) => { try { aan(); return false; } catch { return true; } }; const verseMap = () => mkdtempSync(join(tmpdir(), 'relay-client-register-')); const mappen = []; const map = () => { const nieuw = verseMap(); mappen.push(nieuw); return nieuw; }; // ── Namen ───────────────────────────────────────────────────────────────────── { for (const naam of ['a', 'proef-1', 'trezor_mac', 'A'.repeat(40)]) { toets(`"${naam}" mag als naam`, isBruikbareNaam(naam)); } for (const naam of ['', ' ', 'met spatie', '../ontsnapt', 'pad/naam', 'A'.repeat(41), null, 42]) { toets(`${JSON.stringify(naam)} mag niet als naam`, !isBruikbareNaam(naam)); } } // ── Aanmaken, teruglezen, weggooien ─────────────────────────────────────────── { const waar = map(); const register = openRegister(waar); toets('een vers register is leeg', register.lijst().length === 0); toets('en een onbekende naam geeft null', register.zoek('piet') === null); const toegevoegd = register.voegToe({ naam: 'piet', mnemonic: 'woord ' .repeat(11) + 'woord', ownerId: 'AAA' }); toets('toevoegen geeft de regel terug', toegevoegd.naam === 'piet' && toegevoegd.ownerId === 'AAA'); toets('met een tijdstip erbij', typeof toegevoegd.aangemaakt === 'string'); toets('en hij staat in de lijst', register.lijst().length === 1); toets('en is op naam te vinden', register.zoek('piet')?.ownerId === 'AAA'); // Het niet-gelukkige pad, en het belangrijkste van dit bestand: overschrijven // zou de mnemonic van de vorige eigenaar weggooien. toets( 'dezelfde naam nog eens toevoegen wordt geweigerd', gooit(() => register.voegToe({ naam: 'piet', mnemonic: 'iets anders', ownerId: 'BBB' })), ); toets('en de eerste staat er nog', register.zoek('piet')?.ownerId === 'AAA'); toets( 'een onbruikbare naam wordt geweigerd', gooit(() => register.voegToe({ naam: 'met spatie', mnemonic: 'x', ownerId: 'CCC' })), ); toets('vergeten van een onbekende naam wordt geweigerd', gooit(() => register.vergeet('jan'))); const weg = register.vergeet('piet'); toets('vergeten geeft de regel terug', weg.ownerId === 'AAA'); toets('en daarna is het register leeg', register.lijst().length === 0); } // ── Wat er op schijf komt, komt er weer af ──────────────────────────────────── { const waar = map(); const eerste = openRegister(waar); eerste.voegToe({ naam: 'een', mnemonic: 'mnemonic een', ownerId: 'ID1' }); eerste.voegToe({ naam: 'twee', mnemonic: 'mnemonic twee', ownerId: 'ID2' }); const tweede = openRegister(waar); toets('een tweede register leest hetzelfde bestand', tweede.lijst().length === 2); toets('in dezelfde volgorde', tweede.lijst()[0].naam === 'een' && tweede.lijst()[1].naam === 'twee'); toets('met de mnemonic erbij', tweede.zoek('twee')?.mnemonic === 'mnemonic twee'); // Een register dat je aanpast mag geen lijst teruggeven die meeverandert: // dan zou een aanroeper per ongeluk in de staat kunnen schrijven. const kopie = tweede.lijst(); kopie[0].ownerId = 'GEHACKT'; toets('de lijst is een kopie', tweede.zoek('een')?.ownerId === 'ID1'); } // ── Een onleesbaar bestand wordt niet stil vervangen ────────────────────────── { const waar = map(); const register = openRegister(waar); register.voegToe({ naam: 'een', mnemonic: 'geheim', ownerId: 'ID1' }); const pad = join(waar, 'owners.json'); const origineel = readFileSync(pad, 'utf8'); writeFileSync(pad, '{ dit is geen json', 'utf8'); toets('onleesbare JSON wordt gemeld', gooit(() => openRegister(waar))); toets('en het bestand blijft staan', readFileSync(pad, 'utf8') === '{ dit is geen json'); writeFileSync(pad, JSON.stringify({ version: 2, owners: [] }), 'utf8'); toets('een onbekende versie wordt gemeld', gooit(() => openRegister(waar))); writeFileSync(pad, JSON.stringify({ version: 1, owners: [{ naam: 'een' }] }), 'utf8'); toets('een halve regel wordt gemeld', gooit(() => openRegister(waar))); writeFileSync(pad, origineel, 'utf8'); toets('en het originele bestand leest weer goed', openRegister(waar).zoek('een')?.ownerId === 'ID1'); } // ── normaliseer, los ────────────────────────────────────────────────────────── { toets('null is geen staat', normaliseer(null) === null); toets('een array is geen staat', normaliseer([]) === null); toets('zonder owners is het geen staat', normaliseer({ version: 1 }) === null); toets( 'twee keer dezelfde naam is geen staat', normaliseer({ version: 1, owners: [ { naam: 'a', mnemonic: 'x', ownerId: '1' }, { naam: 'a', mnemonic: 'y', ownerId: '2' }, ], }) === null, ); toets( 'een lege mnemonic is geen staat', normaliseer({ version: 1, owners: [{ naam: 'a', mnemonic: '', ownerId: '1' }] }) === null, ); toets( 'een geldige staat komt er heel uit', normaliseer({ version: 1, owners: [{ naam: 'a', mnemonic: 'x', ownerId: '1' }] })?.owners[0] ?.naam === 'a', ); } // ── De opdrachtregel ────────────────────────────────────────────────────────── { toets('niets geeft geen opdracht en geen fout', (() => { const uit = leesArgumenten([]); return uit.fout === null && uit.opdracht === null; })()); toets('een onbekende opdracht is een fout', leesArgumenten(['bestaatniet']).fout !== null); const nieuw = leesArgumenten(['nieuw', 'piet']); toets('nieuw piet leest goed', nieuw.fout === null && nieuw.waarden.naam === 'piet'); toets('nieuw zonder naam is een fout', leesArgumenten(['nieuw']).fout !== null); // Streng, en dat is de bedoeling: `schrijf piet mijn label` zou anders stil // "mijn" als label nemen en je naar een blob laten kijken die anders heet. toets('een argument te veel is een fout', leesArgumenten(['nieuw', 'piet', 'jan']).fout !== null); const schrijf = leesArgumenten(['schrijf', 'piet', 'mijn label', '--bytes', '512']); toets('schrijf leest beide argumenten', schrijf.waarden.naam === 'piet' && schrijf.waarden.label === 'mijn label'); toets('en de vlag ernaast', schrijf.vlaggen.bytes === '512'); toets('--bytes=512 leest ook', leesArgumenten(['lijst', '--bytes=512']).vlaggen.bytes === '512'); toets('een losse vlag is true', leesArgumenten(['lijst', '--lokaal']).vlaggen.lokaal === true); toets( 'een losse vlag gevolgd door een vlag blijft true', leesArgumenten(['lijst', '--lokaal', '--bytes=8']).vlaggen.lokaal === true, ); toets('elke opdracht heeft uitleg', Object.values(OPDRACHTEN).every((v) => typeof v.uitleg === 'string' && v.uitleg.length > 0)); } // ── De instellingen ─────────────────────────────────────────────────────────── { const gelezen = leesEnvBestand( ['# commentaar', '', 'RELAY_URL=ws://voorbeeld:3852', 'MET_QUOTES="waarde"', 'ROMMEL', 'LEEG='].join('\n'), ); toets('een gewone regel leest', gelezen.RELAY_URL === 'ws://voorbeeld:3852'); toets('aanhalingstekens gaan eraf', gelezen.MET_QUOTES === 'waarde'); toets('een regel zonder = wordt overgeslagen', gelezen.ROMMEL === undefined); toets('een lege waarde is een lege string', gelezen.LEEG === ''); toets('commentaar telt niet mee', Object.keys(gelezen).length === 3); toets('ws:// blijft', naarRelayUrl('ws://umbrel.local:3852') === 'ws://umbrel.local:3852'); toets('wss:// blijft', naarRelayUrl('wss://voorbeeld.nl') === 'wss://voorbeeld.nl'); toets('http:// wordt ws://', naarRelayUrl('http://umbrel.local:3852') === 'ws://umbrel.local:3852'); toets('https:// wordt wss://', naarRelayUrl('https://voorbeeld.nl') === 'wss://voorbeeld.nl'); toets('zonder schema wordt ws://', naarRelayUrl('umbrel.local:3852') === 'ws://umbrel.local:3852'); toets('spaties eromheen doen niets', naarRelayUrl(' umbrel.local:3852 ') === 'ws://umbrel.local:3852'); toets('een leeg adres wordt geweigerd', gooit(() => naarRelayUrl(''))); toets('een onbekend schema wordt geweigerd', gooit(() => naarRelayUrl('ftp://umbrel.local'))); // Een map zonder .env: dan is er geen relay-adres, en dat is geen fout maar een // toestand die de UI moet kunnen tonen. const zonder = leesConfig({ env: {}, wortel: map() }); toets('zonder .env is er geen relay-adres', zonder.relayUrl === null); toets('en staat de UI-poort op de standaard', zonder.uiPoort === 4380); const wortel = map(); writeFileSync(join(wortel, '.env'), 'RELAY_URL=umbrel.local:3852\nUI_PORT=5000\n', 'utf8'); const met = leesConfig({ env: {}, wortel }); toets('een .env wordt gelezen', met.relayUrl === 'ws://umbrel.local:3852'); toets('en de poort ook', met.uiPoort === 5000); const overstemd = leesConfig({ env: { RELAY_URL: 'ws://anders:1234' }, wortel }); toets('de omgeving wint van het bestand', overstemd.relayUrl === 'ws://anders:1234'); } for (const weg of mappen) rmSync(weg, { recursive: true, force: true }); console.log(''); console.log(`${goed} goed, ${fout} fout`); process.exit(fout === 0 ? 0 : 1);