leistungsbilanz-ts/tests/circuit-device-row-move-project-command.repository.test.ts

797 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.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 {
createCircuitDeviceRowMoveProjectCommand,
createCircuitDeviceRowMoveWithNewCircuitProjectCommand,
} from "../src/domain/models/circuit-device-row-move-project-command.model.js";
import type { CircuitSnapshot } from "../src/domain/models/circuit-structure-project-command.model.js";
interface TestFixture {
context: DatabaseContext;
circuitListId: string;
sectionId: string;
}
function createTestDatabase(): TestFixture {
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: "Other project" },
])
.run();
const boards = new DistributionBoardFixtureRepository(context.db);
const board = boards.createWithCircuitListAndDefaultSections(
"project-1",
"UV-01"
);
const foreignBoard = boards.createWithCircuitListAndDefaultSections(
"project-2",
"UV-02"
);
const section = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, board.id))
.get();
const foreignSection = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, foreignBoard.id))
.get();
assert.ok(section);
assert.ok(foreignSection);
context.db
.insert(circuits)
.values([
{
id: "circuit-1",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F1",
sortOrder: 10,
isReserve: 0,
},
{
id: "circuit-2",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F2",
sortOrder: 20,
isReserve: 0,
},
{
id: "circuit-3",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F3",
sortOrder: 30,
isReserve: 0,
},
{
id: "circuit-foreign",
circuitListId: foreignBoard.id,
sectionId: foreignSection.id,
equipmentIdentifier: "-1F1",
sortOrder: 10,
isReserve: 1,
},
])
.run();
context.db
.insert(circuitDeviceRows)
.values([
{
id: "row-1",
circuitId: "circuit-1",
sortOrder: 10,
name: "Quelle 1",
displayName: "Quelle 1",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
{
id: "row-retained",
circuitId: "circuit-1",
sortOrder: 20,
name: "Bleibt",
displayName: "Bleibt",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
{
id: "row-2",
circuitId: "circuit-2",
sortOrder: 10,
name: "Quelle 2",
displayName: "Quelle 2",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
},
{
id: "row-target",
circuitId: "circuit-3",
sortOrder: 10,
name: "Ziel",
displayName: "Ziel",
quantity: 1,
powerPerUnit: 0.3,
simultaneityFactor: 1,
},
])
.run();
return {
context,
circuitListId: board.id,
sectionId: section.id,
};
}
function getRow(context: DatabaseContext, rowId: string) {
return context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.get();
}
function isReserve(context: DatabaseContext, circuitId: string) {
return Boolean(
context.db
.select({ isReserve: circuits.isReserve })
.from(circuits)
.where(eq(circuits.id, circuitId))
.get()?.isReserve
);
}
function createMoveTargetSnapshot(
fixture: TestFixture,
overrides: Partial<CircuitSnapshot> = {}
): CircuitSnapshot {
return {
id: "circuit-new",
circuitListId: fixture.circuitListId,
sectionId: fixture.sectionId,
equipmentIdentifier: "-1F4",
displayName: "Neuer Stromkreis",
sortOrder: 40,
cableType: null,
cableCrossSection: null,
cableLength: null,
rcdAssignment: null,
terminalDesignation: null,
voltage: null,
controlRequirement: null,
status: null,
isReserve: true,
remark: null,
deviceRows: [],
protectionDevice: {
circuitId: "circuit-new",
type: "LS",
ratedCurrentA: 10,
fuseUtilizationCategory: null,
tripCharacteristic: "B",
rcdType: null,
ratedResidualCurrentMa: null,
},
...overrides,
};
}
describe("circuit device-row move project-command repository", () => {
it("moves multiple rows and supports persisted undo and redo", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
const command = createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-1",
expectedCircuitId: "circuit-1",
expectedSortOrder: 10,
targetCircuitId: "circuit-3",
targetSortOrder: 20,
},
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: "circuit-3",
targetSortOrder: 30,
},
]);
const moved = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.deepEqual(
[
getRow(fixture.context, "row-1"),
getRow(fixture.context, "row-2"),
].map((row) => [row?.circuitId, row?.sortOrder]),
[
["circuit-3", 20],
["circuit-3", 30],
]
);
assert.equal(isReserve(fixture.context, "circuit-1"), false);
assert.equal(isReserve(fixture.context, "circuit-2"), true);
assert.equal(isReserve(fixture.context, "circuit-3"), false);
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: moved.revision.changeSetId,
command: moved.inverse,
});
assert.deepEqual(
[
getRow(fixture.context, "row-1"),
getRow(fixture.context, "row-2"),
].map((row) => [row?.circuitId, row?.sortOrder]),
[
["circuit-1", 10],
["circuit-2", 10],
]
);
assert.equal(isReserve(fixture.context, "circuit-1"), false);
assert.equal(isReserve(fixture.context, "circuit-2"), false);
store.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: moved.revision.changeSetId,
command,
});
assert.deepEqual(
new ProjectHistoryRepository(fixture.context.db).getState(
"project-1"
),
{
projectId: "project-1",
currentRevision: 3,
undoDepth: 1,
redoDepth: 0,
undoChangeSetId: moved.revision.changeSetId,
redoChangeSetId: null,
}
);
} finally {
fixture.context.close();
}
});
it("reorders a row inside its current circuit", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
const moved = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-retained",
expectedCircuitId: "circuit-1",
expectedSortOrder: 20,
targetCircuitId: "circuit-1",
targetSortOrder: 5,
},
]),
});
assert.equal(
getRow(fixture.context, "row-retained")?.sortOrder,
5
);
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: moved.revision.changeSetId,
command: moved.inverse,
});
assert.equal(
getRow(fixture.context, "row-retained")?.sortOrder,
20
);
} finally {
fixture.context.close();
}
});
it("rejects stale row positions and cross-project targets", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-1",
expectedCircuitId: "circuit-1",
expectedSortOrder: 999,
targetCircuitId: "circuit-3",
targetSortOrder: 20,
},
]),
}),
/changed before move/
);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-1",
expectedCircuitId: "circuit-1",
expectedSortOrder: 10,
targetCircuitId: "circuit-foreign",
targetSortOrder: 10,
},
]),
}),
/does not belong to project/
);
assert.deepEqual(
[
getRow(fixture.context, "row-1")?.circuitId,
getRow(fixture.context, "row-1")?.sortOrder,
],
["circuit-1", 10]
);
assert.equal(
fixture.context.db.select().from(projectRevisions).all().length,
0
);
} finally {
fixture.context.close();
}
});
it("rolls back rows and reserve states for late history failures", () => {
const fixture = createTestDatabase();
try {
fixture.context.sqlite.exec(`
CREATE TRIGGER fail_move_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced move history failure');
END;
`);
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: "circuit-3",
targetSortOrder: 20,
},
]),
}),
/forced move history failure/
);
assert.deepEqual(
[
getRow(fixture.context, "row-2")?.circuitId,
getRow(fixture.context, "row-2")?.sortOrder,
],
["circuit-2", 10]
);
assert.equal(isReserve(fixture.context, "circuit-2"), false);
assert.equal(isReserve(fixture.context, "circuit-3"), false);
assert.equal(
fixture.context.db.select().from(projectRevisions).all().length,
0
);
} finally {
fixture.context.close();
}
});
it("rolls back a move for a stale project revision", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: createCircuitDeviceRowMoveProjectCommand([
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: "circuit-3",
targetSortOrder: 20,
},
]),
}),
/at revision 0, expected 1/
);
assert.equal(
getRow(fixture.context, "row-2")?.circuitId,
"circuit-2"
);
assert.equal(isReserve(fixture.context, "circuit-2"), false);
} finally {
fixture.context.close();
}
});
it("creates one target circuit and supports persisted undo and redo", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
const targetCircuit = createMoveTargetSnapshot(fixture);
const command =
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"create",
targetCircuit,
[
{
rowId: "row-1",
expectedCircuitId: "circuit-1",
expectedSortOrder: 10,
targetCircuitId: targetCircuit.id,
targetSortOrder: 10,
},
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: targetCircuit.id,
targetSortOrder: 20,
},
]
);
const moved = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.deepEqual(
[
getRow(fixture.context, "row-1"),
getRow(fixture.context, "row-2"),
].map((row) => [row?.circuitId, row?.sortOrder]),
[
["circuit-new", 10],
["circuit-new", 20],
]
);
assert.equal(
fixture.context.db
.select({ equipmentIdentifier: circuits.equipmentIdentifier })
.from(circuits)
.where(eq(circuits.id, "circuit-new"))
.get()?.equipmentIdentifier,
"-1F4"
);
assert.equal(isReserve(fixture.context, "circuit-new"), false);
assert.equal(
fixture.context.db
.select({ ratedCurrentA: circuitProtectionDevices.ratedCurrentA })
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
.get()?.ratedCurrentA,
10
);
assert.equal(isReserve(fixture.context, "circuit-1"), false);
assert.equal(isReserve(fixture.context, "circuit-2"), true);
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: moved.revision.changeSetId,
command: moved.inverse,
});
assert.equal(
fixture.context.db
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, "circuit-new"))
.get(),
undefined
);
assert.equal(
fixture.context.db
.select()
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
.get(),
undefined
);
assert.deepEqual(
[
getRow(fixture.context, "row-1"),
getRow(fixture.context, "row-2"),
].map((row) => [row?.circuitId, row?.sortOrder]),
[
["circuit-1", 10],
["circuit-2", 10],
]
);
assert.equal(isReserve(fixture.context, "circuit-1"), false);
assert.equal(isReserve(fixture.context, "circuit-2"), false);
store.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: moved.revision.changeSetId,
command,
});
assert.equal(
getRow(fixture.context, "row-2")?.circuitId,
"circuit-new"
);
assert.equal(
fixture.context.db
.select({ type: circuitProtectionDevices.type })
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, "circuit-new"))
.get()?.type,
"LS"
);
assert.deepEqual(
new ProjectHistoryRepository(fixture.context.db).getState(
"project-1"
),
{
projectId: "project-1",
currentRevision: 3,
undoDepth: 1,
redoDepth: 0,
undoChangeSetId: moved.revision.changeSetId,
redoChangeSetId: null,
}
);
} finally {
fixture.context.close();
}
});
it("blocks undo from removing a generated circuit used as a feeder", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
const targetCircuit = createMoveTargetSnapshot(fixture);
const command = createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"create",
targetCircuit,
[{
rowId: "row-1",
expectedCircuitId: "circuit-1",
expectedSortOrder: 10,
targetCircuitId: targetCircuit.id,
targetSortOrder: 10,
}]
);
const moved = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
const downstream = new DistributionBoardFixtureRepository(
fixture.context.db
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
fixture.context.db
.update(distributionBoards)
.set({
feedingDistributionBoardId: fixture.circuitListId,
feedingCircuitId: targetCircuit.id,
})
.where(eq(distributionBoards.id, downstream.id))
.run();
assert.throws(
() => store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: moved.revision.changeSetId,
command: moved.inverse,
}),
/Speist UV-02 – erst Einspeisung ändern\./
);
assert.equal(getRow(fixture.context, "row-1")?.circuitId, targetCircuit.id);
assert.ok(
fixture.context.db.select().from(circuits)
.where(eq(circuits.id, targetCircuit.id)).get()
);
} finally {
fixture.context.close();
}
});
it("rejects deletion when the created target changed outside history", () => {
const fixture = createTestDatabase();
try {
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
const targetCircuit = createMoveTargetSnapshot(fixture);
const moved = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"create",
targetCircuit,
[
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: targetCircuit.id,
targetSortOrder: 10,
},
]
),
});
fixture.context.db
.update(circuits)
.set({ displayName: "Außerhalb geändert" })
.where(eq(circuits.id, targetCircuit.id))
.run();
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId:
moved.revision.changeSetId,
command: moved.inverse,
}),
/target circuit changed/
);
assert.equal(
getRow(fixture.context, "row-2")?.circuitId,
targetCircuit.id
);
assert.ok(
fixture.context.db
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, targetCircuit.id))
.get()
);
assert.equal(
new ProjectHistoryRepository(fixture.context.db).getState(
"project-1"
)?.currentRevision,
1
);
} finally {
fixture.context.close();
}
});
it("rolls back target creation and moves for late history failures", () => {
const fixture = createTestDatabase();
try {
fixture.context.sqlite.exec(`
CREATE TRIGGER fail_composite_move_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced composite move history failure');
END;
`);
const targetCircuit = createMoveTargetSnapshot(fixture);
const store = new CircuitDeviceRowMoveProjectCommandRepository(
fixture.context.db
);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createCircuitDeviceRowMoveWithNewCircuitProjectCommand(
"create",
targetCircuit,
[
{
rowId: "row-2",
expectedCircuitId: "circuit-2",
expectedSortOrder: 10,
targetCircuitId: targetCircuit.id,
targetSortOrder: 10,
},
]
),
}),
/forced composite move history failure/
);
assert.equal(
fixture.context.db
.select({ id: circuits.id })
.from(circuits)
.where(eq(circuits.id, targetCircuit.id))
.get(),
undefined
);
assert.equal(
getRow(fixture.context, "row-2")?.circuitId,
"circuit-2"
);
assert.equal(isReserve(fixture.context, "circuit-2"), false);
assert.equal(
fixture.context.db.select().from(projectRevisions).all()
.length,
0
);
} finally {
fixture.context.close();
}
});
});