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 { ExternalCsvConfigurationProjectCommandRepository } from "../src/db/repositories/external-csv-configuration-project-command.repository.js"; import { ExternalCsvConfigurationRepository } from "../src/db/repositories/external-csv-configuration.repository.js"; import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js"; import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js"; import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js"; import { externalCsvConfigurations } from "../src/db/schema/external-csv-configurations.js"; import { projects } from "../src/db/schema/projects.js"; import { createExternalCsvConfigurationUpdateProjectCommand } from "../src/domain/models/external-csv-configuration-project-command.model.js"; import { createProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js"; import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js"; import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.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: "Projekt" }).run(); return context; } function snapshot(version = 1) { const configuration = createDefaultExternalCsvConfiguration(externalCsvTestColumns); configuration.familyTypeRules.push({ exactFamilyAndType: "Steckdose: Standard", internalDeviceType: "Steckdose", connectionKind: null, category: "single_phase", quantityRule: { kind: "fixed", quantity: 1 }, displayNameSuggestion: { kind: "selection-marker" }, }); return { id: "external-config-1", projectId: "project-1", configurationVersion: version, configuration, }; } describe("external CSV configuration project command", () => { it("persists insert and update with exact inverse commands", () => { const context = createTestDatabase(); try { const repository = new ExternalCsvConfigurationProjectCommandRepository(context.db); const initial = snapshot(); const inserted = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", description: "Revit-CSV-Konfiguration anlegen", command: createExternalCsvConfigurationUpdateProjectCommand(null, initial), }); assert.equal(inserted.revision.revisionNumber, 1); assert.deepEqual(readConfiguration(context), initial); assert.deepEqual(inserted.inverse.payload, { expected: initial, target: null }); const target = structuredClone(initial); target.configurationVersion = 2; target.configuration.decimalSeparator = "."; const updated = repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(initial, target), }); assert.equal(updated.revision.revisionNumber, 2); assert.deepEqual(readConfiguration(context), target); assert.deepEqual(updated.inverse.payload, { expected: target, target: initial }); } finally { context.close(); } }); it("rejects stale state and cross-project targets without a partial revision", () => { const context = createTestDatabase(); try { const repository = new ExternalCsvConfigurationProjectCommandRepository(context.db); const initial = snapshot(); repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(null, initial), }); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(null, snapshot(2)), }), /changed before update/ ); const foreign = snapshot(2); foreign.projectId = "project-2"; assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(initial, foreign), }), /identity must remain stable|different project/ ); assert.equal(context.db.select({ revision: projects.currentRevision }).from(projects).get()?.revision, 1); const skippedVersion = snapshot(3); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(initial, skippedVersion), }), /version must advance by one/ ); assert.throws( () => repository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(initial, initial), }), /did not change/ ); } finally { context.close(); } }); it("includes configuration in snapshots and remaps it for project duplicates", () => { const context = createTestDatabase(); try { const repository = new ExternalCsvConfigurationProjectCommandRepository(context.db); const initial = snapshot(); repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(null, initial), }); const state = readProjectStateSnapshot(context.db, "project-1")?.state; assert.deepEqual(state?.externalCsvConfiguration, initial); const transferRepository = new ProjectTransferRepository(context.db); const transfer = transferRepository.exportProject("project-1"); assert.ok(transfer); const duplicate = transferRepository.importDuplicate(transfer); const duplicatedConfiguration = context.db .select() .from(externalCsvConfigurations) .where(eq(externalCsvConfigurations.projectId, duplicate.projectId)) .get(); assert.ok(duplicatedConfiguration); assert.notEqual(duplicatedConfiguration.id, initial.id); assert.equal(duplicatedConfiguration.projectId, duplicate.projectId); assert.deepEqual(duplicatedConfiguration.configuration, initial.configuration); } finally { context.close(); } }); it("distinguishes projects without configuration from unknown projects", () => { const context = createTestDatabase(); try { const reader = new ExternalCsvConfigurationRepository(context.db); assert.deepEqual(reader.getByProject("project-1"), { projectExists: true, configuration: null, }); assert.deepEqual(reader.getByProject("missing"), { projectExists: false, configuration: null, }); } finally { context.close(); } }); it("restores and undoes the complete configuration through snapshot history", () => { const context = createTestDatabase(); try { const configurationRepository = new ExternalCsvConfigurationProjectCommandRepository(context.db); const initial = snapshot(); configurationRepository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(null, initial), }); const configuredState = readProjectStateSnapshot(context.db, "project-1"); assert.ok(configuredState); configurationRepository.execute({ projectId: "project-1", expectedRevision: 1, source: "user", command: createExternalCsvConfigurationUpdateProjectCommand(initial, null), }); const emptyState = readProjectStateSnapshot(context.db, "project-1"); assert.ok(emptyState); const restoreRepository = new ProjectStateRestoreCommandRepository(context.db); const restored = restoreRepository.execute({ projectId: "project-1", expectedRevision: 2, source: "user", command: createProjectStateRestoreCommand( emptyState.payloadSha256, configuredState.state ), }); assert.deepEqual(readConfiguration(context), initial); restoreRepository.execute({ projectId: "project-1", expectedRevision: 3, source: "undo", historyTargetChangeSetId: restored.revision.changeSetId, command: restored.inverse, }); assert.equal(readConfiguration(context), null); } finally { context.close(); } }); }); function readConfiguration(context: DatabaseContext) { return context.db .select() .from(externalCsvConfigurations) .where(eq(externalCsvConfigurations.projectId, "project-1")) .get() ?? null; }