tsx runs tests without type checking and tsconfig.json covers only src, so 71 type errors had accumulated unnoticed. tsconfig.tests.json now checks every test and helper, and npm run typecheck includes it. Fixtures gain the fields the DTOs have grown since they were written (manualQuantity, circuit apparent power/cos phi, downstream board loads, section totals, building, cover and board info fields). Repository lookups that return "row ?? null" now declare the null in their type, which their callers already handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
5.7 KiB
TypeScript
166 lines
5.7 KiB
TypeScript
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,
|
|
phaseCount: 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();
|
|
}
|
|
});
|
|
});
|