543 lines
16 KiB
TypeScript
543 lines
16 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 { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
|
|
import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/project-settings-project-command.repository.js";
|
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
|
import { projects } from "../src/db/schema/projects.js";
|
|
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
|
import { circuitLists } from "../src/db/schema/circuit-lists.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 { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
|
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
|
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
|
|
import { updateProjectSettings } from "../src/frontend/utils/api.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: "Testprojekt",
|
|
internalProjectNumber: "INT-1",
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
})
|
|
.run();
|
|
return context;
|
|
}
|
|
|
|
function getSettings(context: DatabaseContext) {
|
|
return context.db
|
|
.select({
|
|
name: projects.name,
|
|
internalProjectNumber: projects.internalProjectNumber,
|
|
externalProjectNumber: projects.externalProjectNumber,
|
|
buildingOwner: projects.buildingOwner,
|
|
description: projects.description,
|
|
singlePhaseVoltageV: projects.singlePhaseVoltageV,
|
|
threePhaseVoltageV: projects.threePhaseVoltageV,
|
|
enabledDistributionBoardSupplyTypes:
|
|
projects.enabledDistributionBoardSupplyTypes,
|
|
currentRevision: projects.currentRevision,
|
|
})
|
|
.from(projects)
|
|
.where(eq(projects.id, "project-1"))
|
|
.get();
|
|
}
|
|
|
|
function settings(
|
|
overrides: Partial<Parameters<
|
|
typeof createProjectSettingsUpdateProjectCommand
|
|
>[0]> = {}
|
|
) {
|
|
return {
|
|
name: "Testprojekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function seedDerivedVoltageRecords(context: DatabaseContext) {
|
|
context.db.insert(projectDevices).values([
|
|
{
|
|
id: "device-single",
|
|
projectId: "project-1",
|
|
name: "Leuchte",
|
|
displayName: "Leuchte",
|
|
phaseType: "single_phase",
|
|
quantity: 1,
|
|
powerPerUnit: 0.1,
|
|
simultaneityFactor: 1,
|
|
voltageV: 230,
|
|
},
|
|
{
|
|
id: "device-three",
|
|
projectId: "project-1",
|
|
name: "Pumpe",
|
|
displayName: "Pumpe",
|
|
phaseType: "three_phase",
|
|
quantity: 1,
|
|
powerPerUnit: 2,
|
|
simultaneityFactor: 1,
|
|
voltageV: 400,
|
|
},
|
|
]).run();
|
|
const board = new DistributionBoardFixtureRepository(
|
|
context.db
|
|
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
|
|
const list = context.db
|
|
.select()
|
|
.from(circuitLists)
|
|
.where(eq(circuitLists.distributionBoardId, board.id))
|
|
.get();
|
|
assert.ok(list);
|
|
const sections = context.db
|
|
.select()
|
|
.from(circuitSections)
|
|
.where(eq(circuitSections.circuitListId, list.id))
|
|
.all();
|
|
const singleSection = sections.find((section) => section.key === "single_phase");
|
|
const threeSection = sections.find((section) => section.key === "three_phase");
|
|
assert.ok(singleSection);
|
|
assert.ok(threeSection);
|
|
context.db.insert(circuits).values([
|
|
{
|
|
id: "circuit-single",
|
|
circuitListId: list.id,
|
|
sectionId: singleSection.id,
|
|
equipmentIdentifier: "-2F1",
|
|
sortOrder: 10,
|
|
voltage: 230,
|
|
isReserve: 1,
|
|
},
|
|
{
|
|
id: "circuit-three",
|
|
circuitListId: list.id,
|
|
sectionId: threeSection.id,
|
|
equipmentIdentifier: "-3F1",
|
|
sortOrder: 10,
|
|
voltage: 400,
|
|
isReserve: 1,
|
|
},
|
|
]).run();
|
|
}
|
|
|
|
function getDerivedVoltages(context: DatabaseContext) {
|
|
return {
|
|
devices: context.db
|
|
.select({ id: projectDevices.id, voltageV: projectDevices.voltageV })
|
|
.from(projectDevices)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
circuits: context.db
|
|
.select({ id: circuits.id, voltage: circuits.voltage })
|
|
.from(circuits)
|
|
.all()
|
|
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
};
|
|
}
|
|
|
|
describe("project settings project-command repository", () => {
|
|
it("validates settings commands and sends the expected revision from the frontend", async () => {
|
|
assert.throws(
|
|
() =>
|
|
createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
singlePhaseVoltageV: 0,
|
|
}),
|
|
/invalid values/
|
|
);
|
|
const requests: Array<{ url: string; body: unknown }> = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (input, init) => {
|
|
requests.push({
|
|
url: String(input),
|
|
body: JSON.parse(String(init?.body)),
|
|
});
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
try {
|
|
await updateProjectSettings("project-1", 7, {
|
|
...settings(),
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
assert.deepEqual(requests, [
|
|
{
|
|
url: "/api/projects/project-1",
|
|
body: {
|
|
expectedRevision: 7,
|
|
name: "Testprojekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("updates both voltages and supports persisted undo and redo", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
seedDerivedVoltageRecords(context);
|
|
const repository = new ProjectSettingsProjectCommandRepository(
|
|
context.db
|
|
);
|
|
const history = new ProjectHistoryRepository(context.db);
|
|
const command = createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
name: "Neuer Projektname",
|
|
buildingOwner: "Beispiel Bau GmbH",
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
});
|
|
const forward = repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command,
|
|
});
|
|
assert.deepEqual(getSettings(context), {
|
|
...settings({
|
|
name: "Neuer Projektname",
|
|
buildingOwner: "Beispiel Bau GmbH",
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}),
|
|
currentRevision: 1,
|
|
});
|
|
assert.deepEqual(getDerivedVoltages(context), {
|
|
devices: [
|
|
{ id: "device-single", voltageV: 240 },
|
|
{ id: "device-three", voltageV: 415 },
|
|
],
|
|
circuits: [
|
|
{ id: "circuit-single", voltage: 240 },
|
|
{ id: "circuit-three", voltage: 415 },
|
|
],
|
|
});
|
|
assert.deepEqual(forward.inverse.payload, {
|
|
name: "Testprojekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
});
|
|
|
|
const undoStep = history.getNextCommand("project-1", "undo");
|
|
assert.ok(undoStep);
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
source: "undo",
|
|
historyTargetChangeSetId: undoStep.changeSetId,
|
|
command: forward.inverse,
|
|
});
|
|
assert.deepEqual(getSettings(context), {
|
|
name: "Testprojekt",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: null,
|
|
description: null,
|
|
singlePhaseVoltageV: 230,
|
|
threePhaseVoltageV: 400,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
currentRevision: 2,
|
|
});
|
|
assert.deepEqual(getDerivedVoltages(context), {
|
|
devices: [
|
|
{ id: "device-single", voltageV: 230 },
|
|
{ id: "device-three", voltageV: 400 },
|
|
],
|
|
circuits: [
|
|
{ id: "circuit-single", voltage: 230 },
|
|
{ id: "circuit-three", voltage: 400 },
|
|
],
|
|
});
|
|
|
|
const redoStep = history.getNextCommand("project-1", "redo");
|
|
assert.ok(redoStep);
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 2,
|
|
source: "redo",
|
|
historyTargetChangeSetId: redoStep.changeSetId,
|
|
command,
|
|
});
|
|
assert.deepEqual(getSettings(context), {
|
|
name: "Neuer Projektname",
|
|
internalProjectNumber: "INT-1",
|
|
externalProjectNumber: null,
|
|
buildingOwner: "Beispiel Bau GmbH",
|
|
description: null,
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
enabledDistributionBoardSupplyTypes: [
|
|
"AV",
|
|
"SV",
|
|
"EV",
|
|
"USV",
|
|
"MSR",
|
|
"SiBe",
|
|
],
|
|
currentRevision: 3,
|
|
});
|
|
assert.deepEqual(getDerivedVoltages(context), {
|
|
devices: [
|
|
{ id: "device-single", voltageV: 240 },
|
|
{ id: "device-three", voltageV: 415 },
|
|
],
|
|
circuits: [
|
|
{ id: "circuit-single", voltage: 240 },
|
|
{ id: "circuit-three", voltage: 415 },
|
|
],
|
|
});
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("applies persisted version-one voltage commands without clearing metadata", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const repository = new ProjectSettingsProjectCommandRepository(
|
|
context.db
|
|
);
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command: {
|
|
schemaVersion: 1,
|
|
type: "project.update-settings",
|
|
payload: {
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
},
|
|
},
|
|
});
|
|
assert.deepEqual(getSettings(context), {
|
|
...settings({
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}),
|
|
currentRevision: 1,
|
|
});
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const repository = new ProjectSettingsProjectCommandRepository(
|
|
context.db
|
|
);
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
enabledDistributionBoardSupplyTypes: ["AV", "MSR", "SiBe"],
|
|
}),
|
|
});
|
|
assert.deepEqual(
|
|
getSettings(context)?.enabledDistributionBoardSupplyTypes,
|
|
["AV", "MSR", "SiBe"]
|
|
);
|
|
|
|
context.db.insert(distributionBoards).values({
|
|
id: "board-1",
|
|
projectId: "project-1",
|
|
name: "UV SiBe",
|
|
supplyType: "SiBe",
|
|
}).run();
|
|
assert.throws(
|
|
() =>
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
enabledDistributionBoardSupplyTypes: ["AV", "MSR"],
|
|
}),
|
|
}),
|
|
/SiBe/
|
|
);
|
|
assert.equal(getSettings(context)?.currentRevision, 1);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rejects no-op, invalid project and stale revision without partial writes", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
const repository = new ProjectSettingsProjectCommandRepository(
|
|
context.db
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
}),
|
|
}),
|
|
/did not change/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
repository.executeUpdate({
|
|
projectId: "missing",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}),
|
|
}),
|
|
/Project not found/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 1,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}),
|
|
}),
|
|
ProjectRevisionConflictError
|
|
);
|
|
assert.deepEqual(getSettings(context), {
|
|
...settings(),
|
|
currentRevision: 0,
|
|
});
|
|
assert.equal(
|
|
context.db.select().from(projectRevisions).all().length,
|
|
0
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
|
|
it("rolls back both values and revision after a late history failure", () => {
|
|
const context = createTestDatabase();
|
|
try {
|
|
context.sqlite.exec(`
|
|
CREATE TRIGGER fail_project_settings_history
|
|
BEFORE INSERT ON project_history_stack_entries
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'forced project settings history failure');
|
|
END;
|
|
`);
|
|
const repository = new ProjectSettingsProjectCommandRepository(
|
|
context.db
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
repository.executeUpdate({
|
|
projectId: "project-1",
|
|
expectedRevision: 0,
|
|
source: "user",
|
|
command: createProjectSettingsUpdateProjectCommand({
|
|
...settings(),
|
|
singlePhaseVoltageV: 240,
|
|
threePhaseVoltageV: 415,
|
|
}),
|
|
}),
|
|
/forced project settings history failure/
|
|
);
|
|
assert.deepEqual(getSettings(context), {
|
|
...settings(),
|
|
currentRevision: 0,
|
|
});
|
|
assert.equal(
|
|
context.db.select().from(projectRevisions).all().length,
|
|
0
|
|
);
|
|
} finally {
|
|
context.close();
|
|
}
|
|
});
|
|
});
|