Files
UmbrelApps/tests/test_client_register.mjs
T

244 lines
11 KiB
JavaScript
Raw Normal View History

// ═══════════════════════════════════════════════════════════════════════════════
// 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);