Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
// Generated, do not edit. sha256:9963d8baa8caaf89cbf056ba74cfb376898f7081b03f3679eb8d46496383efaf
import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
import { mkdtemp, rm, readFile, writeFile, access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const DIR = await mkdtemp(join(tmpdir(), 'assetapp-backup-'));
process.env.ASSETAPP_DATA_DIR = DIR;
const { initDatabase, dbFile } = await import('../../server/db.js');
const { closeDatabase, run, one } = await import('../../server/db/conn.mjs');
const { secretsFile } = await import('../../server/keys.mjs');
const { prepareLayout } = await import('../../server/portfolios.mjs');
prepareLayout();
const DB_FILE = dbFile();
const SECRETS_FILE = secretsFile();
const { createBackup, restoreBackup, isBackupBestand, FORMAAT } = await import('../../server/db-backup.mjs');
const { CONFIG } = await import('../../shared/config.js');
const { SCHEMA } = await import('../../server/db/schema.js');
const WACHTWOORD = 'een-lang-genoeg-wachtwoord';
async function bundelVan(dbBytes, secretsTekst, manifestOver = null) {
const { randomBytes, scrypt, createCipheriv } = await import('node:crypto');
const { promisify } = await import('node:util');
const salt = randomBytes(FORMAAT.SALT_LEN);
const iv = randomBytes(FORMAAT.IV_LEN);
const sleutel = await promisify(scrypt)(WACHTWOORD, salt, 32, FORMAAT.SCRYPT);
const secrets = Buffer.from(secretsTekst, 'utf-8');
const manifest = Buffer.from(JSON.stringify(
{ v: 1, gemaakt: '2026-01-01T00:00:00.000Z', db: dbBytes.length, secrets: secrets.length, ...manifestOver })
+ '\n', 'utf-8');
const c = createCipheriv('aes-256-gcm', sleutel, iv);
const cijfer = Buffer.concat([c.update(Buffer.concat([manifest, dbBytes, secrets])), c.final()]);
return Buffer.concat([FORMAAT.MAGIC, Buffer.of(FORMAAT.VERSIE), salt, iv, c.getAuthTag(), cijfer]);
}
let fixtureTeller = 0;
async function geldigeDatabase(versie = CONFIG.SCHEMA_VERSION) {
const p = join(DIR, `fixture-${versie}-${++fixtureTeller}.sqlite`);
const db = new DatabaseSync(p);
db.exec(SCHEMA);
db.prepare(`INSERT INTO Schema (version) VALUES (?)`).run(BigInt(versie));
db.close();
return readFile(p);
}
const bestaat = async p => { try { await access(p); return true; } catch { return false; } };
const merk = () => { const r = one(`SELECT name FROM Label LIMIT 1`); return r ? r.name : null; };
const geheimen = async () => JSON.parse(await readFile(SECRETS_FILE, 'utf-8'));
async function opzet(naam) {
closeDatabase();
await rm(DB_FILE, { force: true });
for (const b of ['-wal', '-shm']) await rm(DB_FILE + b, { force: true });
initDatabase();
run(`DELETE FROM Label`);
run(`INSERT INTO Label (name, searchName) VALUES (?,?)`, [naam, naam]);
await writeFile(SECRETS_FILE, JSON.stringify({ [`nwc_${naam}`]: { uri: `geheim-van-${naam}` } }), { mode: 0o600 });
}
before(() => initDatabase());
after(async () => { closeDatabase(); await rm(DIR, { recursive: true, force: true }); });
beforeEach(() => opzet('origineel'));
describe('createBackup / restoreBackup: heen en terug', () => {
it('zet database én geheimen samen terug', async () => {
const { bytes } = await createBackup(WACHTWOORD);
await opzet('later');
assert.equal(merk(), 'later');
const r = await restoreBackup(bytes, WACHTWOORD);
assert.equal(merk(), 'origineel');
assert.deepEqual(await geheimen(), { nwc_origineel: { uri: 'geheim-van-origineel' } });
assert.equal(r.geheimen, 1);
assert.equal(r.versie, CONFIG.SCHEMA_VERSION);
});
it('bewaart van allebei de bestanden de vorige versie', async () => {
const { bytes } = await createBackup(WACHTWOORD);
await opzet('later');
const r = await restoreBackup(bytes, WACHTWOORD);
assert.ok(await bestaat(r.previousDb), 'de vorige database staat er nog');
assert.ok(await bestaat(r.previousSecrets), 'en de vorige geheimen ook');
assert.deepEqual(JSON.parse(await readFile(r.previousSecrets, 'utf-8')),
{ nwc_later: { uri: 'geheim-van-later' } });
});
it('werkt zonder geheimenbestand (verse installatie)', async () => {
await rm(SECRETS_FILE, { force: true });
const { bytes, secrets } = await createBackup(WACHTWOORD);
assert.ok(secrets > 0, 'er zit een lege bundel in, geen niets');
const r = await restoreBackup(bytes, WACHTWOORD);
assert.equal(r.geheimen, 0);
});
it('bevat de laatste schrijfacties, ook die nog in het WAL stonden', async () => {
run(`INSERT INTO Label (name, searchName) VALUES ('net-geschreven','net-geschreven')`);
const { bytes } = await createBackup(WACHTWOORD);
await opzet('later');
await restoreBackup(bytes, WACHTWOORD);
assert.equal(one(`SELECT COUNT(*) AS n FROM Label WHERE name = 'net-geschreven'`).n, 1);
});
});
describe('createBackup / restoreBackup: het formaat', () => {
it('is herkenbaar aan zijn kop', async () => {
const { bytes } = await createBackup(WACHTWOORD);
assert.ok(isBackupBestand(bytes));
assert.ok(!isBackupBestand(Buffer.from('SQLite format 3\0')), 'een losse database is geen back-up');
});
it('houdt de kop-indeling vast, zodat hij met de hand te openen blijft', async () => {
assert.equal(FORMAAT.MAGIC.toString('latin1'), 'ASSETAPP-BACKUP');
assert.deepEqual(
{ v: FORMAAT.VERSIE, salt: FORMAAT.SALT_LEN, iv: FORMAAT.IV_LEN, tag: FORMAAT.TAG_LEN, kop: FORMAAT.KOP_LEN },
{ v: 1, salt: 16, iv: 12, tag: 16, kop: 60 });
assert.deepEqual({ N: FORMAAT.SCRYPT.N, r: FORMAAT.SCRYPT.r, p: FORMAAT.SCRYPT.p }, { N: 32768, r: 8, p: 1 });
});
it('geeft bij dezelfde inhoud twee keer een ander bestand', async () => {
const a = await createBackup(WACHTWOORD);
const b = await createBackup(WACHTWOORD);
assert.ok(!a.bytes.equals(b.bytes));
const salt = x => x.bytes.subarray(16, 32).toString('hex');
const iv = x => x.bytes.subarray(32, 44).toString('hex');
assert.notEqual(salt(a), salt(b));
assert.notEqual(iv(a), iv(b));
});
it('bevat het geheim niet leesbaar', async () => {
const { bytes } = await createBackup(WACHTWOORD);
assert.equal(bytes.includes(Buffer.from('geheim-van-origineel')), false);
assert.equal(bytes.includes(Buffer.from('SQLite format 3')), false, 'ook de database is versleuteld');
});
it('weigert een te kort wachtwoord', async () => {
await assert.rejects(() => createBackup(''), (e) => e.key === 'dbfile.passTooShort');
await assert.rejects(() => createBackup('kort'), (e) => e.key === 'dbfile.passTooShort');
});
});
describe('restoreBackup: een afgekeurde bundel raakt niets aan', () => {
async function weigert(bytes, wachtwoord, patroon) {
await assert.rejects(() => restoreBackup(bytes, wachtwoord), patroon);
assert.equal(merk(), 'origineel', 'de live database is niet aangeraakt');
assert.deepEqual(await geheimen(), { nwc_origineel: { uri: 'geheim-van-origineel' } },
'en de geheimen ook niet');
assert.equal(await bestaat(DB_FILE + '.new'), false, 'geen half bestand achtergelaten');
}
it('weigert een verkeerd wachtwoord', async () => {
const { bytes } = await createBackup(WACHTWOORD);
await weigert(bytes, 'een-ander-wachtwoord', (e) => e.key === 'dbfile.decryptFailed');
});
it('weigert een bundel waar één byte aan veranderd is', async () => {
const { bytes } = await createBackup(WACHTWOORD);
const stuk = Buffer.from(bytes);
stuk[stuk.length - 1] ^= 0xff;
await weigert(stuk, WACHTWOORD, (e) => e.key === 'dbfile.decryptFailed');
});
it('weigert een afgekapte bundel', async () => {
const { bytes } = await createBackup(WACHTWOORD);
await weigert(bytes.subarray(0, Math.floor(bytes.length / 2)), WACHTWOORD, (e) => (e.key || '').startsWith('dbfile.'));
});
it('weigert een bestand dat geen back-up is, met een verwijzing naar de andere knop', async () => {
await weigert(Buffer.from('SQLite format 3\0hier staat een database'), WACHTWOORD, (e) => (e.key || '').startsWith('dbfile.notABackup'));
});
it('weigert een onbekende formaatversie', async () => {
const { bytes } = await createBackup(WACHTWOORD);
const toekomst = Buffer.from(bytes);
toekomst[15] = 99;
await weigert(toekomst, WACHTWOORD, (e) => e.key === 'dbfile.unknownBackupVersion');
});
it('weigert een bundel met een database die te oud is voor deze app', async () => {
const oud = join(DIR, 'oud.sqlite');
const db = new DatabaseSync(oud);
db.exec(`CREATE TABLE Schema (version INTEGER NOT NULL) STRICT;`);
db.prepare(`INSERT INTO Schema (version) VALUES (?)`).run(59n);
db.close();
await weigert(await bundelVan(await readFile(oud), '{}'), WACHTWOORD, /baseline-convert/);
});
it('weigert een bundel waarvan de lengtes in het manifest niet bij de inhoud passen', async () => {
const db = await geldigeDatabase();
await weigert(await bundelVan(db, '{}', { db: db.length - 100 }), WACHTWOORD, (e) => e.key === 'dbfile.manifestLengths');
});
it('weigert een bundel waarin het geheimenbestand geen JSON is', async () => {
await weigert(await bundelVan(await geldigeDatabase(), 'dit is geen json'), WACHTWOORD, (e) => e.key === 'dbfile.secretsUnreadable');
});
});