leistungsbilanz-ts/tests/distribution-board-subtree-project-command.repository.test.ts

363 lines
12 KiB
TypeScript
Raw Permalink 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 { DistributionBoardSubtreeProjectCommandRepository } from "../src/db/repositories/distribution-board-subtree-project-command.repository.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 { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js";
import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.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 {
createDistributionBoardDeleteSubtreeProjectCommand,
createDistributionBoardInsertSubtreeProjectCommand,
} from "../src/domain/models/distribution-board-subtree-project-command.model.js";
import { cloneDistributionBoardSubtree } from "../src/domain/models/distribution-board-subtree-snapshot.model.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: "Test project" })
.run();
return context;
}
function createPopulatedBoard(context: DatabaseContext) {
const board = new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "UV 1");
const section = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, board.id))
.get();
assert.ok(section);
context.db
.insert(distributionBoardComponents)
.values({
id: "group-rcd",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection",
placement: "group",
sortOrder: 30,
})
.run();
context.db
.insert(distributionBoardComponentProtectionDevices)
.values({
componentId: "group-rcd",
type: "FI",
ratedCurrentA: 40,
rcdType: "A",
ratedResidualCurrentMa: 30,
})
.run();
context.db
.insert(circuits)
.values({
id: "circuit-1",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F1.1",
displayName: "Licht",
sortOrder: 10,
voltage: 230,
})
.run();
context.db
.insert(circuitProtectionDevices)
.values({
circuitId: "circuit-1",
type: "LS",
ratedCurrentA: 10,
tripCharacteristic: "B",
})
.run();
context.db
.insert(circuitDeviceRows)
.values({
id: "row-1",
circuitId: "circuit-1",
sortOrder: 10,
name: "Leuchte",
displayName: "Leuchte",
phaseType: "single_phase",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
})
.run();
return board;
}
describe("distribution-board subtree project command", () => {
it("copies and deletes populated boards through persistent undo and redo", () => {
const context = createTestDatabase();
try {
const source = createPopulatedBoard(context);
const repository =
new DistributionBoardSubtreeProjectCommandRepository(context.db);
const history = new ProjectHistoryRepository(context.db);
const sourceSnapshot = repository.capture("project-1", source.id);
const clone = cloneDistributionBoardSubtree(
sourceSnapshot,
"UV 1 Kopie"
);
const inserted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
description: "Verteilung kopieren",
command: createDistributionBoardInsertSubtreeProjectCommand(clone),
});
assert.deepEqual(
repository.capture("project-1", clone.distributionBoard.id),
clone
);
const undoInsert = history.getNextCommand("project-1", "undo");
assert.ok(undoInsert);
const removedClone = repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: undoInsert.changeSetId,
command: inserted.inverse,
});
assert.throws(
() => repository.capture("project-1", clone.distributionBoard.id),
/not found/
);
const redoInsert = history.getNextCommand("project-1", "redo");
assert.ok(redoInsert);
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: redoInsert.changeSetId,
command: removedClone.inverse,
});
assert.deepEqual(
repository.capture("project-1", clone.distributionBoard.id),
clone
);
const deleted = repository.execute({
projectId: "project-1",
expectedRevision: 3,
source: "user",
description: "Verteilung löschen",
command:
createDistributionBoardDeleteSubtreeProjectCommand(sourceSnapshot),
});
assert.throws(
() => repository.capture("project-1", source.id),
/not found/
);
const undoDelete = history.getNextCommand("project-1", "undo");
assert.ok(undoDelete);
repository.execute({
projectId: "project-1",
expectedRevision: 4,
source: "undo",
historyTargetChangeSetId: undoDelete.changeSetId,
command: deleted.inverse,
});
assert.deepEqual(
repository.capture("project-1", source.id),
sourceSnapshot
);
} finally {
context.close();
}
});
it("copies board info values and accepts snapshots stored before they existed", () => {
const context = createTestDatabase();
try {
const board = createPopulatedBoard(context);
context.db
.update(distributionBoards)
.set({ boardNumber: "UV 1.1", location: "Technik" })
.where(eq(distributionBoards.id, board.id))
.run();
const repository =
new DistributionBoardSubtreeProjectCommandRepository(context.db);
const withInfo = repository.capture("project-1", board.id);
const clone = cloneDistributionBoardSubtree(withInfo, "UV 1 Kopie");
assert.equal(clone.distributionBoard.boardNumber, "UV 1.1");
assert.equal(clone.distributionBoard.location, "Technik");
context.db
.update(distributionBoards)
.set({ boardNumber: null, location: null })
.where(eq(distributionBoards.id, board.id))
.run();
const legacySnapshot = structuredClone(
repository.capture("project-1", board.id)
);
const legacyBoard = legacySnapshot.distributionBoard as Record<
string,
unknown
>;
delete legacyBoard.boardNumber;
delete legacyBoard.location;
delete legacyBoard.fedFrom;
delete legacyBoard.remark;
const deleted = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
description: "Verteilung löschen",
command: createDistributionBoardDeleteSubtreeProjectCommand(
legacySnapshot
),
});
assert.throws(
() => repository.capture("project-1", board.id),
/not found/
);
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: deleted.inverse,
});
assert.equal(
repository.capture("project-1", board.id).distributionBoard.fedFrom,
null
);
} finally {
context.close();
}
});
it("rejects changed deletion snapshots without partial writes", () => {
const context = createTestDatabase();
try {
const board = createPopulatedBoard(context);
const repository =
new DistributionBoardSubtreeProjectCommandRepository(context.db);
const snapshot = repository.capture("project-1", board.id);
context.db
.update(distributionBoards)
.set({ name: "Extern geändert" })
.where(eq(distributionBoards.id, board.id))
.run();
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
}),
/changed before deletion/
);
assert.equal(
context.db
.select({ name: distributionBoards.name })
.from(distributionBoards)
.where(eq(distributionBoards.id, board.id))
.get()?.name,
"Extern geändert"
);
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
it("blocks deleting a board subtree that feeds another board", () => {
const context = createTestDatabase();
try {
const upstream = createPopulatedBoard(context);
const downstream = new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "Unterverteiler");
context.db
.update(distributionBoards)
.set({
boardNumber: "UV-03",
feedingDistributionBoardId: upstream.id,
})
.where(eq(distributionBoards.id, downstream.id))
.run();
const repository =
new DistributionBoardSubtreeProjectCommandRepository(context.db);
const snapshot = repository.capture("project-1", upstream.id);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
}),
/Speist UV-03 – erst Einspeisung ändern\./
);
assert.deepEqual(repository.capture("project-1", upstream.id), snapshot);
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
it("rolls back a cascaded deletion when history persistence fails", () => {
const context = createTestDatabase();
try {
const board = createPopulatedBoard(context);
const repository =
new DistributionBoardSubtreeProjectCommandRepository(context.db);
const snapshot = repository.capture("project-1", board.id);
context.sqlite.exec(`
CREATE TRIGGER fail_distribution_board_subtree_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced distribution-board subtree history failure');
END;
`);
assert.throws(
() =>
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardDeleteSubtreeProjectCommand(snapshot),
}),
/forced distribution-board subtree history failure/
);
assert.deepEqual(repository.capture("project-1", board.id), snapshot);
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
});