427 lines
12 KiB
TypeScript
427 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 { 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 { 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,
|
|
};
|
|
}
|
|
|
|
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 {
|
|
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(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,
|
|
});
|
|
|
|
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,
|
|
});
|
|
} 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();
|
|
}
|
|
});
|
|
});
|