Files
leistungsbilanz-ts/tests/project-device-sync.service.test.ts
T

214 lines
7.1 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { ProjectDeviceSyncService } from "../src/domain/services/project-device-sync.service.js";
import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js";
import { db } from "../src/db/client.js";
function projectDevice() {
return {
id: "pd1",
projectId: "p1",
name: "Luminaire",
displayName: "Office lighting",
phaseType: "single_phase",
connectionKind: "fixed",
costGroup: "440",
category: "Lighting",
quantity: 6,
powerPerUnit: 0.04,
simultaneityFactor: 0.8,
totalPower: 0.192,
cosPhi: 0.95,
remark: "DALI",
};
}
function linkedRow() {
return {
id: "row1",
circuitId: "c1",
linkedProjectDeviceId: "pd1",
legacyConsumerId: null,
sortOrder: 10,
name: "Old luminaire",
displayName: "Local display name",
phaseType: "single_phase",
connectionKind: null,
costGroup: null,
category: "Lighting",
level: null,
roomId: null,
roomNumberSnapshot: "1.01",
roomNameSnapshot: "Office",
quantity: 2,
powerPerUnit: 0.03,
simultaneityFactor: 1,
cosPhi: 1,
remark: null,
overriddenFields: JSON.stringify(["displayName", "quantity"]),
equipmentIdentifier: "-1F1",
circuitDisplayName: "Office",
circuitListId: "list1",
circuitListName: "UV-01 Stromkreisliste",
distributionBoardId: "db1",
distributionBoardName: "UV-01",
};
}
function createService() {
const rows = [linkedRow()];
const updates: Array<Record<string, unknown>> = [];
let transactionCalls = 0;
const service = new ProjectDeviceSyncService({
projectDeviceRepository: {
async findById() {
return projectDevice() as never;
},
},
deviceRowRepository: {
async listLinkedByProjectDevice() {
return rows.filter((row) => row.linkedProjectDeviceId === "pd1") as never;
},
async listLinkStatesByIds() {
return rows.map((row) => ({ id: row.id, linkedProjectDeviceId: row.linkedProjectDeviceId })) as never;
},
updateLinkedRowsTransactional(_projectDeviceId, changes) {
transactionCalls += 1;
for (const change of changes) {
updates.push({ rowId: change.rowId, ...change.input });
Object.assign(rows.find((row) => row.id === change.rowId)!, change.input);
}
},
disconnectLinkedRowsTransactional(_projectDeviceId, rowIds) {
transactionCalls += 1;
for (const rowId of rowIds) {
const row = rows.find((entry) => entry.id === rowId)!;
updates.push({ rowId, linkedProjectDeviceId: undefined, displayName: row.displayName, quantity: row.quantity });
Object.assign(row, { linkedProjectDeviceId: null });
}
},
reconnectRowsTransactional(projectDeviceId, rowIds) {
transactionCalls += 1;
for (const rowId of rowIds) {
Object.assign(rows.find((row) => row.id === rowId)!, { linkedProjectDeviceId: projectDeviceId });
}
},
} as never,
});
return { service, rows, updates, transactionCalls: () => transactionCalls };
}
describe("project device synchronization", () => {
it("shows field differences and local overrides without changing rows", async () => {
const { service, updates } = createService();
const preview = await service.getPreview("p1", "pd1");
assert.equal(updates.length, 0);
assert.equal(preview.rows[0].equipmentIdentifier, "-1F1");
assert.equal(preview.rows[0].differences.some((difference) => difference.field === "quantity"), true);
assert.equal(
preview.rows[0].differences.find((difference) => difference.field === "displayName")?.isOverridden,
true
);
});
it("synchronizes only explicitly selected fields", async () => {
const { service, rows, transactionCalls } = createService();
await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
assert.equal(rows[0].quantity, 6);
assert.equal(rows[0].powerPerUnit, 0.04);
assert.equal(rows[0].displayName, "Local display name");
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName"]);
assert.equal(transactionCalls(), 1);
});
it("rejects rows that are not linked to the selected project device", async () => {
const { service } = createService();
await assert.rejects(
() => service.synchronize("p1", "pd1", ["other-row"], ["quantity"]),
/not linked/
);
});
it("disconnects selected rows without changing local values", async () => {
const { service, updates } = createService();
await service.disconnect("p1", "pd1", ["row1"]);
assert.equal(updates[0].linkedProjectDeviceId, undefined);
assert.equal(updates[0].displayName, "Local display name");
assert.equal(updates[0].quantity, 2);
});
it("restores synchronized values and override markers", async () => {
const { service, rows } = createService();
const result = await service.synchronize("p1", "pd1", ["row1"], ["quantity", "powerPerUnit"]);
await service.restore("p1", "pd1", result.undo.rows);
assert.equal(rows[0].quantity, 2);
assert.equal(rows[0].powerPerUnit, 0.03);
assert.deepEqual(JSON.parse(rows[0].overriddenFields ?? "[]"), ["displayName", "quantity"]);
});
it("reconnects rows only while they remain disconnected", async () => {
const { service, rows } = createService();
const result = await service.disconnect("p1", "pd1", ["row1"]);
await service.reconnect("p1", "pd1", result.undo.rowIds);
assert.equal(rows[0].linkedProjectDeviceId, "pd1");
});
it("executes multi-row updates inside one synchronous transaction", () => {
const repository = new CircuitDeviceRowRepository();
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
let transactionCalls = 0;
let updateCalls = 0;
let returnedPromise = false;
(db as unknown as { transaction: (callback: (tx: unknown) => unknown) => void }).transaction = (callback) => {
transactionCalls += 1;
const fakeTx = {
update() {
return {
set() {
return {
where() {
return {
run() {
updateCalls += 1;
return { changes: 1 };
},
};
},
};
},
};
},
};
const result = callback(fakeTx);
returnedPromise = Boolean(result && typeof (result as Promise<unknown>).then === "function");
};
try {
const input = {
linkedProjectDeviceId: "pd1",
name: "Luminaire",
displayName: "Office lighting",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
};
repository.updateLinkedRowsTransactional("pd1", [
{ rowId: "r1", input },
{ rowId: "r2", input },
]);
assert.equal(transactionCalls, 1);
assert.equal(updateCalls, 2);
assert.equal(returnedPromise, false);
} finally {
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
}
});
});