554 lines
14 KiB
TypeScript
554 lines
14 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
deserializeProjectStateSnapshot,
|
|
parseProjectStateSnapshot,
|
|
projectStateSnapshotSchemaVersion,
|
|
serializeProjectStateSnapshot,
|
|
} from "../src/domain/models/project-state-snapshot.model.js";
|
|
import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
|
|
|
function minimalSnapshot() {
|
|
return {
|
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
|
project: {
|
|
id: "project-1",
|
|
name: "Projekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
},
|
|
distributionBoards: [],
|
|
circuitLists: [],
|
|
circuitSections: [],
|
|
distributionBoardComponents: [],
|
|
circuitProtectionDevices: [],
|
|
distributionBoardComponentProtectionDevices: [],
|
|
circuits: [],
|
|
projectDevices: [],
|
|
floors: [],
|
|
rooms: [],
|
|
};
|
|
}
|
|
|
|
function legacyMinimalSnapshot() {
|
|
const {
|
|
distributionBoardComponents: _distributionBoardComponents,
|
|
circuitProtectionDevices: _circuitProtectionDevices,
|
|
distributionBoardComponentProtectionDevices:
|
|
_distributionBoardComponentProtectionDevices,
|
|
...snapshot
|
|
} = minimalSnapshot();
|
|
return snapshot;
|
|
}
|
|
|
|
function protectedSnapshot() {
|
|
return {
|
|
...minimalSnapshot(),
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
},
|
|
],
|
|
circuitLists: [
|
|
{
|
|
id: "list-1",
|
|
projectId: "project-1",
|
|
distributionBoardId: "board-1",
|
|
name: "UV 1 Stromkreisliste",
|
|
},
|
|
],
|
|
circuitSections: [
|
|
{
|
|
id: "section-1",
|
|
circuitListId: "list-1",
|
|
key: "lighting",
|
|
displayName: "Beleuchtung 1",
|
|
prefix: "-1F1.",
|
|
sortOrder: 10,
|
|
category: "lighting" as const,
|
|
groupNumber: 1,
|
|
},
|
|
],
|
|
circuits: [
|
|
{
|
|
id: "circuit-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1F1.1",
|
|
displayName: "Reserve",
|
|
sortOrder: 10,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: 230,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
distributionBoardComponents: [
|
|
{
|
|
id: "component-1",
|
|
circuitListId: "list-1",
|
|
sectionId: "section-1",
|
|
equipmentIdentifier: "-1Q1.0",
|
|
name: "Gruppen-FI",
|
|
role: "group_residual_current_protection" as const,
|
|
placement: "group" as const,
|
|
sortOrder: 10,
|
|
},
|
|
],
|
|
circuitProtectionDevices: [
|
|
{
|
|
circuitId: "circuit-1",
|
|
type: "LS" as const,
|
|
ratedCurrentA: 10,
|
|
fuseUtilizationCategory: null,
|
|
tripCharacteristic: "B" as const,
|
|
rcdType: null,
|
|
ratedResidualCurrentMa: null,
|
|
},
|
|
],
|
|
distributionBoardComponentProtectionDevices: [
|
|
{
|
|
componentId: "component-1",
|
|
type: "FI" as const,
|
|
ratedCurrentA: 40,
|
|
fuseUtilizationCategory: null,
|
|
tripCharacteristic: null,
|
|
rcdType: "A" as const,
|
|
ratedResidualCurrentMa: 30,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("project state snapshot model", () => {
|
|
it("round-trips a versioned logical project state", () => {
|
|
const snapshot = parseProjectStateSnapshot(minimalSnapshot());
|
|
assert.deepEqual(
|
|
deserializeProjectStateSnapshot(
|
|
serializeProjectStateSnapshot(snapshot)
|
|
),
|
|
snapshot
|
|
);
|
|
});
|
|
|
|
it("upgrades version-one snapshots with empty project metadata", () => {
|
|
const legacy = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 1 as const,
|
|
project: {
|
|
id: "project-1",
|
|
name: "Altprojekt",
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
},
|
|
};
|
|
const snapshot = parseProjectStateSnapshot(legacy);
|
|
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.deepEqual(snapshot.project, {
|
|
...legacy.project,
|
|
internalProjectNumber: null,
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
});
|
|
});
|
|
|
|
it("upgrades version-two distribution boards without inferred assignments", () => {
|
|
const previous = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 2 as const,
|
|
project: {
|
|
...minimalSnapshot().project,
|
|
enabledDistributionBoardSupplyTypes: undefined,
|
|
},
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
},
|
|
],
|
|
};
|
|
delete previous.project.enabledDistributionBoardSupplyTypes;
|
|
const snapshot = parseProjectStateSnapshot(previous);
|
|
assert.deepEqual(snapshot.distributionBoards, [
|
|
{
|
|
...previous.distributionBoards[0],
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("upgrades version-three snapshots with all project supply types", () => {
|
|
const previous = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 3 as const,
|
|
project: {
|
|
...minimalSnapshot().project,
|
|
enabledDistributionBoardSupplyTypes: undefined,
|
|
},
|
|
};
|
|
delete previous.project.enabledDistributionBoardSupplyTypes;
|
|
const snapshot = parseProjectStateSnapshot(previous);
|
|
assert.deepEqual(snapshot.project.enabledDistributionBoardSupplyTypes, [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
]);
|
|
});
|
|
|
|
it("normalizes version-four device voltages to project settings", () => {
|
|
const previous = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 4 as const,
|
|
projectDevices: [
|
|
{
|
|
id: "device-1",
|
|
projectId: "project-1",
|
|
name: "Pumpe",
|
|
displayName: "Pumpe",
|
|
phaseType: "three_phase" as const,
|
|
connectionKind: null,
|
|
costGroup: null,
|
|
category: null,
|
|
quantity: 1,
|
|
powerPerUnit: 2,
|
|
simultaneityFactor: 1,
|
|
cosPhi: null,
|
|
remark: null,
|
|
voltageV: 500,
|
|
},
|
|
],
|
|
};
|
|
|
|
const snapshot = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
|
|
});
|
|
|
|
it("upgrades version-five distribution boards with a neutral factor", () => {
|
|
const previous = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 5 as const,
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: null,
|
|
supplyType: "AV" as const,
|
|
},
|
|
],
|
|
};
|
|
|
|
const snapshot = parseProjectStateSnapshot(previous);
|
|
|
|
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
|
|
assert.equal(
|
|
snapshot.distributionBoards[0]?.simultaneityFactor,
|
|
1
|
|
);
|
|
});
|
|
|
|
it("upgrades version-six sections without inventing component data", () => {
|
|
const previous = {
|
|
...legacyMinimalSnapshot(),
|
|
schemaVersion: 6 as const,
|
|
circuitSections: [
|
|
{
|
|
id: "section-lighting",
|
|
circuitListId: "list-1",
|
|
key: "lighting",
|
|
displayName: "Beleuchtung",
|
|
prefix: "-1F",
|
|
sortOrder: 10,
|
|
},
|
|
{
|
|
id: "section-unassigned",
|
|
circuitListId: "list-1",
|
|
key: "unassigned",
|
|
displayName: "Nicht zugeordnet",
|
|
prefix: "-UF",
|
|
sortOrder: 90,
|
|
},
|
|
],
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
},
|
|
],
|
|
circuitLists: [
|
|
{
|
|
id: "list-1",
|
|
projectId: "project-1",
|
|
distributionBoardId: "board-1",
|
|
name: "UV 1 Stromkreisliste",
|
|
},
|
|
],
|
|
};
|
|
|
|
const snapshot = parseProjectStateSnapshot(previous);
|
|
|
|
assert.deepEqual(
|
|
snapshot.circuitSections.map(
|
|
({ key, category, groupNumber }) => ({
|
|
key,
|
|
category,
|
|
groupNumber,
|
|
})
|
|
),
|
|
[
|
|
{
|
|
key: "lighting",
|
|
category: "lighting",
|
|
groupNumber: 1,
|
|
},
|
|
{
|
|
key: "unassigned",
|
|
category: null,
|
|
groupNumber: null,
|
|
},
|
|
]
|
|
);
|
|
assert.deepEqual(snapshot.distributionBoardComponents, []);
|
|
assert.deepEqual(snapshot.circuitProtectionDevices, []);
|
|
assert.deepEqual(
|
|
snapshot.distributionBoardComponentProtectionDevices,
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("validates the complete group, component and protection graph", () => {
|
|
const snapshot = parseProjectStateSnapshot(protectedSnapshot());
|
|
|
|
assert.equal(snapshot.circuitSections[0]?.groupNumber, 1);
|
|
assert.equal(
|
|
snapshot.distributionBoardComponents[0]?.sectionId,
|
|
"section-1"
|
|
);
|
|
assert.equal(
|
|
snapshot.circuitProtectionDevices[0]?.tripCharacteristic,
|
|
"B"
|
|
);
|
|
assert.equal(
|
|
snapshot.distributionBoardComponentProtectionDevices[0]
|
|
?.ratedResidualCurrentMa,
|
|
30
|
|
);
|
|
|
|
const duplicateBmk = protectedSnapshot();
|
|
duplicateBmk.distributionBoardComponents[0]!.equipmentIdentifier =
|
|
" -1f1.1 ";
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(duplicateBmk),
|
|
/duplicate equipment identifiers/
|
|
);
|
|
|
|
const invalidProtection = protectedSnapshot();
|
|
invalidProtection.circuitProtectionDevices[0] = {
|
|
...invalidProtection.circuitProtectionDevices[0]!,
|
|
ratedCurrentA: 35,
|
|
};
|
|
assert.throws(
|
|
() => parseProjectStateSnapshot(invalidProtection),
|
|
/protection-device configuration is invalid/
|
|
);
|
|
});
|
|
|
|
it("rejects duplicate ids and cross-project ownership", () => {
|
|
assert.throws(
|
|
() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
},
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 2",
|
|
floorId: null,
|
|
supplyType: null,
|
|
simultaneityFactor: 1,
|
|
},
|
|
],
|
|
}),
|
|
/duplicate distribution board ids/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
floors: [
|
|
{
|
|
id: "floor-1",
|
|
projectId: "project-2",
|
|
name: "EG",
|
|
sortOrder: 10,
|
|
},
|
|
],
|
|
}),
|
|
/floor belongs to a different project/
|
|
);
|
|
});
|
|
|
|
it("rejects invalid references and inconsistent reserve circuits", () => {
|
|
assert.throws(
|
|
() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
distributionBoards: [
|
|
{
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV 1",
|
|
floorId: "missing",
|
|
supplyType: "AV",
|
|
simultaneityFactor: 1,
|
|
},
|
|
],
|
|
}),
|
|
/distribution board floor reference is invalid/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
rooms: [
|
|
{
|
|
id: "room-1",
|
|
projectId: "project-1",
|
|
floorId: "missing",
|
|
roomNumber: "001",
|
|
roomName: "Technik",
|
|
},
|
|
],
|
|
}),
|
|
/room floor reference is invalid/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseProjectStateSnapshot({
|
|
...minimalSnapshot(),
|
|
circuits: [
|
|
{
|
|
id: "circuit-1",
|
|
circuitListId: "missing",
|
|
sectionId: "missing",
|
|
equipmentIdentifier: "-1F1",
|
|
displayName: null,
|
|
sortOrder: 10,
|
|
protectionType: null,
|
|
protectionRatedCurrent: null,
|
|
protectionCharacteristic: null,
|
|
cableType: null,
|
|
cableCrossSection: null,
|
|
cableLength: null,
|
|
rcdAssignment: null,
|
|
terminalDesignation: null,
|
|
voltage: null,
|
|
controlRequirement: null,
|
|
status: null,
|
|
isReserve: true,
|
|
remark: null,
|
|
deviceRows: [],
|
|
},
|
|
],
|
|
}),
|
|
/circuit list reference is invalid/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("named project snapshot request", () => {
|
|
it("normalizes bounded snapshot metadata", () => {
|
|
assert.deepEqual(
|
|
createNamedProjectSnapshotSchema.parse({
|
|
expectedRevision: 12,
|
|
name: " Freigabe ",
|
|
description: " Stand vor Ausschreibung ",
|
|
}),
|
|
{
|
|
expectedRevision: 12,
|
|
name: "Freigabe",
|
|
description: "Stand vor Ausschreibung",
|
|
}
|
|
);
|
|
});
|
|
|
|
it("rejects stale-shaped and excessive input", () => {
|
|
for (const input of [
|
|
{ expectedRevision: -1, name: "Stand" },
|
|
{ expectedRevision: 0, name: " " },
|
|
{ expectedRevision: 0, name: "x".repeat(101) },
|
|
{
|
|
expectedRevision: 0,
|
|
name: "Stand",
|
|
description: "x".repeat(501),
|
|
},
|
|
{
|
|
expectedRevision: 0,
|
|
name: "Stand",
|
|
unexpected: true,
|
|
},
|
|
]) {
|
|
assert.equal(
|
|
createNamedProjectSnapshotSchema.safeParse(input).success,
|
|
false
|
|
);
|
|
}
|
|
});
|
|
});
|