Files

256 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:7b65131858517607493b339c40ff58b8b81242aed88852e46fb2e02ffabfaa2b
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, rm, writeFile, mkdir, readFile, readdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import {
installDir, pointerFile, portfolioDir, assertValidName, normalizeName, sameName,
readActiveName, writeActiveName, activeName, activeDir,
migrateLooseLayout, prepareLayout, DEFAULT_NAME, MIGRATION_NAME,
} from '../../server/portfolios.mjs';
import { dbFile } from '../../server/db.js';
import { secretsFile } from '../../server/keys.mjs';
let DIR;
let vorige;
beforeEach(async () => {
vorige = process.env.ASSETAPP_DATA_DIR;
DIR = await mkdtemp(join(tmpdir(), 'assetapp-portefeuilles-'));
process.env.ASSETAPP_DATA_DIR = DIR;
});
afterEach(async () => {
if (vorige === undefined) delete process.env.ASSETAPP_DATA_DIR;
else process.env.ASSETAPP_DATA_DIR = vorige;
await rm(DIR, { recursive: true, force: true });
});
const bestandenIn = async (pad) => (await readdir(pad)).sort((a, b) => a.localeCompare(b, 'nl'));
describe('portfolios: de naam als pad', () => {
it('leest de installatiemap bij elke aanroep, niet bij het laden', () => {
assert.equal(installDir(), DIR);
process.env.ASSETAPP_DATA_DIR = join(DIR, 'ergens-anders');
assert.equal(installDir(), join(DIR, 'ergens-anders'));
process.env.ASSETAPP_DATA_DIR = DIR;
});
it('weigert een naam die buiten de datamap wijst', () => {
for (const naam of ['..', '../buiten', '..\\buiten', '/etc/passwd', 'C:\\Windows']) {
assert.throws(() => portfolioDir(naam), (e) => e.key.startsWith('portfolios.name'), `'${naam}' hoort geweigerd te worden`);
}
});
it('weigert de gereserveerde apparaatnamen van Windows, ook met een extensie', () => {
for (const naam of ['CON', 'con', 'NUL', 'COM1', 'LPT9', 'CON.txt', 'aux.sqlite']) {
assert.throws(() => assertValidName(naam), (e) => e.key === 'portfolios.nameReserved', `'${naam}' hoort geweigerd te worden`);
}
for (const naam of ['Console', 'Com', 'Auxiliair', 'Nulmeting']) assertValidName(naam);
});
it('weigert een naam die op een spatie of punt eindigt, in plaats van hem stil af te knippen', () => {
assert.throws(() => assertValidName('Demo '), (e) => e.key === 'portfolios.nameTrailing');
assert.throws(() => assertValidName('Demo.'), (e) => e.key === 'portfolios.nameTrailing');
assert.throws(() => assertValidName(' Demo'), (e) => e.key === 'portfolios.nameLeadingSpace');
});
it('weigert de verboden tekens en de stuurtekens', () => {
for (const naam of ['a<b', 'a>b', 'a:b', 'a"b', 'a|b', 'a?b', 'a*b']) {
assert.throws(() => assertValidName(naam), (e) => e.key === 'portfolios.nameForbiddenChars', `'${naam}' hoort geweigerd te worden`);
}
assert.throws(() => assertValidName(`regel${String.fromCharCode(10)}twee`), (e) => e.key === 'portfolios.nameForbiddenChars');
assert.throws(() => assertValidName(`nul${String.fromCharCode(0)}byte`), (e) => e.key === 'portfolios.nameForbiddenChars');
});
it('weigert een lege naam en een naam die te lang is', () => {
assert.throws(() => assertValidName(''), (e) => e.key === 'portfolios.nameRequired');
assert.throws(() => assertValidName(null), (e) => e.key === 'portfolios.nameRequired');
assert.throws(() => assertValidName('x'.repeat(65)), (e) => e.key === 'portfolios.nameTooLong');
assertValidName('x'.repeat(64));
});
it('laat gewone namen met spaties, koppeltekens en accenten door', () => {
for (const naam of ['Echt', 'Demo van Jan', 'test-2026', 'Privé']) {
assert.equal(portfolioDir(naam), join(DIR, naam));
}
});
it('vergelijkt namen op hun genormaliseerde vorm, want niet elk bestandssysteem doet dat gelijk', () => {
assert.ok(sameName('Echt', 'echt'), 'hoofdletters maken op Windows en macOS geen verschil');
const samengesteld = `Priv${String.fromCharCode(101, 0x0301)}`;
assert.equal(normalizeName(samengesteld), normalizeName('Privé'));
assert.ok(!sameName('Echt', 'Demo'));
});
it('weigert alles wat geen tekst is, in plaats van er stil een tekst van te maken', () => {
assert.throws(() => sameName({ naam: 'Echt' }, 'Echt'), TypeError);
assert.throws(() => normalizeName(42), TypeError);
});
it('maakt twee verschillende objecten niet aan elkaar gelijk', () => {
assert.throws(() => sameName({ naam: 'Echt' }, { naam: 'Demo' }), TypeError,
'zonder deze guard gaven allebei "[object Object]" en was het antwoord true');
});
it('laat "nog geen wijzer" wél toe, want daar wordt rechtstreeks mee vergeleken', () => {
assert.equal(sameName('Echt', null), false);
assert.equal(sameName(null, null), false, 'niets is ook niet gelijk aan zichzelf');
assert.equal(sameName(undefined, 'Echt'), false);
});
});
describe('portfolios: de wijzer', () => {
it('geeft null als er nog geen wijzer is, en dan valt activeName op de standaard terug', () => {
assert.equal(readActiveName(), null);
assert.equal(activeName(), DEFAULT_NAME);
assert.equal(readActiveName(), null);
});
it('schrijft en leest dezelfde naam terug', () => {
writeActiveName('Demo');
assert.equal(readActiveName(), 'Demo');
assert.equal(activeDir(), join(DIR, 'Demo'));
});
it('weigert een wijzer die geen geldige JSON is', async () => {
await writeFile(pointerFile(), '{ dit is geen json', 'utf-8');
assert.throws(() => readActiveName(), /not valid JSON/);
});
it('weigert een wijzer zonder portefeuille, en een wijzer met een onmogelijke naam', async () => {
await writeFile(pointerFile(), JSON.stringify({ iets: 'anders' }), 'utf-8');
assert.throws(() => readActiveName(), /names no portfolio/);
await writeFile(pointerFile(), JSON.stringify({ portefeuille: '../buiten' }), 'utf-8');
assert.throws(() => readActiveName(), (e) => e.key === 'portfolios.nameForbiddenChars');
});
it('weigert een onmogelijke naam ook bij het schrijven', () => {
assert.throws(() => writeActiveName('CON'), (e) => e.key === 'portfolios.nameReserved');
assert.throws(() => writeActiveName('../buiten'), (e) => e.key === 'portfolios.nameForbiddenChars');
});
});
describe('portfolios: de eenmalige verhuizing', () => {
const legLosseInstallatieAan = async () => {
await writeFile(join(DIR, 'db.sqlite'), 'database', 'utf-8');
await writeFile(join(DIR, 'db.sqlite-wal'), 'wal', 'utf-8');
await writeFile(join(DIR, 'db.sqlite-shm'), 'shm', 'utf-8');
await writeFile(join(DIR, 'secrets.json'), '{"assetapp_auth":{"user":"ik"}}', 'utf-8');
await mkdir(join(DIR, 'tls'), { recursive: true });
await writeFile(join(DIR, 'tls', 'cert.pem'), 'certificaat', 'utf-8');
};
it('verhuist de database en de geheimen naar data/Test/, en laat tls/ staan', async () => {
await legLosseInstallatieAan();
assert.equal(migrateLooseLayout(), MIGRATION_NAME);
assert.deepEqual(await bestandenIn(join(DIR, MIGRATION_NAME)),
['db.sqlite', 'db.sqlite-shm', 'db.sqlite-wal', 'secrets.json']);
assert.deepEqual(await bestandenIn(DIR), [MIGRATION_NAME, 'tls']);
assert.equal(await readFile(join(DIR, MIGRATION_NAME, 'secrets.json'), 'utf-8'),
'{"assetapp_auth":{"user":"ik"}}');
});
it('doet niets als er niets los staat, en is dus veilig om elke start te draaien', async () => {
await mkdir(join(DIR, 'Echt'), { recursive: true });
await writeFile(join(DIR, 'Echt', 'db.sqlite'), 'database', 'utf-8');
assert.equal(migrateLooseLayout(), null);
assert.deepEqual(await bestandenIn(DIR), ['Echt']);
});
it('WEIGERT als er zowel losse bestanden als een database op de doelplek staan', async () => {
await legLosseInstallatieAan();
await mkdir(join(DIR, MIGRATION_NAME), { recursive: true });
await writeFile(join(DIR, MIGRATION_NAME, 'db.sqlite'), 'een andere database', 'utf-8');
assert.throws(() => migrateLooseLayout(), /does not guess/);
assert.equal(await readFile(join(DIR, 'db.sqlite'), 'utf-8'), 'database');
assert.equal(await readFile(join(DIR, MIGRATION_NAME, 'db.sqlite'), 'utf-8'), 'een andere database');
});
});
describe('portfolios: database en geheimen volgen de wijzer', () => {
it('wijst na een wissel naar de andere map, zonder dat er iets opnieuw geladen wordt', () => {
writeActiveName('Echt');
assert.equal(dbFile(), join(DIR, 'Echt', 'db.sqlite'));
assert.equal(secretsFile(), join(DIR, 'Echt', 'secrets.json'));
writeActiveName('Demo');
assert.equal(dbFile(), join(DIR, 'Demo', 'db.sqlite'));
assert.equal(secretsFile(), join(DIR, 'Demo', 'secrets.json'));
});
it('houdt database en geheimen in dezelfde map, want geheimen zijn op rij-id gesleuteld', () => {
writeActiveName('Demo');
assert.equal(dirname(dbFile()), dirname(secretsFile()));
});
it('laat ASSETAPP_DB_FILE winnen, want daar leunen container en test op', () => {
writeActiveName('Echt');
process.env.ASSETAPP_DB_FILE = join(DIR, 'ergens', 'anders.sqlite');
try {
assert.equal(dbFile(), join(DIR, 'ergens', 'anders.sqlite'));
} finally {
delete process.env.ASSETAPP_DB_FILE;
}
});
});
describe('portfolios: de installatie klaarzetten', () => {
it('maakt bij een verse installatie de standaardportefeuille en de wijzer', async () => {
const uitkomst = prepareLayout();
assert.equal(uitkomst.name, DEFAULT_NAME);
assert.equal(uitkomst.migrated, null);
assert.equal(readActiveName(), DEFAULT_NAME);
assert.deepEqual(await bestandenIn(DIR), ['active.json', DEFAULT_NAME]);
assert.match(uitkomst.regel, new RegExp(`portefeuille: ${DEFAULT_NAME}`));
});
it('wijst na een verhuizing naar de verhuisde portefeuille en niet naar de standaard', async () => {
await writeFile(join(DIR, 'db.sqlite'), 'database', 'utf-8');
const uitkomst = prepareLayout();
assert.equal(uitkomst.migrated, MIGRATION_NAME);
assert.equal(uitkomst.name, MIGRATION_NAME);
assert.equal(readActiveName(), MIGRATION_NAME);
assert.match(uitkomst.regel, /verhuisd/);
});
it('trekt de wijzer van de eerste dag recht, zonder de keuze te verliezen', async () => {
await writeFile(join(DIR, 'actief.json'), JSON.stringify({ portefeuille: 'Test' }), 'utf-8');
const uitkomst = prepareLayout();
assert.equal(uitkomst.name, 'Test');
assert.equal(readActiveName(), 'Test');
assert.deepEqual(await bestandenIn(DIR), ['active.json', 'Test'], 'en het oude bestand is weg');
});
it('laat een bestaande wijzer met rust', async () => {
writeActiveName('Demo');
const uitkomst = prepareLayout();
assert.equal(uitkomst.name, 'Demo');
assert.equal(readActiveName(), 'Demo');
});
it('is idempotent: twee keer draaien verandert niets aan de uitkomst', async () => {
await writeFile(join(DIR, 'db.sqlite'), 'database', 'utf-8');
const eerste = prepareLayout();
const tweede = prepareLayout();
assert.equal(tweede.name, eerste.name);
assert.equal(tweede.migrated, null, 'de tweede keer valt er niets meer te verhuizen');
assert.deepEqual(await bestandenIn(DIR), ['active.json', MIGRATION_NAME]);
});
});