Files
leistungsbilanz-ts/tests/distribution-board-component-structure-project-command.repository.test.ts
T
2026-07-30 19:42:09 +02:00

701 lines
21 KiB
TypeScript

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,
createDistributionBoardComponentUpdateProjectCommand,
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("updates an auxiliary component and restores its name, BMK and order", () => {
const context = createTestDatabase();
try {
const { list } = getListAndSection(context);
const expected: DistributionBoardComponentSnapshot = {
component: {
id: "component-update-aux",
circuitListId: list.id,
sectionId: null,
equipmentIdentifier: "-K20",
name: "KNX Aktor",
role: "auxiliary",
placement: "footer",
sortOrder: 10,
},
protectionDevice: null,
};
const target: DistributionBoardComponentSnapshot = {
component: {
...expected.component,
equipmentIdentifier: "-K21",
name: "KNX Jalousieaktor",
sortOrder: 30,
},
protectionDevice: null,
};
const repository =
new DistributionBoardComponentStructureProjectCommandRepository(
context.db
);
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardComponentInsertProjectCommand(
expected
),
});
const updated = repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command:
createDistributionBoardComponentUpdateProjectCommand(
expected,
target
),
});
assert.deepEqual(
context.db
.select()
.from(distributionBoardComponents)
.where(eq(distributionBoardComponents.id, expected.component.id))
.get(),
target.component
);
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "undo",
historyTargetChangeSetId: new ProjectHistoryRepository(
context.db
).getNextCommand("project-1", "undo")?.changeSetId,
command: updated.inverse,
});
assert.deepEqual(
context.db
.select()
.from(distributionBoardComponents)
.where(eq(distributionBoardComponents.id, expected.component.id))
.get(),
expected.component
);
} finally {
context.close();
}
});
it("updates group protection data atomically and rejects stale state", () => {
const context = createTestDatabase();
try {
const { list, section } = getListAndSection(context);
const expected: DistributionBoardComponentSnapshot = {
component: {
id: "component-update-protection",
circuitListId: list.id,
sectionId: section.id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection",
placement: "group",
sortOrder: 10,
},
protectionDevice: {
componentId: "component-update-protection",
type: "FI",
ratedCurrentA: 40,
fuseUtilizationCategory: null,
tripCharacteristic: null,
rcdType: "A",
ratedResidualCurrentMa: 30,
},
};
const target: DistributionBoardComponentSnapshot = {
component: {
...expected.component,
name: "Gruppen-FI 63 A",
},
protectionDevice: {
...expected.protectionDevice!,
ratedCurrentA: 63,
},
};
const repository =
new DistributionBoardComponentStructureProjectCommandRepository(
context.db
);
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardComponentInsertProjectCommand(
expected
),
});
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command:
createDistributionBoardComponentUpdateProjectCommand(
expected,
target
),
});
assert.deepEqual(
context.db
.select()
.from(distributionBoardComponentProtectionDevices)
.where(
eq(
distributionBoardComponentProtectionDevices.componentId,
expected.component.id
)
)
.get(),
target.protectionDevice
);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "user",
command:
createDistributionBoardComponentUpdateProjectCommand(
expected,
target
),
}),
/changed before update/
);
assert.equal(
context.db.select().from(projectRevisions).all().length,
2
);
assert.throws(
() =>
createDistributionBoardComponentUpdateProjectCommand(
target,
{
...target,
component: {
...target.component,
sectionId: "another-section",
},
}
),
/cannot change ownership or role/
);
} finally {
context.close();
}
});
it("rolls back a component update when history persistence fails", () => {
const context = createTestDatabase();
try {
const { list } = getListAndSection(context);
const expected: DistributionBoardComponentSnapshot = {
component: {
id: "component-update-rollback",
circuitListId: list.id,
sectionId: null,
equipmentIdentifier: "-K30",
name: "Aktor",
role: "auxiliary",
placement: "footer",
sortOrder: 10,
},
protectionDevice: null,
};
const target: DistributionBoardComponentSnapshot = {
component: {
...expected.component,
name: "Geänderter Aktor",
sortOrder: 20,
},
protectionDevice: null,
};
const repository =
new DistributionBoardComponentStructureProjectCommandRepository(
context.db
);
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardComponentInsertProjectCommand(
expected
),
});
context.sqlite.exec(`
CREATE TRIGGER fail_component_update_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced component update history failure');
END;
`);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command:
createDistributionBoardComponentUpdateProjectCommand(
expected,
target
),
}),
/forced component update history failure/
);
assert.deepEqual(
context.db
.select()
.from(distributionBoardComponents)
.where(eq(distributionBoardComponents.id, expected.component.id))
.get(),
expected.component
);
assert.equal(
context.db.select().from(projectRevisions).all().length,
1
);
} 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();
}
});
});