257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
// 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]);
|
|
});
|
|
});
|