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 { 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, currentRevision: projects.currentRevision, }) .from(projects) .where(eq(projects.id, "project-1")) .get(); } function settings( overrides: Partial[0]> = {} ) { return { name: "Testprojekt", internalProjectNumber: "INT-1", externalProjectNumber: null, buildingOwner: null, description: null, singlePhaseVoltageV: 230, threePhaseVoltageV: 400, ...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, }, }, ]); }); 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, }); const forward = repository.executeUpdate({ projectId: "project-1", expectedRevision: 0, source: "user", command, }); assert.deepEqual(getSettings(context), { name: "Neuer Projektname", internalProjectNumber: "INT-1", externalProjectNumber: null, buildingOwner: "Beispiel Bau GmbH", description: null, 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, }); 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, 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, 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("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), { name: "Testprojekt", internalProjectNumber: "INT-1", externalProjectNumber: null, buildingOwner: null, description: null, singlePhaseVoltageV: 230, threePhaseVoltageV: 400, 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), { name: "Testprojekt", internalProjectNumber: "INT-1", externalProjectNumber: null, buildingOwner: null, description: null, singlePhaseVoltageV: 230, threePhaseVoltageV: 400, currentRevision: 0, }); assert.equal( context.db.select().from(projectRevisions).all().length, 0 ); } finally { context.close(); } }); });