import crypto from "node:crypto"; import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type DatabaseContext, } from "../src/db/database-context.js"; import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js"; import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.js"; import { floors } from "../src/db/schema/floors.js"; import { projectDevices } from "../src/db/schema/project-devices.js"; import { projectSnapshots } from "../src/db/schema/project-snapshots.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.js"; import { deserializeProjectStateSnapshot } from "../src/domain/models/project-state-snapshot.model.js"; import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js"; import { ProjectSnapshotNameConflictError } from "../src/domain/errors/project-snapshot-name-conflict.error.js"; function createTestDatabase(): DatabaseContext { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations"), }); context.db .insert(projects) .values({ id: "project-1", name: "Snapshot-Projekt", singlePhaseVoltageV: 230, threePhaseVoltageV: 400, }) .run(); const board = new DistributionBoardRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); const list = context.db .select() .from(circuitLists) .where(eq(circuitLists.distributionBoardId, board.id)) .get(); assert.ok(list); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, list.id)) .get(); assert.ok(section); context.db .insert(floors) .values({ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 10, }) .run(); context.db .insert(rooms) .values({ id: "room-1", projectId: "project-1", floorId: "floor-1", roomNumber: "001", roomName: "Technik", }) .run(); context.db .insert(projectDevices) .values({ id: "device-1", projectId: "project-1", name: "Pumpe", displayName: "Pumpe", phaseType: "three_phase", quantity: 1, powerPerUnit: 2.5, simultaneityFactor: 0.8, cosPhi: 0.9, voltageV: 400, }) .run(); context.db .insert(circuits) .values({ id: "circuit-1", circuitListId: list.id, sectionId: section.id, equipmentIdentifier: "-1F1", displayName: "Technik", sortOrder: 10, voltage: 230, isReserve: 0, }) .run(); context.db .insert(circuitDeviceRows) .values({ id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", roomId: "room-1", sortOrder: 10, name: "Pumpe", displayName: "Pumpe lokal", phaseType: "three_phase", quantity: 1, powerPerUnit: 2.5, simultaneityFactor: 0.8, cosPhi: 0.9, }) .run(); return context; } describe("project snapshot repository", () => { it("captures, hashes and lists a complete named logical snapshot", () => { const context = createTestDatabase(); try { const repository = new ProjectSnapshotRepository(context.db); const created = repository.createNamed({ projectId: "project-1", expectedRevision: 0, name: " Vor Ausschreibung ", description: " Sicherer Stand ", actorId: "planner-1", }); assert.ok(created); assert.equal(created.sourceRevision, 0); assert.equal(created.schemaVersion, 1); assert.equal(created.name, "Vor Ausschreibung"); assert.equal(created.description, "Sicherer Stand"); assert.equal(created.createdByActorId, "planner-1"); assert.match(created.payloadSha256, /^[a-f0-9]{64}$/); assert.equal( context.db .select({ currentRevision: projects.currentRevision }) .from(projects) .where(eq(projects.id, "project-1")) .get()?.currentRevision, 0 ); const persisted = context.db .select() .from(projectSnapshots) .where(eq(projectSnapshots.id, created.id)) .get(); assert.ok(persisted); assert.equal( crypto .createHash("sha256") .update(persisted.payloadJson) .digest("hex"), persisted.payloadSha256 ); const state = deserializeProjectStateSnapshot( persisted.payloadJson ); assert.equal(state.project.id, "project-1"); assert.equal(state.distributionBoards.length, 1); assert.equal(state.circuitLists.length, 1); assert.equal( state.circuitSections.length, context.db.select().from(circuitSections).all().length ); assert.equal(state.circuits.length, 1); assert.equal(state.circuits[0].deviceRows.length, 1); assert.equal( state.circuits[0].deviceRows[0].linkedProjectDeviceId, "device-1" ); assert.equal(state.projectDevices.length, 1); assert.equal(state.floors.length, 1); assert.equal(state.rooms.length, 1); assert.deepEqual(repository.listByProject("project-1"), [ created, ]); } finally { context.close(); } }); it("rejects stale revisions and duplicate names without partial rows", () => { const context = createTestDatabase(); try { const repository = new ProjectSnapshotRepository(context.db); assert.throws( () => repository.createNamed({ projectId: "project-1", expectedRevision: 1, name: "Stale", }), ProjectRevisionConflictError ); const created = repository.createNamed({ projectId: "project-1", expectedRevision: 0, name: "Freigabe", }); assert.ok(created); assert.throws( () => repository.createNamed({ projectId: "project-1", expectedRevision: 0, name: " Freigabe ", }), ProjectSnapshotNameConflictError ); assert.equal( context.db.select().from(projectSnapshots).all().length, 1 ); } finally { context.close(); } }); it("distinguishes unknown projects from empty snapshot lists", () => { const context = createTestDatabase(); try { const repository = new ProjectSnapshotRepository(context.db); assert.deepEqual(repository.listByProject("project-1"), []); assert.equal(repository.listByProject("missing"), null); assert.equal( repository.createNamed({ projectId: "missing", expectedRevision: 0, name: "Unbekannt", }), null ); } finally { context.close(); } }); it("verifies stored payload integrity before preparing a restore", () => { const context = createTestDatabase(); try { const repository = new ProjectSnapshotRepository(context.db); const snapshot = repository.createNamed({ projectId: "project-1", expectedRevision: 0, name: "Freigabe", }); assert.ok(snapshot); const prepared = repository.prepareRestore( "project-1", snapshot.id ); assert.ok(prepared); assert.equal( prepared.command.payload.targetState.project.id, "project-1" ); context.db .update(projectSnapshots) .set({ payloadJson: `${JSON.stringify({})}\n` }) .where(eq(projectSnapshots.id, snapshot.id)) .run(); assert.throws( () => repository.prepareRestore("project-1", snapshot.id), /checksum verification failed/ ); assert.equal( repository.prepareRestore("project-1", "missing"), null ); } finally { context.close(); } }); });