// 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'); }); });