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

446 lines
12 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 { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-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 { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { projectDevices } from "../src/db/schema/project-devices.js";
import { projectRevisions } from "../src/db/schema/project-revisions.js";
import { projects } from "../src/db/schema/projects.js";
import { rooms } from "../src/db/schema/rooms.js";
import {
createCircuitDeviceRowDeleteProjectCommand,
createCircuitDeviceRowInsertProjectCommand,
type CircuitDeviceRowSnapshot,
} from "../src/domain/models/circuit-device-row-structure-project-command.model.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" },
{ 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-reserve",
circuitListId: board.id,
sectionId: section.id,
equipmentIdentifier: "-1F2",
sortOrder: 20,
isReserve: 1,
},
{
id: "circuit-foreign",
circuitListId: foreignBoard.id,
sectionId: foreignSection.id,
equipmentIdentifier: "-1F1",
sortOrder: 10,
isReserve: 1,
},
])
.run();
context.db
.insert(projectDevices)
.values([
{
id: "device-1",
projectId: "project-1",
name: "Leuchte",
displayName: "Leuchte",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
},
{
id: "device-foreign",
projectId: "project-2",
name: "Fremdgerät",
displayName: "Fremdgerät",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
},
])
.run();
context.db
.insert(rooms)
.values([
{
id: "room-1",
projectId: "project-1",
roomNumber: "001",
roomName: "Büro",
},
{
id: "room-foreign",
projectId: "project-2",
roomNumber: "999",
roomName: "Fremdraum",
},
])
.run();
context.db
.insert(circuitDeviceRows)
.values({
id: "row-1",
circuitId: "circuit-1",
linkedProjectDeviceId: "device-1",
legacyConsumerId: "legacy-1",
sortOrder: 10,
name: "Leuchte",
displayName: "Leuchte lokal",
phaseType: "single_phase",
connectionKind: "fixed",
costGroup: "440",
category: "lighting",
level: "EG",
roomId: "room-1",
roomNumberSnapshot: "001",
roomNameSnapshot: "Büro",
quantity: 2,
powerPerUnit: 0.12,
simultaneityFactor: 0.8,
cosPhi: 0.9,
remark: "Bestand",
overriddenFields: "[\"displayName\"]",
})
.run();
return context;
}
function createInsertSnapshot(
overrides: Partial<CircuitDeviceRowSnapshot> = {}
): CircuitDeviceRowSnapshot {
return {
id: "row-new",
circuitId: "circuit-reserve",
linkedProjectDeviceId: "device-1",
legacyConsumerId: null,
sortOrder: 10,
name: "Steckdose",
displayName: "Steckdose lokal",
phaseType: "single_phase",
connectionKind: "socket",
costGroup: "440",
category: "socket",
level: "EG",
roomId: "room-1",
roomNumberSnapshot: "001",
roomNameSnapshot: "Büro",
quantity: 1,
powerPerUnit: 0.2,
simultaneityFactor: 1,
cosPhi: 0.95,
remark: null,
overriddenFields: null,
...overrides,
};
}
function getIsReserve(context: DatabaseContext, circuitId: string) {
return Boolean(
context.db
.select({ isReserve: circuits.isReserve })
.from(circuits)
.where(eq(circuits.id, circuitId))
.get()?.isReserve
);
}
describe("circuit device-row structure project-command repository", () => {
it("inserts a stable row and supports persisted undo and redo", () => {
const context = createTestDatabase();
try {
const store =
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
const command = createCircuitDeviceRowInsertProjectCommand(
createInsertSnapshot()
);
const inserted = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.equal(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-new"))
.get()?.displayName,
"Steckdose lokal"
);
assert.equal(getIsReserve(context, "circuit-reserve"), false);
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: inserted.revision.changeSetId,
command: inserted.inverse,
});
assert.equal(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-new"))
.get(),
undefined
);
assert.equal(getIsReserve(context, "circuit-reserve"), true);
store.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: inserted.revision.changeSetId,
command,
});
assert.equal(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-new"))
.get()?.id,
"row-new"
);
assert.deepEqual(
new ProjectHistoryRepository(context.db).getState("project-1"),
{
projectId: "project-1",
currentRevision: 3,
undoDepth: 1,
redoDepth: 0,
undoChangeSetId: inserted.revision.changeSetId,
redoChangeSetId: null,
}
);
} finally {
context.close();
}
});
it("deletes and restores the complete persisted row snapshot", () => {
const context = createTestDatabase();
try {
const store =
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
const before = context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-1"))
.get();
assert.ok(before);
const deleted = store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowDeleteProjectCommand(
"row-1",
"circuit-1"
),
});
assert.equal(getIsReserve(context, "circuit-1"), true);
assert.deepEqual(deleted.inverse.payload, { row: before });
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: deleted.revision.changeSetId,
command: deleted.inverse,
});
assert.deepEqual(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-1"))
.get(),
before
);
assert.equal(getIsReserve(context, "circuit-1"), false);
} finally {
context.close();
}
});
it("rolls back insert and delete when late history persistence fails", () => {
const context = createTestDatabase();
try {
context.sqlite.exec(`
CREATE TRIGGER fail_structure_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced structure history failure');
END;
`);
const store =
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowInsertProjectCommand(
createInsertSnapshot()
),
}),
/forced structure history failure/
);
assert.equal(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-new"))
.get(),
undefined
);
assert.equal(getIsReserve(context, "circuit-reserve"), true);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowDeleteProjectCommand(
"row-1",
"circuit-1"
),
}),
/forced structure history failure/
);
assert.ok(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-1"))
.get()
);
assert.equal(getIsReserve(context, "circuit-1"), false);
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
it("rolls back a structural write for a stale expected revision", () => {
const context = createTestDatabase();
try {
const store =
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
assert.throws(
() =>
store.execute({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: createCircuitDeviceRowDeleteProjectCommand(
"row-1",
"circuit-1"
),
}),
/at revision 0, expected 1/
);
assert.ok(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-1"))
.get()
);
assert.equal(getIsReserve(context, "circuit-1"), false);
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
it("rejects foreign ownership, references and duplicate row ids", () => {
const context = createTestDatabase();
try {
const store =
new CircuitDeviceRowStructureProjectCommandRepository(context.db);
for (const snapshot of [
createInsertSnapshot({ circuitId: "circuit-foreign" }),
createInsertSnapshot({
id: "row-foreign-device",
linkedProjectDeviceId: "device-foreign",
}),
createInsertSnapshot({
id: "row-foreign-room",
roomId: "room-foreign",
}),
createInsertSnapshot({ id: "row-1" }),
createInsertSnapshot({
id: "row-legacy",
legacyConsumerId: "legacy-user-value",
}),
]) {
assert.throws(() =>
store.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command: createCircuitDeviceRowInsertProjectCommand(snapshot),
})
);
}
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
} finally {
context.close();
}
});
});