603 lines
20 KiB
TypeScript
603 lines
20 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
|
import { db } from "../src/db/client.js";
|
|
|
|
describe("circuit write service rules", () => {
|
|
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
|
const service = new CircuitWriteService({
|
|
circuitListRepository: {
|
|
async findById() {
|
|
return { id: "list1", projectId: "p1" } as never;
|
|
},
|
|
} as never,
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "sec1", circuitListId: "list1" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async existsByEquipmentIdentifier() {
|
|
return true;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.createCircuit("p1", "list1", {
|
|
sectionId: "sec1",
|
|
equipmentIdentifier: "-2F1",
|
|
sortOrder: 10,
|
|
}),
|
|
/Duplicate equipmentIdentifier/
|
|
);
|
|
});
|
|
|
|
it("rejects section and list mismatch", async () => {
|
|
const service = new CircuitWriteService({
|
|
circuitListRepository: {
|
|
async findById() {
|
|
return { id: "list1", projectId: "p1" } as never;
|
|
},
|
|
} as never,
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "sec1", circuitListId: "other" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async existsByEquipmentIdentifier() {
|
|
return false;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.createCircuit("p1", "list1", {
|
|
sectionId: "sec1",
|
|
equipmentIdentifier: "-2F1",
|
|
sortOrder: 10,
|
|
}),
|
|
/Section does not belong to circuit list/
|
|
);
|
|
});
|
|
|
|
it("deleting last device row keeps circuit and sets reserve", async () => {
|
|
let reserveFlag = false;
|
|
const service = new CircuitWriteService({
|
|
deviceRowRepository: {
|
|
async findById() {
|
|
return { id: "r1", circuitId: "c1" } as never;
|
|
},
|
|
async delete() {
|
|
return;
|
|
},
|
|
async countByCircuit() {
|
|
return 0;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async findById() {
|
|
return {
|
|
id: "c1",
|
|
sectionId: "s1",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-2F1",
|
|
sortOrder: 10,
|
|
isReserve: 0,
|
|
} as never;
|
|
},
|
|
async update(_id: string, payload: { isReserve: boolean }) {
|
|
reserveFlag = payload.isReserve;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.deleteDeviceRow("r1");
|
|
assert.equal(reserveFlag, true);
|
|
});
|
|
|
|
it("creating device row in reserve circuit clears reserve status", async () => {
|
|
let reserveFlag = true;
|
|
const service = new CircuitWriteService({
|
|
circuitRepository: {
|
|
async findById() {
|
|
return {
|
|
id: "c1",
|
|
sectionId: "s1",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-2F1",
|
|
sortOrder: 10,
|
|
isReserve: 1,
|
|
} as never;
|
|
},
|
|
async update(_id: string, payload: { isReserve: boolean }) {
|
|
reserveFlag = payload.isReserve;
|
|
},
|
|
} as never,
|
|
deviceRowRepository: {
|
|
async countByCircuit() {
|
|
return 0;
|
|
},
|
|
async create() {
|
|
return "row1";
|
|
},
|
|
async findById() {
|
|
return { id: "row1" } as never;
|
|
},
|
|
} as never,
|
|
circuitListRepository: {} as never,
|
|
circuitSectionRepository: {} as never,
|
|
projectDeviceRepository: {
|
|
async findById() {
|
|
return { id: "pd1" } as never;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.createDeviceRow("c1", {
|
|
name: "Load",
|
|
displayName: "Load",
|
|
quantity: 1,
|
|
powerPerUnit: 1,
|
|
simultaneityFactor: 1,
|
|
});
|
|
assert.equal(reserveFlag, false);
|
|
});
|
|
|
|
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
|
const service = new CircuitWriteService({
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s1", circuitListId: "l1", prefix: "-2F" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async listBySection() {
|
|
return [
|
|
{ id: "cB", sectionId: "s1", equipmentIdentifier: "-2F2", sortOrder: 10, isReserve: 0 },
|
|
{ id: "cA", sectionId: "s1", equipmentIdentifier: "-2F1", sortOrder: 20, isReserve: 1 },
|
|
] as never[];
|
|
},
|
|
async listByCircuitList() {
|
|
return [{ id: "x1", sectionId: "s2", equipmentIdentifier: "-3F1" }] as never[];
|
|
},
|
|
async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
|
safeUpdatePayload = updates;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
const result = await service.renumberSection("s1");
|
|
assert.deepEqual(safeUpdatePayload, [
|
|
{ id: "cB", equipmentIdentifier: "-2F1" },
|
|
{ id: "cA", equipmentIdentifier: "-2F2" },
|
|
]);
|
|
assert.equal(result.length, 2);
|
|
});
|
|
|
|
it("renumber shifts forward/backward and respects other sections", async () => {
|
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
|
const service = new CircuitWriteService({
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async listBySection() {
|
|
return [
|
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 10, isReserve: 0 },
|
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 20, isReserve: 0 },
|
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
|
|
] as never[];
|
|
},
|
|
async listByCircuitList() {
|
|
return [{ id: "o1", sectionId: "s2", equipmentIdentifier: "-1F2" }] as never[];
|
|
},
|
|
async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
|
safeUpdatePayload = updates;
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.renumberSection("s1");
|
|
assert.deepEqual(safeUpdatePayload, [
|
|
{ id: "c2", equipmentIdentifier: "-1F1" },
|
|
{ id: "c1", equipmentIdentifier: "-1F3" },
|
|
{ id: "c3", equipmentIdentifier: "-1F4" },
|
|
]);
|
|
});
|
|
|
|
it("renumber handles gaps and keeps device rows untouched by identifier-only update path", async () => {
|
|
let safeCalled = 0;
|
|
const service = new CircuitWriteService({
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async listBySection() {
|
|
return [
|
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
|
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 20, isReserve: 0 },
|
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
|
|
] as never[];
|
|
},
|
|
async listByCircuitList() {
|
|
return [] as never[];
|
|
},
|
|
async updateEquipmentIdentifiersSafely() {
|
|
safeCalled += 1;
|
|
},
|
|
} as never,
|
|
deviceRowRepository: {
|
|
async update() {
|
|
throw new Error("device rows must not be touched");
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.renumberSection("s1");
|
|
assert.equal(safeCalled, 1);
|
|
});
|
|
|
|
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
|
const updatedReserve: Array<{ id: string; isReserve: boolean }> = [];
|
|
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
|
const service = new CircuitWriteService({
|
|
deviceRowRepository: {
|
|
async findById() {
|
|
return { id: "r1", circuitId: "c1" } as never;
|
|
},
|
|
async countByCircuit(circuitId: string) {
|
|
if (circuitId === "c2") {
|
|
return 2;
|
|
}
|
|
if (circuitId === "c1") {
|
|
return 0;
|
|
}
|
|
return 0;
|
|
},
|
|
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async findById(circuitId: string) {
|
|
if (circuitId === "c1") {
|
|
return {
|
|
id: "c1",
|
|
sectionId: "s1",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-1F1",
|
|
sortOrder: 10,
|
|
isReserve: 0,
|
|
} as never;
|
|
}
|
|
return {
|
|
id: "c2",
|
|
sectionId: "s1",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-1F2",
|
|
sortOrder: 20,
|
|
isReserve: 1,
|
|
} as never;
|
|
},
|
|
async update(id: string, payload: { isReserve: boolean }) {
|
|
updatedReserve.push({ id, isReserve: payload.isReserve });
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
|
assert.deepEqual(movedCalls, [{ rowId: "r1", targetCircuitId: "c2", sortOrder: 30 }]);
|
|
assert.deepEqual(updatedReserve, [
|
|
{ id: "c1", isReserve: true },
|
|
{ id: "c2", isReserve: false },
|
|
]);
|
|
});
|
|
|
|
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
|
let createdCircuitPayload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean } | null = null;
|
|
const service = new CircuitWriteService({
|
|
deviceRowRepository: {
|
|
async findById() {
|
|
return { id: "r1", circuitId: "c1" } as never;
|
|
},
|
|
async countByCircuit(circuitId: string) {
|
|
if (circuitId === "c-new") {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
},
|
|
async moveToCircuit() {
|
|
return;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async findById(circuitId: string) {
|
|
if (circuitId === "c1") {
|
|
return {
|
|
id: "c1",
|
|
sectionId: "s1",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-1F1",
|
|
sortOrder: 10,
|
|
isReserve: 0,
|
|
} as never;
|
|
}
|
|
if (circuitId === "c-new") {
|
|
return {
|
|
id: "c-new",
|
|
sectionId: "s2",
|
|
circuitListId: "l1",
|
|
equipmentIdentifier: "-2F8",
|
|
sortOrder: 50,
|
|
isReserve: 0,
|
|
} as never;
|
|
}
|
|
return null as never;
|
|
},
|
|
async listBySection() {
|
|
return [{ sortOrder: 40 }] as never[];
|
|
},
|
|
async create(payload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean }) {
|
|
createdCircuitPayload = payload;
|
|
return "c-new";
|
|
},
|
|
async update() {
|
|
return;
|
|
},
|
|
} as never,
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
|
},
|
|
} as never,
|
|
numberingService: {
|
|
async getNextIdentifier() {
|
|
return "-2F8";
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
|
assert.equal(createdCircuitPayload?.sectionId, "s2");
|
|
assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8");
|
|
assert.equal(createdCircuitPayload?.isReserve, false);
|
|
});
|
|
|
|
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
|
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
|
const reserveUpdates: Array<{ id: string; isReserve: boolean }> = [];
|
|
const service = new CircuitWriteService({
|
|
deviceRowRepository: {
|
|
async findById(rowId: string) {
|
|
if (rowId === "r1") {
|
|
return { id: "r1", circuitId: "c1" } as never;
|
|
}
|
|
return { id: "r2", circuitId: "c2" } as never;
|
|
},
|
|
async countByCircuit(circuitId: string) {
|
|
if (circuitId === "c3") {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
},
|
|
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async findById(circuitId: string) {
|
|
if (circuitId === "c1") {
|
|
return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
|
}
|
|
if (circuitId === "c2") {
|
|
return { id: "c2", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 } as never;
|
|
}
|
|
return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never;
|
|
},
|
|
async update(id: string, payload: { isReserve: boolean }) {
|
|
reserveUpdates.push({ id, isReserve: payload.isReserve });
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" });
|
|
assert.deepEqual(movedCalls, [
|
|
{ rowId: "r1", targetCircuitId: "c3", sortOrder: 20 },
|
|
{ rowId: "r2", targetCircuitId: "c3", sortOrder: 30 },
|
|
]);
|
|
assert.deepEqual(reserveUpdates, [
|
|
{ id: "c1", isReserve: true },
|
|
{ id: "c2", isReserve: true },
|
|
{ id: "c3", isReserve: false },
|
|
]);
|
|
});
|
|
|
|
it("moving multiple device rows to placeholder creates exactly one new circuit", async () => {
|
|
let createCount = 0;
|
|
const service = new CircuitWriteService({
|
|
deviceRowRepository: {
|
|
async findById(rowId: string) {
|
|
return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never;
|
|
},
|
|
async countByCircuit(circuitId: string) {
|
|
if (circuitId === "c-new") {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
},
|
|
async moveToCircuit() {
|
|
return;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async findById(circuitId: string) {
|
|
if (circuitId === "c1" || circuitId === "c2") {
|
|
return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never;
|
|
}
|
|
if (circuitId === "c-new") {
|
|
return { id: "c-new", sectionId: "s2", circuitListId: "l1", equipmentIdentifier: "-2F8", sortOrder: 40, isReserve: 0 } as never;
|
|
}
|
|
return null as never;
|
|
},
|
|
async listBySection() {
|
|
return [{ sortOrder: 30 }] as never[];
|
|
},
|
|
async create() {
|
|
createCount += 1;
|
|
return "c-new";
|
|
},
|
|
async update() {
|
|
return;
|
|
},
|
|
} as never,
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never;
|
|
},
|
|
} as never,
|
|
numberingService: {
|
|
async getNextIdentifier() {
|
|
return "-2F8";
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
const result = await service.moveDeviceRowsBulk({
|
|
rowIds: ["r1", "r2"],
|
|
targetSectionId: "s2",
|
|
createNewCircuit: true,
|
|
});
|
|
assert.equal(createCount, 1);
|
|
assert.equal(result.createdCircuitId, "c-new");
|
|
});
|
|
|
|
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
|
const updates: Array<{ id: string; sortOrder: number; equipmentIdentifier: string }> = [];
|
|
const service = new CircuitWriteService({
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s1", circuitListId: "l1" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async listBySection() {
|
|
return [
|
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-2F7", sortOrder: 10, isReserve: 0 },
|
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-2F9", sortOrder: 20, isReserve: 0 },
|
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-2F5", sortOrder: 30, isReserve: 1 },
|
|
] as never[];
|
|
},
|
|
async update(id: string, payload: { sortOrder: number; equipmentIdentifier: string }) {
|
|
updates.push({ id, sortOrder: payload.sortOrder, equipmentIdentifier: payload.equipmentIdentifier });
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
|
assert.deepEqual(updates, [
|
|
{ id: "c3", sortOrder: 10, equipmentIdentifier: "-2F5" },
|
|
{ id: "c1", sortOrder: 20, equipmentIdentifier: "-2F7" },
|
|
{ id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" },
|
|
]);
|
|
});
|
|
|
|
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
|
let safeCalled = false;
|
|
const service = new CircuitWriteService({
|
|
circuitSectionRepository: {
|
|
async findById() {
|
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
|
},
|
|
} as never,
|
|
circuitRepository: {
|
|
async listBySection() {
|
|
return [
|
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
|
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 },
|
|
] as never[];
|
|
},
|
|
async updateEquipmentIdentifiersSafely() {
|
|
safeCalled = true;
|
|
},
|
|
} as never,
|
|
});
|
|
await service.updateSectionEquipmentIdentifiers("s1", {
|
|
identifiers: [
|
|
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
|
|
{ circuitId: "c2", equipmentIdentifier: "-1F1" },
|
|
],
|
|
});
|
|
assert.equal(safeCalled, true);
|
|
});
|
|
|
|
it("safe identifier bulk update uses synchronous transaction callback", async () => {
|
|
const repository = new CircuitRepository();
|
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
|
|
|
let callbackReturnedPromise = false;
|
|
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
|
|
const fakeTx = {
|
|
select() {
|
|
return {
|
|
from() {
|
|
return {
|
|
where() {
|
|
return {
|
|
all() {
|
|
return [{ id: "c1" }, { id: "c2" }];
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
update() {
|
|
return {
|
|
set() {
|
|
return {
|
|
where() {
|
|
return {
|
|
run() {
|
|
return;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
const result = cb(fakeTx);
|
|
callbackReturnedPromise = Boolean(result && typeof (result as Promise<unknown>).then === "function");
|
|
if (callbackReturnedPromise) {
|
|
throw new Error("Transaction function cannot return a promise");
|
|
}
|
|
};
|
|
|
|
try {
|
|
await repository.updateEquipmentIdentifiersSafely(
|
|
"l1",
|
|
[
|
|
{ id: "c1", equipmentIdentifier: "-1F1" },
|
|
{ id: "c2", equipmentIdentifier: "-1F2" },
|
|
],
|
|
"s1"
|
|
);
|
|
assert.equal(callbackReturnedPromise, false);
|
|
} finally {
|
|
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
|
}
|
|
});
|
|
});
|