244 lines
11 KiB
JavaScript
244 lines
11 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
// 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);
|