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 { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-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 { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projects } from "../src/db/schema/projects.js"; import { createDistributionBoardComponentInsertProjectCommand, type DistributionBoardComponentSnapshot, } from "../src/domain/models/distribution-board-component-structure-project-command.model.js"; import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js"; import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.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: "Projekt" }, { id: "project-2", name: "Fremdprojekt" }, ]) .run(); new DistributionBoardFixtureRepository( context.db ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); return context; } function getListAndSection(context: DatabaseContext) { const list = context.db.select().from(circuitLists).get(); assert.ok(list); const section = context.db .select() .from(circuitSections) .where(eq(circuitSections.circuitListId, list.id)) .get(); assert.ok(section); return { list, section }; } describe("distribution-board component structure project command", () => { it("inserts an auxiliary component and preserves it through undo and redo", () => { const context = createTestDatabase(); try { const { list } = getListAndSection(context); const snapshot: DistributionBoardComponentSnapshot = { component: { id: "component-aux", circuitListId: list.id, sectionId: null, equipmentIdentifier: "-K1", name: "KNX Schaltaktor", role: "auxiliary", placement: "footer", sortOrder: 10, }, protectionDevice: null, }; const repository = new DistributionBoardComponentStructureProjectCommandRepository( context.db ); const history = new ProjectHistoryRepository(context.db); const command = createDistributionBoardComponentInsertProjectCommand(snapshot); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual( context.db .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.id, "component-aux")) .get(), snapshot.component ); const undo = history.getNextCommand("project-1", "undo"); assert.ok(undo); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: undo.changeSetId, command: inserted.inverse, }); assert.equal( context.db .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.id, "component-aux")) .get(), undefined ); const redo = history.getNextCommand("project-1", "redo"); assert.ok(redo); repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: redo.changeSetId, command, }); assert.ok( context.db .select() .from(distributionBoardComponents) .where(eq(distributionBoardComponents.id, "component-aux")) .get() ); } finally { context.close(); } }); it("stores and restores a group RCD with its protection data atomically", () => { const context = createTestDatabase(); try { const { list, section } = getListAndSection(context); const snapshot: DistributionBoardComponentSnapshot = { component: { id: "component-rcd", circuitListId: list.id, sectionId: section.id, equipmentIdentifier: "-1Q1.0", name: "Gruppen-FI", role: "group_residual_current_protection", placement: "group", sortOrder: 10, }, protectionDevice: { componentId: "component-rcd", type: "FI", ratedCurrentA: 40, fuseUtilizationCategory: null, tripCharacteristic: null, rcdType: "A", ratedResidualCurrentMa: 30, }, }; const repository = new DistributionBoardComponentStructureProjectCommandRepository( context.db ); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardComponentInsertProjectCommand( snapshot ), }); assert.deepEqual( context.db .select() .from(distributionBoardComponentProtectionDevices) .get(), snapshot.protectionDevice ); repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: new ProjectHistoryRepository( context.db ).getNextCommand("project-1", "undo")?.changeSetId, command: inserted.inverse, }); assert.equal( context.db .select() .from(distributionBoardComponentProtectionDevices) .get(), undefined ); } finally { context.close(); } }); it("rejects fixed roles, foreign sections, duplicate BMKs and stale revisions", () => { const context = createTestDatabase(); try { const { list } = getListAndSection(context); assert.throws( () => createDistributionBoardComponentInsertProjectCommand({ component: { id: "fixed", circuitListId: list.id, sectionId: null, equipmentIdentifier: "-Q1", name: "Hauptschalter", role: "main_switch", placement: "header", sortOrder: 10, } as never, protectionDevice: null, }), /Fixed distribution-board components/ ); const snapshot: DistributionBoardComponentSnapshot = { component: { id: "duplicate", circuitListId: list.id, sectionId: null, equipmentIdentifier: " -q0 ", name: "Doppelt", role: "auxiliary", placement: "footer", sortOrder: 10, }, protectionDevice: null, }; const repository = new DistributionBoardComponentStructureProjectCommandRepository( context.db ); const foreignBoard = new DistributionBoardFixtureRepository( context.db ).createWithCircuitListAndDefaultSections( "project-2", "UV fremd" ); const foreignSection = context.db .select() .from(circuitSections) .where( eq(circuitSections.circuitListId, foreignBoard.id) ) .get(); assert.ok(foreignSection); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardComponentInsertProjectCommand({ component: { id: "foreign-section", circuitListId: list.id, sectionId: foreignSection.id, equipmentIdentifier: "-1Q2.0", name: "Fremder Gruppen-FI", role: "group_residual_current_protection", placement: "group", sortOrder: 10, }, protectionDevice: { componentId: "foreign-section", type: "FI", ratedCurrentA: 40, fuseUtilizationCategory: null, tripCharacteristic: null, rcdType: "A", ratedResidualCurrentMa: 30, }, }), }), /section does not belong/ ); const staleSnapshot: DistributionBoardComponentSnapshot = { component: { ...snapshot.component, id: "stale", equipmentIdentifier: "-K-stale", }, protectionDevice: null, }; assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createDistributionBoardComponentInsertProjectCommand( staleSnapshot ), }), ProjectRevisionConflictError ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardComponentInsertProjectCommand( snapshot ), }), /UNIQUE constraint failed/ ); assert.equal( context.db.select().from(projectRevisions).all().length, 0 ); } finally { context.close(); } }); it("rejects changed deletes and rolls back late history failures", () => { const context = createTestDatabase(); try { const { list } = getListAndSection(context); const snapshot: DistributionBoardComponentSnapshot = { component: { id: "component-change", circuitListId: list.id, sectionId: null, equipmentIdentifier: "-K2", name: "Aktor", role: "auxiliary", placement: "footer", sortOrder: 20, }, protectionDevice: null, }; const repository = new DistributionBoardComponentStructureProjectCommandRepository( context.db ); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardComponentInsertProjectCommand( snapshot ), }); context.db .update(distributionBoardComponents) .set({ name: "Direkt geƤndert" }) .where(eq(distributionBoardComponents.id, snapshot.component.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/ ); } finally { context.close(); } const rollback = createTestDatabase(); try { const { list } = getListAndSection(rollback); rollback.sqlite.exec(` CREATE TRIGGER fail_component_history BEFORE INSERT ON project_history_stack_entries BEGIN SELECT RAISE(ABORT, 'forced component history failure'); END; `); const repository = new DistributionBoardComponentStructureProjectCommandRepository( rollback.db ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createDistributionBoardComponentInsertProjectCommand({ component: { id: "component-rollback", circuitListId: list.id, sectionId: null, equipmentIdentifier: "-K3", name: "Aktor", role: "auxiliary", placement: "footer", sortOrder: 30, }, protectionDevice: null, }), }), /forced component history failure/ ); assert.equal( rollback.db .select() .from(distributionBoardComponents) .where( eq( distributionBoardComponents.id, "component-rollback" ) ) .get(), undefined ); } finally { rollback.close(); } }); });