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 { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.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 { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js"; import { createDistributionBoardInsertProjectCommand, createDistributionBoardStructureSnapshot, } from "../src/domain/models/distribution-board-structure-project-command.model.js"; import { createDistributionBoard } from "../src/frontend/utils/api.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 getStructureCounts(context: DatabaseContext) { return { boards: context.db.select().from(distributionBoards).all().length, lists: context.db.select().from(circuitLists).all().length, sections: context.db.select().from(circuitSections).all().length, revisions: context.db.select().from(projectRevisions).all().length, }; } describe("distribution-board structure project command", () => { it("builds the fixed setup and sends the expected frontend revision", async () => { const structure = createDistributionBoardStructureSnapshot( "project-1", " UV-02 " ); assert.equal(structure.distributionBoard.name, "UV-02"); assert.equal( structure.circuitList.name, "UV-02 Stromkreisliste" ); assert.deepEqual( structure.sections.map((section) => [ section.key, section.prefix, ]), [ ["lighting", "-1F"], ["single_phase", "-2F"], ["three_phase", "-3F"], ["unassigned", "-UF"], ] ); assert.throws( () => createDistributionBoardInsertProjectCommand({ ...structure, sections: structure.sections.slice(0, 3), }), /incomplete/ ); 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 createDistributionBoard("project-1", "UV-02", 7); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [ { url: "/api/projects/project-1/distribution-boards", body: { name: "UV-02", expectedRevision: 7 }, }, ]); }); it("creates stable structure and supports persisted undo and redo", () => { const context = createTestDatabase(); try { const repository = new DistributionBoardStructureProjectCommandRepository( context.db ); const history = new ProjectHistoryRepository(context.db); const structure = createDistributionBoardStructureSnapshot( "project-1", "UV-02" ); const command = createDistributionBoardInsertProjectCommand(structure); const forward = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual(getStructureCounts(context), { boards: 1, lists: 1, sections: 4, 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: forward.inverse, }); assert.deepEqual(getStructureCounts(context), { boards: 0, lists: 0, sections: 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(getStructureCounts(context), { boards: 1, lists: 1, sections: 4, revisions: 3, }); assert.ok( context.db .select() .from(distributionBoards) .where( eq( distributionBoards.id, structure.distributionBoard.id ) ) .get() ); } finally { context.close(); } }); it("rejects foreign, stale and changed structures without partial writes", () => { const context = createTestDatabase(); try { const repository = new DistributionBoardStructureProjectCommandRepository( context.db ); const foreign = createDistributionBoardInsertProjectCommand( createDistributionBoardStructureSnapshot( "project-2", "UV fremd" ) ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: foreign, }), /does not belong/ ); const structure = createDistributionBoardStructureSnapshot( "project-1", "UV-02" ); const command = createDistributionBoardInsertProjectCommand(structure); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command, }), ProjectRevisionConflictError ); assert.deepEqual(getStructureCounts(context), { boards: 0, lists: 0, sections: 0, revisions: 0, }); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); context.db .update(distributionBoards) .set({ name: "Direkt geƤndert" }) .where(eq(distributionBoards.id, structure.distributionBoard.id)) .run(); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo")?.changeSetId, command: inserted.inverse, }), /changed before deletion/ ); assert.equal( context.db.select().from(projectRevisions).all().length, 1 ); } finally { context.close(); } }); it("refuses populated deletion and rolls back late history failures", () => { const populatedContext = createTestDatabase(); try { const repository = new DistributionBoardStructureProjectCommandRepository( populatedContext.db ); const structure = createDistributionBoardStructureSnapshot( "project-1", "UV-02" ); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardInsertProjectCommand(structure), }); populatedContext.db .insert(circuits) .values({ id: "circuit-1", circuitListId: structure.circuitList.id, sectionId: structure.sections[0].id, equipmentIdentifier: "-1F1", sortOrder: 10, }) .run(); const undoStep = new ProjectHistoryRepository( populatedContext.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, }), /populated/ ); } finally { populatedContext.close(); } const rollbackContext = createTestDatabase(); try { rollbackContext.sqlite.exec(` CREATE TRIGGER fail_distribution_board_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced distribution-board history failure'); END; `); const repository = new DistributionBoardStructureProjectCommandRepository( rollbackContext.db ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardInsertProjectCommand( createDistributionBoardStructureSnapshot( "project-1", "UV rollback" ) ), }), /forced distribution-board history failure/ ); assert.deepEqual(getStructureCounts(rollbackContext), { boards: 0, lists: 0, sections: 0, revisions: 0, }); } finally { rollbackContext.close(); } }); });