import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, it } from "node:test"; import Database from "better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createVerifiedDatabaseBackup, verifyDatabaseFile, } from "../src/db/database-backup.js"; import { createDatabaseContext } from "../src/db/database-context.js"; import { projects } from "../src/db/schema/projects.js"; describe("database backup and restore", () => { it("backs up committed WAL data and restores it as an independent database", async () => { const testDirectory = fs.mkdtempSync( path.join(os.tmpdir(), "leistungsbilanz-db-backup-") ); const sourcePath = path.join(testDirectory, "source.db"); const backupPath = path.join(testDirectory, "backup.db"); const restoredPath = path.join(testDirectory, "restored.db"); const source = createDatabaseContext(sourcePath); try { source.sqlite.pragma("journal_mode = WAL"); migrate(source.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); source.db .insert(projects) .values({ id: "project-in-backup", name: "Backup project" }) .run(); assert.equal(fs.existsSync(`${sourcePath}-wal`), true); const backupVerification = await createVerifiedDatabaseBackup( sourcePath, backupPath ); assert.deepEqual(backupVerification, { integrityCheck: "ok", foreignKeyViolations: 0, }); source.db .insert(projects) .values({ id: "project-after-backup", name: "Later project" }) .run(); source.close(); await createVerifiedDatabaseBackup(backupPath, restoredPath); const restored = new Database(restoredPath, { readonly: true, fileMustExist: true, }); try { const restoredProjects = restored .prepare("SELECT id, name FROM projects ORDER BY id") .all(); assert.deepEqual(restoredProjects, [ { id: "project-in-backup", name: "Backup project" }, ]); } finally { restored.close(); } } finally { if (source.sqlite.open) { source.close(); } fs.rmSync(testDirectory, { recursive: true, force: true }); } }); it("rejects a corrupt database file", () => { const testDirectory = fs.mkdtempSync( path.join(os.tmpdir(), "leistungsbilanz-db-verify-") ); const corruptPath = path.join(testDirectory, "corrupt.db"); try { fs.writeFileSync(corruptPath, "not a sqlite database"); assert.throws(() => verifyDatabaseFile(corruptPath), /not a database/); } finally { fs.rmSync(testDirectory, { recursive: true, force: true }); } }); });