Files
leistungsbilanz-ts/tests/circuit-device-row-transaction.repository.test.ts
T
2026-07-23 18:52:40 +02:00

530 lines
16 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 { CircuitDeviceRowTransactionRepository } from "../src/db/repositories/circuit-device-row-transaction.repository.js";
import { DistributionBoardRepository } from "../src/db/repositories/distribution-board.repository.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { projects } from "../src/db/schema/projects.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" }).run();
const board = new DistributionBoardRepository(
context.db
).createWithCircuitListAndDefaultSections(
"project-1",
"UV-01"
);
const [section] = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, board.id))
.limit(1)
.all();
context.db
.insert(circuits)
.values({
id: "circuit-1",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F1",
displayName: "Reserve",
sortOrder: 10,
isReserve: 1,
})
.run();
return context;
}
function insertCircuit(
context: DatabaseContext,
input: {
id: string;
equipmentIdentifier: string;
sortOrder: number;
isReserve?: number;
}
) {
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
context.db
.insert(circuits)
.values({
id: input.id,
circuitListId: seedCircuit.circuitListId,
sectionId: seedCircuit.sectionId,
equipmentIdentifier: input.equipmentIdentifier,
displayName: input.equipmentIdentifier,
sortOrder: input.sortOrder,
isReserve: input.isReserve ?? 1,
})
.run();
}
function insertDeviceRow(
context: DatabaseContext,
input: {
id?: string;
circuitId?: string;
sortOrder?: number;
displayName?: string;
} = {}
) {
const rowId = input.id ?? "row-1";
const circuitId = input.circuitId ?? "circuit-1";
context.db
.insert(circuitDeviceRows)
.values({
id: rowId,
circuitId,
sortOrder: input.sortOrder ?? 10,
name: "Leuchte",
displayName: input.displayName ?? "Leuchte",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
})
.run();
context.db
.update(circuits)
.set({ isReserve: 0 })
.where(eq(circuits.id, circuitId))
.run();
}
describe("circuit device-row transaction repository", () => {
it("commits a new device row and clears the circuit reserve status together", () => {
const context = createTestDatabase();
try {
const repository = new CircuitDeviceRowTransactionRepository(context.db);
const rowId = repository.createInCircuit({
circuitId: "circuit-1",
name: "Steckdose",
displayName: "Steckdose",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
});
const [row] = context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, rowId))
.all();
const [circuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-1"))
.all();
assert.equal(row.circuitId, "circuit-1");
assert.equal(row.sortOrder, 10);
assert.equal(circuit.isReserve, 0);
} finally {
context.close();
}
});
it("rolls back the row insert when clearing the reserve status fails", () => {
const context = createTestDatabase();
try {
context.sqlite.exec(`
CREATE TRIGGER fail_reserve_clear
BEFORE UPDATE OF is_reserve ON circuits
WHEN NEW.is_reserve = 0
BEGIN
SELECT RAISE(ABORT, 'forced reserve clear failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() =>
repository.createInCircuit({
circuitId: "circuit-1",
name: "Steckdose",
displayName: "Steckdose",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
}),
/forced reserve clear failure/
);
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
} finally {
context.close();
}
});
it("commits a new circuit and all initial device rows together", () => {
const context = createTestDatabase();
try {
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
const repository = new CircuitDeviceRowTransactionRepository(context.db);
const created = repository.createCircuitWithDeviceRows({
circuit: {
circuitListId: seedCircuit.circuitListId,
sectionId: seedCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Beleuchtung",
sortOrder: 20,
},
deviceRows: [
{
name: "Leuchte",
displayName: "Leuchte 1",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
sortOrder: 15,
},
{
name: "Leuchte",
displayName: "Leuchte 2",
quantity: 2,
powerPerUnit: 0.1,
simultaneityFactor: 0.8,
},
],
});
const [createdCircuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, created.circuitId))
.all();
const createdRows = context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.circuitId, created.circuitId))
.all()
.sort((left, right) => left.sortOrder - right.sortOrder);
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
assert.equal(createdCircuit.isReserve, 0);
assert.deepEqual(
createdRows.map((row) => [row.id, row.displayName, row.sortOrder]),
[
[created.rowIds[0], "Leuchte 1", 15],
[created.rowIds[1], "Leuchte 2", 20],
]
);
} finally {
context.close();
}
});
it("rolls back the circuit and earlier rows when an initial row insert fails", () => {
const context = createTestDatabase();
try {
const [seedCircuit] = context.db.select().from(circuits).limit(1).all();
context.sqlite.exec(`
CREATE TRIGGER fail_initial_device_row
BEFORE INSERT ON circuit_device_rows
WHEN NEW.name = 'Fail'
BEGIN
SELECT RAISE(ABORT, 'forced initial row failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() =>
repository.createCircuitWithDeviceRows({
circuit: {
circuitListId: seedCircuit.circuitListId,
sectionId: seedCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Rollback",
sortOrder: 20,
},
deviceRows: [
{
name: "First",
displayName: "Erste Zeile",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
{
name: "Fail",
displayName: "Fehlerhafte Zeile",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
],
}),
/forced initial row failure/
);
assert.equal(context.db.select().from(circuits).all().length, 1);
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
} finally {
context.close();
}
});
it("commits the last-row deletion and activates the circuit reserve status together", () => {
const context = createTestDatabase();
try {
insertDeviceRow(context);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
repository.deleteFromCircuit("row-1", "circuit-1");
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0);
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 1);
} finally {
context.close();
}
});
it("rolls back the row deletion when activating the reserve status fails", () => {
const context = createTestDatabase();
try {
insertDeviceRow(context);
context.sqlite.exec(`
CREATE TRIGGER fail_reserve_activation
BEFORE UPDATE OF is_reserve ON circuits
WHEN NEW.is_reserve = 1
BEGIN
SELECT RAISE(ABORT, 'forced reserve activation failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() => repository.deleteFromCircuit("row-1", "circuit-1"),
/forced reserve activation failure/
);
assert.equal(context.db.select().from(circuitDeviceRows).all().length, 1);
assert.equal(context.db.select().from(circuits).all()[0].isReserve, 0);
} finally {
context.close();
}
});
it("moves rows to an existing circuit and updates both reserve states together", () => {
const context = createTestDatabase();
try {
insertCircuit(context, {
id: "circuit-2",
equipmentIdentifier: "-1F2",
sortOrder: 20,
});
insertCircuit(context, {
id: "circuit-3",
equipmentIdentifier: "-1F3",
sortOrder: 30,
});
insertDeviceRow(context);
insertDeviceRow(context, {
id: "row-2",
circuitId: "circuit-2",
sortOrder: 10,
displayName: "Zweite Quellzeile",
});
insertDeviceRow(context, {
id: "target-row",
circuitId: "circuit-3",
sortOrder: 10,
displayName: "Bestehende Zielzeile",
});
const repository = new CircuitDeviceRowTransactionRepository(context.db);
const result = repository.moveRows({
rows: [
{ id: "row-1", expectedCircuitId: "circuit-1" },
{ id: "row-2", expectedCircuitId: "circuit-2" },
],
targetCircuitId: "circuit-3",
});
const movedRows = context.db
.select()
.from(circuitDeviceRows)
.all()
.filter((row) => row.id === "row-1" || row.id === "row-2")
.sort((left, right) => left.sortOrder - right.sortOrder);
const persistedCircuits = context.db.select().from(circuits).all();
const targetCircuit = persistedCircuits.find(
(circuit) => circuit.id === "circuit-3"
);
assert.deepEqual(result, {
movedRowIds: ["row-1", "row-2"],
targetCircuitId: "circuit-3",
createdCircuitId: undefined,
});
assert.deepEqual(
movedRows.map((row) => [row.id, row.circuitId, row.sortOrder]),
[
["row-1", "circuit-3", 20],
["row-2", "circuit-3", 30],
]
);
assert.equal(
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
1
);
assert.equal(
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
1
);
assert.equal(targetCircuit?.isReserve, 0);
} finally {
context.close();
}
});
it("rolls back an existing-target move when a reserve update fails", () => {
const context = createTestDatabase();
try {
insertCircuit(context, {
id: "circuit-2",
equipmentIdentifier: "-1F2",
sortOrder: 20,
});
insertDeviceRow(context);
context.sqlite.exec(`
CREATE TRIGGER fail_source_reserve_update
BEFORE UPDATE OF is_reserve ON circuits
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
BEGIN
SELECT RAISE(ABORT, 'forced source reserve failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() =>
repository.moveRows({
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
targetCircuitId: "circuit-2",
}),
/forced source reserve failure/
);
const [row] = context.db.select().from(circuitDeviceRows).all();
const persistedCircuits = context.db.select().from(circuits).all();
assert.equal(row.circuitId, "circuit-1");
assert.equal(
persistedCircuits.find((circuit) => circuit.id === "circuit-1")?.isReserve,
0
);
assert.equal(
persistedCircuits.find((circuit) => circuit.id === "circuit-2")?.isReserve,
1
);
} finally {
context.close();
}
});
it("creates a new target circuit and moves rows into it atomically", () => {
const context = createTestDatabase();
try {
insertDeviceRow(context);
const [sourceCircuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-1"))
.all();
const repository = new CircuitDeviceRowTransactionRepository(context.db);
const result = repository.moveRows({
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
createTargetCircuit: {
circuitListId: sourceCircuit.circuitListId,
sectionId: sourceCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Neues Ziel",
sortOrder: 20,
},
});
const [movedRow] = context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-1"))
.all();
const [createdCircuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, result.createdCircuitId!))
.all();
const [updatedSource] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-1"))
.all();
assert.equal(result.targetCircuitId, result.createdCircuitId);
assert.equal(createdCircuit.equipmentIdentifier, "-1F2");
assert.equal(createdCircuit.isReserve, 0);
assert.equal(movedRow.circuitId, createdCircuit.id);
assert.equal(updatedSource.isReserve, 1);
} finally {
context.close();
}
});
it("rolls back a newly created target circuit when the move fails", () => {
const context = createTestDatabase();
try {
insertDeviceRow(context);
const [sourceCircuit] = context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-1"))
.all();
context.sqlite.exec(`
CREATE TRIGGER fail_new_target_source_reserve
BEFORE UPDATE OF is_reserve ON circuits
WHEN NEW.id = 'circuit-1' AND NEW.is_reserve = 1
BEGIN
SELECT RAISE(ABORT, 'forced new-target move failure');
END;
`);
const repository = new CircuitDeviceRowTransactionRepository(context.db);
assert.throws(
() =>
repository.moveRows({
rows: [{ id: "row-1", expectedCircuitId: "circuit-1" }],
createTargetCircuit: {
circuitListId: sourceCircuit.circuitListId,
sectionId: sourceCircuit.sectionId,
equipmentIdentifier: "-1F2",
displayName: "Rollback-Ziel",
sortOrder: 20,
},
}),
/forced new-target move failure/
);
const [row] = context.db.select().from(circuitDeviceRows).all();
const persistedCircuits = context.db.select().from(circuits).all();
assert.equal(row.circuitId, "circuit-1");
assert.equal(persistedCircuits.length, 1);
assert.equal(persistedCircuits[0].isReserve, 0);
} finally {
context.close();
}
});
});