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 { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js"; import { consumers } from "../src/db/schema/consumers.js"; import { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { floors } from "../src/db/schema/floors.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.js"; import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js"; import { createProjectFloorInsertProjectCommand, createProjectFloorSnapshot, createProjectRoomInsertProjectCommand, createProjectRoomSnapshot, } from "../src/domain/models/project-location-structure-project-command.model.js"; import { createFloor, createRoom } from "../src/frontend/utils/api.js"; import { createFloorSchema, createRoomSchema, } from "../src/shared/validation/project-structure.schemas.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: "Test project" }, { id: "project-2", name: "Foreign project" }, ]) .run(); return context; } function getCounts(context: DatabaseContext) { return { floors: context.db.select().from(floors).all().length, rooms: context.db.select().from(rooms).all().length, revisions: context.db.select().from(projectRevisions).all().length, }; } describe("project-location structure project command", () => { it("normalizes snapshots and sends optimistic frontend revisions", async () => { const floor = createProjectFloorSnapshot("project-1", " EG ", 0); const room = createProjectRoomSnapshot("project-1", { floorId: floor.id, roomNumber: " 001 ", roomName: " Technik ", }); assert.equal(floor.name, "EG"); assert.equal(room.roomNumber, "001"); assert.equal(room.roomName, "Technik"); assert.throws( () => createProjectFloorInsertProjectCommand({ ...floor, name: " EG ", }), /normalized/ ); assert.equal( createFloorSchema.safeParse({ name: "EG" }).success, false ); assert.equal( createRoomSchema.safeParse({ expectedRevision: 0, roomNumber: "001", roomName: "Technik", unknown: true, }).success, false ); const requests: Array<{ url: string; body: unknown }> = []; const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { requests.push({ url: String(input), body: JSON.parse(String(init?.body)), }); return new Response(JSON.stringify({}), { status: 200, headers: { "Content-Type": "application/json" }, }); }; try { await createFloor("project-1", { name: "EG" }, 7); await createRoom( "project-1", { floorId: "floor-1", roomNumber: "001", roomName: "Technik", }, 8 ); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [ { url: "/api/projects/project-1/floors", body: { name: "EG", expectedRevision: 7 }, }, { url: "/api/projects/project-1/rooms", body: { floorId: "floor-1", roomNumber: "001", roomName: "Technik", expectedRevision: 8, }, }, ]); }); it("creates a stable floor and supports persisted undo and redo", () => { const context = createTestDatabase(); try { const repository = new ProjectLocationStructureProjectCommandRepository(context.db); const history = new ProjectHistoryRepository(context.db); const floor = createProjectFloorSnapshot("project-1", "EG", 0); const command = createProjectFloorInsertProjectCommand(floor); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual(getCounts(context), { floors: 1, rooms: 0, revisions: 1, }); const undoStep = history.getNextCommand("project-1", "undo"); assert.ok(undoStep); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }); assert.deepEqual(getCounts(context), { floors: 0, rooms: 0, revisions: 2, }); const redoStep = history.getNextCommand("project-1", "redo"); assert.ok(redoStep); repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: redoStep.changeSetId, command, }); assert.deepEqual( context.db .select() .from(floors) .where(eq(floors.id, floor.id)) .get(), floor ); } finally { context.close(); } }); it("does not undo a floor assigned to a distribution board", () => { const context = createTestDatabase(); try { const repository = new ProjectLocationStructureProjectCommandRepository(context.db); const floor = createProjectFloorSnapshot("project-1", "EG", 0); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createProjectFloorInsertProjectCommand(floor), }); context.db .insert(distributionBoards) .values({ id: "board-1", projectId: "project-1", name: "UV-01", floorId: floor.id, supplyType: "AV", }) .run(); const undoStep = new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo"); assert.ok(undoStep); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), /assigned to a distribution board/ ); assert.equal( context.db.select().from(projectRevisions).all().length, 1 ); } finally { context.close(); } }); it("creates a stable room and supports persisted undo and redo", () => { const context = createTestDatabase(); try { context.db .insert(floors) .values({ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 0, }) .run(); const repository = new ProjectLocationStructureProjectCommandRepository(context.db); const history = new ProjectHistoryRepository(context.db); const room = createProjectRoomSnapshot("project-1", { floorId: "floor-1", roomNumber: "001", roomName: "Technik", }); const command = createProjectRoomInsertProjectCommand(room); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); const undoStep = history.getNextCommand("project-1", "undo"); assert.ok(undoStep); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }); assert.equal( context.db .select() .from(rooms) .where(eq(rooms.id, room.id)) .get(), undefined ); const redoStep = history.getNextCommand("project-1", "redo"); assert.ok(redoStep); repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: redoStep.changeSetId, command, }); assert.deepEqual( context.db .select() .from(rooms) .where(eq(rooms.id, room.id)) .get(), room ); } finally { context.close(); } }); it("rejects foreign, stale and changed locations without partial writes", () => { const context = createTestDatabase(); try { context.db .insert(floors) .values({ id: "foreign-floor", projectId: "project-2", name: "Fremd", sortOrder: 0, }) .run(); const repository = new ProjectLocationStructureProjectCommandRepository(context.db); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createProjectRoomInsertProjectCommand( createProjectRoomSnapshot("project-1", { floorId: "foreign-floor", roomNumber: "001", roomName: "Fremdraum", }) ), }), /foreign floor/ ); const floor = createProjectFloorSnapshot("project-1", "EG", 0); const command = createProjectFloorInsertProjectCommand(floor); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command, }), ProjectRevisionConflictError ); assert.deepEqual(getCounts(context), { floors: 1, rooms: 0, revisions: 0, }); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); context.db .update(floors) .set({ name: "Direkt geƤndert" }) .where(eq(floors.id, floor.id)) .run(); const undoStep = new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo"); assert.ok(undoStep); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), /changed before deletion/ ); assert.equal( context.db.select().from(projectRevisions).all().length, 1 ); } finally { context.close(); } }); it("refuses referenced deletes and rolls back late history failures", () => { const floorContext = createTestDatabase(); try { const repository = new ProjectLocationStructureProjectCommandRepository( floorContext.db ); const floor = createProjectFloorSnapshot("project-1", "EG", 0); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createProjectFloorInsertProjectCommand(floor), }); floorContext.db .insert(rooms) .values({ id: "room-1", projectId: "project-1", floorId: floor.id, roomNumber: "001", roomName: "Technik", }) .run(); const undoStep = new ProjectHistoryRepository( floorContext.db ).getNextCommand("project-1", "undo"); assert.ok(undoStep); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), /assigned rooms/ ); } finally { floorContext.close(); } const roomContext = createTestDatabase(); try { const repository = new ProjectLocationStructureProjectCommandRepository( roomContext.db ); const room = createProjectRoomSnapshot("project-1", { roomNumber: "001", roomName: "Technik", }); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createProjectRoomInsertProjectCommand(room), }); roomContext.db .insert(consumers) .values({ id: "legacy-consumer-1", projectId: "project-1", roomId: room.id, name: "Bestand", quantity: 1, installedPowerPerUnitKw: 1, demandFactor: 1, }) .run(); const undoStep = new ProjectHistoryRepository( roomContext.db ).getNextCommand("project-1", "undo"); assert.ok(undoStep); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), /referenced project room/ ); } finally { roomContext.close(); } const rollbackContext = createTestDatabase(); try { rollbackContext.sqlite.exec(` CREATE TRIGGER fail_project_location_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced project-location history failure'); END; `); const repository = new ProjectLocationStructureProjectCommandRepository( rollbackContext.db ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createProjectFloorInsertProjectCommand( createProjectFloorSnapshot( "project-1", "Rollback", 0 ) ), }), /forced project-location history failure/ ); assert.deepEqual(getCounts(rollbackContext), { floors: 0, rooms: 0, revisions: 0, }); } finally { rollbackContext.close(); } }); });