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 { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js"; import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js"; import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js"; import { CircuitRepository } from "../src/db/repositories/circuit.repository.js"; import { FloorRepository } from "../src/db/repositories/floor.repository.js"; import { GlobalDeviceRepository } from "../src/db/repositories/global-device.repository.js"; import { ProjectDeviceRepository } from "../src/db/repositories/project-device.repository.js"; import { ProjectRepository } from "../src/db/repositories/project.repository.js"; import { RoomRepository } from "../src/db/repositories/room.repository.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 }); } }); }); describe("application repository database contexts", () => { it("keep reads and writes inside their injected database", async () => { const first = createDatabaseContext(":memory:"); const second = createDatabaseContext(":memory:"); try { for (const context of [first, second]) { migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); } const firstProjects = new ProjectRepository(first.db); const secondProjects = new ProjectRepository(second.db); const project = await firstProjects.create({ name: "Nur in Datenbank A" }); assert.equal((await firstProjects.list()).length, 1); assert.deepEqual(await secondProjects.list(), []); const firstGlobalDevices = new GlobalDeviceRepository(first.db); const secondGlobalDevices = new GlobalDeviceRepository(second.db); await firstGlobalDevices.create({ name: "Steckdose", displayName: "Steckdose", quantity: 1, installedPowerPerUnitKw: 0.2, demandFactor: 1, }); assert.equal((await firstGlobalDevices.list()).length, 1); assert.deepEqual(await secondGlobalDevices.list(), []); assert.deepEqual( await new CircuitListRepository(first.db).listByProject(project.id), [] ); assert.deepEqual( await new CircuitSectionRepository(first.db).listByCircuitList( "unknown-list" ), [] ); assert.deepEqual( await new CircuitRepository(first.db).listByCircuitList("unknown-list"), [] ); assert.deepEqual( await new CircuitDeviceRowRepository(first.db).listByCircuitList([]), [] ); assert.deepEqual( await new FloorRepository(first.db).listByProject(project.id), [] ); assert.deepEqual( await new RoomRepository(first.db).listByProject(project.id), [] ); assert.deepEqual( await new ProjectDeviceRepository(first.db).listByProject(project.id), [] ); } finally { first.close(); second.close(); } }); });