From 7222398628a1a1c31387a39bdaae70e8eedca615 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 29 Oct 2026 13:17:45 +0100 Subject: [PATCH] Typecheck tests against current types tsx runs tests without type checking and tsconfig.json covers only src, so 71 type errors had accumulated unnoticed. tsconfig.tests.json now checks every test and helper, and npm run typecheck includes it. Fixtures gain the fields the DTOs have grown since they were written (manualQuantity, circuit apparent power/cos phi, downstream board loads, section totals, building, cover and board info fields). Repository lookups that return "row ?? null" now declare the null in their type, which their callers already handle. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + README.md | 1 + docs/current-architecture.md | 2 ++ docs/development-workflow.md | 1 + package.json | 3 ++- src/db/repositories/circuit-list.repository.ts | 2 +- .../repositories/circuit-section.repository.ts | 2 +- .../repositories/global-device.repository.ts | 2 +- src/db/repositories/project.repository.ts | 2 +- src/db/repositories/user.repository.ts | 4 ++-- tests/authentication.test.ts | 15 ++++++++------- tests/circuit-device-row-move-command.test.ts | 4 ++++ tests/circuit-editor-history.test.ts | 12 +++++++++--- tests/circuit-grid-model.test.ts | 4 ++++ tests/circuit-grid-projection.test.ts | 7 +++++++ tests/circuit-group-numbering.test.ts | 13 +++++++++++++ tests/circuit-section-renumber-command.test.ts | 5 +++++ tests/circuit-section-reorder-command.test.ts | 3 +++ tests/circuit-structure-projection.test.ts | 4 ++++ tests/database-backup.test.ts | 1 + ...-circuit-project-command.repository.test.ts | 5 +++-- tests/distribution-board-subtree.test.ts | 4 ++++ ...p-import-project-command.repository.test.ts | 5 ++--- tests/external-model-xlsx-foundation.test.ts | 2 +- tests/external-xlsx-follow-up-import.test.ts | 5 ++--- tests/external-xlsx-transport.test.ts | 7 ++++--- tests/fixtures/revit-xlsx-fixtures.ts | 13 +++++++++++++ ...inear-xlsx-external-model-migration.test.ts | 4 ++-- tests/overview-filters.test.ts | 4 ++-- tests/print-view-migration.test.ts | 12 ++++++------ ...settings-project-command.repository.test.ts | 10 ++++++---- tests/project-state-snapshot.test.ts | 9 +++++---- tests/project-version-history.test.ts | 4 ++++ tests/revit-xlsx-end-to-end.test.ts | 7 +++---- tests/security/security.config.ts | 14 +++++++++----- tests/support/migrate-database.ts | 11 +++++++++++ tsconfig.tests.json | 18 ++++++++++++++++++ 37 files changed, 166 insertions(+), 56 deletions(-) create mode 100644 tests/support/migrate-database.ts create mode 100644 tsconfig.tests.json diff --git a/AGENTS.md b/AGENTS.md index ee872c6..cf4bd23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -742,6 +742,7 @@ For a normal code change run the relevant focused tests plus: - `npm run build:api` - `npm run build:web` - `npm run typecheck:scripts` +- `npm run typecheck:tests` - `npx tsc --noEmit -p tsconfig.next.json` - `make prepush` before every push, full `make check` before deploy and weekly (see Tests & Security Baseline) diff --git a/README.md b/README.md index 51a3860..7d04632 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ npm test npm run build:api npm run build:web npm run typecheck:scripts +npm run typecheck:tests npx tsc --noEmit -p tsconfig.next.json ``` diff --git a/docs/current-architecture.md b/docs/current-architecture.md index d7f28cb..396ba88 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -602,6 +602,8 @@ Oberfläche: `/projects/:projectId/grobbilanz`. Details: - `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup. - `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu erzeugen. +- `npm run typecheck:tests` prüft alle Tests und Testhilfen gegen die aktuellen + Typen; `tsx` führt Tests sonst ohne Typprüfung aus. - Die Baseline ersetzt alle vorigen Entwicklungsmigrationen. Datenbanken, Snapshots und persistierte Commands aus Vor-Baseline-Ständen werden bewusst nicht unterstützt. Ab der ersten veröffentlichten Version sind angewendete diff --git a/docs/development-workflow.md b/docs/development-workflow.md index f630063..af6b7d8 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -82,6 +82,7 @@ npm test npm run build:api npm run build:web npm run typecheck:scripts +npm run typecheck:tests npx tsc --noEmit -p tsconfig.next.json git diff --check ``` diff --git a/package.json b/package.json index 87f543e..54807a1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build:api": "tsc -p tsconfig.json", "build:web": "next build", "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json", + "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json", "start": "node dist/server/index.js", "test": "node scripts/run-tests.mjs", "test:watch": "node scripts/run-tests.mjs --watch", @@ -29,7 +30,7 @@ "db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js", "revit:verify-reference": "tsx scripts/verify-revit-reference-xlsx.ts", "lint": "eslint .", - "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.next.json && tsc --noEmit -p tsconfig.scripts.json" + "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.next.json && tsc --noEmit -p tsconfig.scripts.json && tsc --noEmit -p tsconfig.tests.json" }, "keywords": [], "author": "", diff --git a/src/db/repositories/circuit-list.repository.ts b/src/db/repositories/circuit-list.repository.ts index 6386094..9b9ca4a 100644 --- a/src/db/repositories/circuit-list.repository.ts +++ b/src/db/repositories/circuit-list.repository.ts @@ -10,7 +10,7 @@ export class CircuitListRepository { return db.select().from(circuitLists).where(eq(circuitLists.projectId, projectId)); } - async findById(projectId: string, circuitListId: string) { + async findById(projectId: string, circuitListId: string): Promise { const db = this.database; const [row] = await db .select() diff --git a/src/db/repositories/circuit-section.repository.ts b/src/db/repositories/circuit-section.repository.ts index 79a3704..0a6b0ae 100644 --- a/src/db/repositories/circuit-section.repository.ts +++ b/src/db/repositories/circuit-section.repository.ts @@ -15,7 +15,7 @@ export function createDefaultCircuitSectionValues(circuitListId: string) { export class CircuitSectionRepository { constructor(private readonly database: AppDatabase) {} - async findById(sectionId: string) { + async findById(sectionId: string): Promise { const db = this.database; const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1); return row ?? null; diff --git a/src/db/repositories/global-device.repository.ts b/src/db/repositories/global-device.repository.ts index c275f5e..20c289e 100644 --- a/src/db/repositories/global-device.repository.ts +++ b/src/db/repositories/global-device.repository.ts @@ -53,7 +53,7 @@ export class GlobalDeviceRepository { .where(eq(globalDevices.id, globalDeviceId)); } - async findById(globalDeviceId: string) { + async findById(globalDeviceId: string): Promise { const db = this.database; const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1); return row ?? null; diff --git a/src/db/repositories/project.repository.ts b/src/db/repositories/project.repository.ts index f35ed4f..9fa0f5c 100644 --- a/src/db/repositories/project.repository.ts +++ b/src/db/repositories/project.repository.ts @@ -41,7 +41,7 @@ export class ProjectRepository { return project; } - async findById(projectId: string) { + async findById(projectId: string): Promise { const db = this.database; const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1); return row ?? null; diff --git a/src/db/repositories/user.repository.ts b/src/db/repositories/user.repository.ts index 1fd55c7..8d60d9e 100644 --- a/src/db/repositories/user.repository.ts +++ b/src/db/repositories/user.repository.ts @@ -32,13 +32,13 @@ export class UserRepository implements UserStore { return rows.length; } - async findById(userId: string) { + async findById(userId: string): Promise { const db = this.database; const [row] = await db.select().from(users).where(eq(users.id, userId)).limit(1); return row ?? null; } - async findByUsername(username: string) { + async findByUsername(username: string): Promise { const db = this.database; const [row] = await db .select() diff --git a/tests/authentication.test.ts b/tests/authentication.test.ts index e5c431d..b64333d 100644 --- a/tests/authentication.test.ts +++ b/tests/authentication.test.ts @@ -160,16 +160,17 @@ describe("same-origin guard for writes", () => { function run(method: string, fetchSite?: string, includeApplicationHeader = true) { let status: number | undefined; let passed = false; + const headers: Record = { + ...(fetchSite ? { "sec-fetch-site": fetchSite } : {}), + ...(includeApplicationHeader + ? { [WRITE_REQUEST_HEADER_NAME.toLowerCase()]: WRITE_REQUEST_HEADER_VALUE } + : {}), + }; const req = { method, - headers: { - ...(fetchSite ? { "sec-fetch-site": fetchSite } : {}), - ...(includeApplicationHeader - ? { [WRITE_REQUEST_HEADER_NAME.toLowerCase()]: WRITE_REQUEST_HEADER_VALUE } - : {}), - }, + headers, get(name: string) { - return this.headers[name.toLowerCase() as keyof typeof this.headers]; + return headers[name.toLowerCase()]; }, } as unknown as Request; const res = { diff --git a/tests/circuit-device-row-move-command.test.ts b/tests/circuit-device-row-move-command.test.ts index 57805e2..5aaf984 100644 --- a/tests/circuit-device-row-move-command.test.ts +++ b/tests/circuit-device-row-move-command.test.ts @@ -18,6 +18,7 @@ function row( name: id, displayName: id, quantity: 1, + manualQuantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, rowTotalPower: 0.1, @@ -36,7 +37,10 @@ function circuit( sortOrder: 10, isReserve: deviceRows.length === 0, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows, + downstreamBoardLoads: [], }; } diff --git a/tests/circuit-editor-history.test.ts b/tests/circuit-editor-history.test.ts index 616333c..b4b26f3 100644 --- a/tests/circuit-editor-history.test.ts +++ b/tests/circuit-editor-history.test.ts @@ -13,12 +13,18 @@ import type { function tree(currentRevision: number): CircuitTreeResponseDto { return { - id: "list-1", - projectId: "project-1", + circuitListId: "list-1", distributionBoardId: "board-1", - name: "Liste", + distributionBoardName: "UV 1", currentRevision, + singlePhaseVoltageV: 230, + threePhaseVoltageV: 400, + distributionBoardSimultaneityFactor: 1, + distributionBoardTotalPower: 0, + distributionBoardTotalPowerWithSimultaneityFactor: 0, + headerComponents: [], sections: [], + footerComponents: [], }; } diff --git a/tests/circuit-grid-model.test.ts b/tests/circuit-grid-model.test.ts index 475d781..2cf0aa3 100644 --- a/tests/circuit-grid-model.test.ts +++ b/tests/circuit-grid-model.test.ts @@ -27,6 +27,7 @@ const device: CircuitTreeDeviceRowDto = { roomNumberSnapshot: "1.01", roomNameSnapshot: "Office", quantity: 2, + manualQuantity: 2, powerPerUnit: 0.05, simultaneityFactor: 0.8, rowTotalPower: 0.08, @@ -53,7 +54,10 @@ const circuit: CircuitTreeCircuitDto = { isReserve: false, remark: "Circuit remark", circuitTotalPower: 0.08, + circuitApparentPower: 0.08, + circuitCosPhi: 1, deviceRows: [device], + downstreamBoardLoads: [], }; describe("circuit grid model", () => { diff --git a/tests/circuit-grid-projection.test.ts b/tests/circuit-grid-projection.test.ts index 6551aee..18cdba2 100644 --- a/tests/circuit-grid-projection.test.ts +++ b/tests/circuit-grid-projection.test.ts @@ -25,6 +25,7 @@ function makeDevice( displayName, roomNumberSnapshot, quantity: 1, + manualQuantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, rowTotalPower: 0.1, @@ -77,6 +78,8 @@ const sections: CircuitTreeSectionDto[] = [ displayName: "Lighting", prefix: "-1F", sortOrder: 10, + sectionTotalPower: 0.3, + components: [], circuits: [compactCircuit, multiDeviceCircuit, reserveCircuit], }, ]; @@ -90,6 +93,8 @@ describe("circuit grid projection", () => { displayName: "Lighting", prefix: "-1F", sortOrder: 10, + sectionTotalPower: 0, + components: [], circuits: [], }, { @@ -98,6 +103,8 @@ describe("circuit grid projection", () => { displayName: "Single-phase circuits", prefix: "-2F", sortOrder: 20, + sectionTotalPower: 0, + components: [], circuits: [], }, ]; diff --git a/tests/circuit-group-numbering.test.ts b/tests/circuit-group-numbering.test.ts index a63c766..fa41c0c 100644 --- a/tests/circuit-group-numbering.test.ts +++ b/tests/circuit-group-numbering.test.ts @@ -138,7 +138,10 @@ describe("circuit group numbering", () => { sortOrder: 10, isReserve: false, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }, ], components: [ @@ -235,7 +238,10 @@ describe("circuit group numbering", () => { sortOrder: 10, isReserve: false, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }, ], }; @@ -250,7 +256,10 @@ describe("circuit group numbering", () => { sortOrder: 20, isReserve: false, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }, ], }; @@ -328,6 +337,9 @@ describe("circuit group numbering", () => { voltage: 230, isReserve: false, circuitTotalPower: 0.1, + circuitApparentPower: 0.1 / 0.95, + circuitCosPhi: 0.95, + downstreamBoardLoads: [], protectionDevice: { type: "LS" as const, ratedCurrentA: 10, @@ -342,6 +354,7 @@ describe("circuit group numbering", () => { displayName: "Leuchte Flur", phaseType: "single_phase", quantity: 2, + manualQuantity: 2, powerPerUnit: 0.05, simultaneityFactor: 1, cosPhi: 0.95, diff --git a/tests/circuit-section-renumber-command.test.ts b/tests/circuit-section-renumber-command.test.ts index 0802d36..55e383f 100644 --- a/tests/circuit-section-renumber-command.test.ts +++ b/tests/circuit-section-renumber-command.test.ts @@ -24,7 +24,10 @@ function circuit( sortOrder, isReserve: true, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }; } @@ -39,6 +42,8 @@ function section( displayName: id, prefix, sortOrder: 10, + sectionTotalPower: 0, + components: [], circuits: circuits.map((entry) => ({ ...entry, sectionId: id, diff --git a/tests/circuit-section-reorder-command.test.ts b/tests/circuit-section-reorder-command.test.ts index 4a871e0..c2f2965 100644 --- a/tests/circuit-section-reorder-command.test.ts +++ b/tests/circuit-section-reorder-command.test.ts @@ -24,7 +24,10 @@ function circuit( sortOrder, isReserve: true, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }; } diff --git a/tests/circuit-structure-projection.test.ts b/tests/circuit-structure-projection.test.ts index a36e96b..cc616f5 100644 --- a/tests/circuit-structure-projection.test.ts +++ b/tests/circuit-structure-projection.test.ts @@ -42,7 +42,10 @@ function circuit( sortOrder: 10, isReserve: true, circuitTotalPower: 0, + circuitApparentPower: 0, + circuitCosPhi: 1, deviceRows: [], + downstreamBoardLoads: [], }; } @@ -124,6 +127,7 @@ describe("circuit structure projection", () => { name: "Leuchte", displayName: "Leuchte", quantity: 1, + manualQuantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, rowTotalPower: 0.1, diff --git a/tests/database-backup.test.ts b/tests/database-backup.test.ts index 88778e5..520e082 100644 --- a/tests/database-backup.test.ts +++ b/tests/database-backup.test.ts @@ -122,6 +122,7 @@ describe("application repository database contexts", () => { quantity: 1, installedPowerPerUnitKw: 0.2, demandFactor: 1, + phaseCount: 1, }); assert.equal((await firstGlobalDevices.list()).length, 1); diff --git a/tests/distribution-board-feeder-circuit-project-command.repository.test.ts b/tests/distribution-board-feeder-circuit-project-command.repository.test.ts index d4d2c26..a6742f5 100644 --- a/tests/distribution-board-feeder-circuit-project-command.repository.test.ts +++ b/tests/distribution-board-feeder-circuit-project-command.repository.test.ts @@ -208,7 +208,8 @@ describe("distribution-board feeder-circuit project command", () => { const undo = history.getNextCommand("project-1", "undo"); assert.ok(undo); - assertDistributionBoardFeederCircuitProjectCommand(undo.command); + const undoCommand = undo.command; + assertDistributionBoardFeederCircuitProjectCommand(undoCommand); assert.throws( () => repository.executeFeederCircuit({ @@ -216,7 +217,7 @@ describe("distribution-board feeder-circuit project command", () => { expectedRevision: 3, source: "undo", historyTargetChangeSetId: undo.changeSetId, - command: undo.command, + command: undoCommand, }), /changed before history removal/ ); diff --git a/tests/distribution-board-subtree.test.ts b/tests/distribution-board-subtree.test.ts index 6e48294..6caff60 100644 --- a/tests/distribution-board-subtree.test.ts +++ b/tests/distribution-board-subtree.test.ts @@ -21,6 +21,10 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot { simultaneityFactor: 0.8, feedingDistributionBoardId: "board-upstream", feedingCircuitId: "circuit-upstream", + boardNumber: null, + location: null, + fedFrom: null, + remark: null, }, circuitList: { id: "list-1", diff --git a/tests/external-follow-up-import-project-command.repository.test.ts b/tests/external-follow-up-import-project-command.repository.test.ts index fcb6ae7..8b7f6cc 100644 --- a/tests/external-follow-up-import-project-command.repository.test.ts +++ b/tests/external-follow-up-import-project-command.repository.test.ts @@ -1,7 +1,6 @@ import path from "node:path"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import ExcelJS from "exceljs"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext } from "../src/db/database-context.js"; @@ -26,7 +25,7 @@ import { buildExternalXlsxFollowUpTarget } from "../src/external-model/applicati import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js"; import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js"; import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js"; -import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; +import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook, firstWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; const deviceTypeA = "10000000-0000-4000-8000-000000000001"; const deviceTypeB = "10000000-0000-4000-8000-000000000002"; @@ -54,7 +53,7 @@ describe("external follow-up import project command", () => { const importedFirst = new ExternalModelStateRepository(context.db).getByProject("project-1").state.objects.find((object) => object.worksharingId === firstWorksharingId)!; context.db.update(externalModelObjects).set({ circuitDeviceRowId: "row-1" }).where(eq(externalModelObjects.id, importedFirst.id)).run(); - const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(initialBytes); workbook.worksheets[0]!.getCell(3, 6).value = 3; + const workbook = await loadXlsxWorkbook(initialBytes); workbook.worksheets[0]!.getCell(3, 6).value = 3; const followBytes = new Uint8Array(await workbook.xlsx.writeBuffer()); const current = new ExternalModelStateRepository(context.db).getByProject("project-1").state; const returnValues = new Map(current.objects.map((object) => [object.id, { circuitCombined: object.acceptedSourceValues.circuitCombined, boardName: object.acceptedSourceValues.boardName, circuitIdentifier: object.acceptedSourceValues.circuitIdentifier, designation: object.acceptedSourceValues.designation, electricalPowerW: object.acceptedSourceValues.electricalPowerW, voltageV: object.acceptedSourceValues.voltageV, currentA: object.acceptedSourceValues.currentA }])); diff --git a/tests/external-model-xlsx-foundation.test.ts b/tests/external-model-xlsx-foundation.test.ts index a4836ac..0dfee8c 100644 --- a/tests/external-model-xlsx-foundation.test.ts +++ b/tests/external-model-xlsx-foundation.test.ts @@ -78,7 +78,7 @@ describe("Linear XLSX external model foundation", () => { `); insert.run("o1", lowerId); insert.run("o2", upperId); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM external_model_objects").get()!.count, 2); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM external_model_objects").get()!.count, 2); assert.throws(() => insert.run("o3", lowerId), /UNIQUE constraint failed/); } finally { context.close(); diff --git a/tests/external-xlsx-follow-up-import.test.ts b/tests/external-xlsx-follow-up-import.test.ts index c4ec95e..eb88fa0 100644 --- a/tests/external-xlsx-follow-up-import.test.ts +++ b/tests/external-xlsx-follow-up-import.test.ts @@ -1,13 +1,12 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import ExcelJS from "exceljs"; import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js"; import { buildExternalXlsxFollowUpTarget } from "../src/external-model/application/external-xlsx-follow-up-target.js"; import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js"; import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js"; import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js"; import type { ExternalXlsxReturnValues } from "../src/external-model/xlsx/external-xlsx-contracts.js"; -import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; +import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook, firstWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; const deviceTypeA = "10000000-0000-4000-8000-000000000001"; const deviceTypeB = "10000000-0000-4000-8000-000000000002"; @@ -26,7 +25,7 @@ async function initialState() { } async function changedWorkbook(firstId = firstWorksharingId) { - const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(await createRevitXlsxFixture()); + const workbook = await loadXlsxWorkbook(await createRevitXlsxFixture()); const sheet = workbook.worksheets[0]!; sheet.getCell(3, 1).value = firstId; sheet.getCell(3, 6).value = 3; sheet.getCell(5, 1).value = newWorksharingId; sheet.getCell(5, 2).value = "Leuchte neu"; sheet.getCell(5, 5).value = "Leuchte: Decke"; sheet.getCell(5, 6).value = 1; diff --git a/tests/external-xlsx-transport.test.ts b/tests/external-xlsx-transport.test.ts index 32cd784..3625306 100644 --- a/tests/external-xlsx-transport.test.ts +++ b/tests/external-xlsx-transport.test.ts @@ -172,10 +172,11 @@ describe("external XLSX transport", () => { assert.equal(worksheet.getCell("P4").value, "bleibt"); assert.equal(worksheet.getCell("L3").numFmt, "0.00"); assert.equal(worksheet.getColumn(1).width, 12); + const worksharingNote = worksheet.getCell("A1").note; assert.equal( - typeof worksheet.getCell("A1").note === "string" - ? worksheet.getCell("A1").note - : worksheet.getCell("A1").note?.texts?.[0]?.text, + typeof worksharingNote === "string" + ? worksharingNote + : worksharingNote?.texts?.[0]?.text, "Kommentar Worksharing-ID" ); const commentedCells: string[] = []; diff --git a/tests/fixtures/revit-xlsx-fixtures.ts b/tests/fixtures/revit-xlsx-fixtures.ts index a4afd7f..911c941 100644 --- a/tests/fixtures/revit-xlsx-fixtures.ts +++ b/tests/fixtures/revit-xlsx-fixtures.ts @@ -12,6 +12,19 @@ export const secondWorksharingId = export const externalXlsxTestConfiguration: ExternalXlsxTransportConfiguration = createDefaultExternalXlsxTransportConfiguration(); +/** + * Loads workbook bytes for editing in a test. ExcelJS declares its own Buffer + * type, which Node 24's generic Buffer does not satisfy (see loadWorkbook in + * external-xlsx-transport.ts). + */ +export async function loadXlsxWorkbook(bytes: Uint8Array) { + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load( + Buffer.from(bytes) as unknown as Parameters[0] + ); + return workbook; +} + export async function createRevitXlsxFixture(options?: { duplicateWorksharingId?: boolean; secondWorksharingId?: string; diff --git a/tests/linear-xlsx-external-model-migration.test.ts b/tests/linear-xlsx-external-model-migration.test.ts index 6bfc8bf..f5e519d 100644 --- a/tests/linear-xlsx-external-model-migration.test.ts +++ b/tests/linear-xlsx-external-model-migration.test.ts @@ -53,8 +53,8 @@ it("replaces legacy CSV state and resets linked row quantities to their manual s (context.sqlite.prepare("SELECT quantity FROM circuit_device_rows WHERE id = 'r'").get() as { quantity: number }).quantity, 2 ); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM external_model_objects").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM external_model_configurations").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM external_model_objects").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM external_model_configurations").get()!.count, 0); assert.throws(() => context.sqlite.prepare("SELECT * FROM external_csv_configurations").all(), /no such table/); const columns = context.sqlite.prepare("PRAGMA table_info(external_model_objects)").all() as Array<{ name: string; notnull: number }>; diff --git a/tests/overview-filters.test.ts b/tests/overview-filters.test.ts index 84c5807..74bd1d4 100644 --- a/tests/overview-filters.test.ts +++ b/tests/overview-filters.test.ts @@ -61,8 +61,8 @@ function device( } const floors: FloorDto[] = [ - { id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2 }, - { id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1 }, + { id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2, buildingId: null }, + { id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1, buildingId: null }, ]; const floorNameById = new Map(floors.map((floor) => [floor.id, floor.name])); diff --git a/tests/print-view-migration.test.ts b/tests/print-view-migration.test.ts index 343de70..404248d 100644 --- a/tests/print-view-migration.test.ts +++ b/tests/print-view-migration.test.ts @@ -54,12 +54,12 @@ it("upgrades a populated pre-print database through the current schema without c assert.equal(board.feeding_circuit_id, null); assert.equal(floor.building_id, null); assert.equal(room.area_m2, null); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM buildings").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM document_branding_assets").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM document_branding_settings").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM estimate_balances").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM estimate_groups").get()!.count, 0); - assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM estimate_rows").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM buildings").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM document_branding_assets").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM document_branding_settings").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM estimate_balances").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM estimate_groups").get()!.count, 0); + assert.equal(context.sqlite.prepare<[], { count: number }>("SELECT count(*) AS count FROM estimate_rows").get()!.count, 0); assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []); assert.equal(context.sqlite.pragma("integrity_check", { simple: true }), "ok"); migrate(context.db, { migrationsFolder: source }); diff --git a/tests/project-settings-project-command.repository.test.ts b/tests/project-settings-project-command.repository.test.ts index 610fbbe..52dd16d 100644 --- a/tests/project-settings-project-command.repository.test.ts +++ b/tests/project-settings-project-command.repository.test.ts @@ -63,11 +63,13 @@ function getSettings(context: DatabaseContext) { .get(); } +type ProjectSettingsInput = Parameters< + typeof createProjectSettingsUpdateProjectCommand +>[0]; + function settings( - overrides: Partial[0]> = {} -) { + overrides: Partial = {} +): ProjectSettingsInput { return { name: "Testprojekt", internalProjectNumber: "INT-1", diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index 4d99f8e..8cb8694 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -7,6 +7,7 @@ import { serializeProjectStateSnapshot, } from "../src/domain/models/project-state-snapshot.model.js"; import { remapProjectState } from "../src/domain/models/project-transfer.model.js"; +import type { CircuitGroupCategory } from "../src/shared/constants/circuit-group.js"; import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js"; import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js"; import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js"; @@ -156,7 +157,7 @@ function protectedSnapshot() { displayName: "Beleuchtung 1", prefix: "-1F1.", sortOrder: 10, - category: "lighting" as const, + category: "lighting" as CircuitGroupCategory, groupNumber: 1, }, ], @@ -370,7 +371,7 @@ describe("project state snapshot model", () => { it("rejects legacy CSV configuration and imported state with a German migration error", () => { const configured = withoutVersionNineFields(structuredClone(minimalSnapshot())); configured.schemaVersion = 9 as typeof configured.schemaVersion; - configured.externalCsvConfiguration = { + (configured as Record).externalCsvConfiguration = { id: "legacy", projectId: "project-1", configurationVersion: 1, @@ -380,7 +381,7 @@ describe("project state snapshot model", () => { const imported = withoutVersionNineFields(structuredClone(minimalSnapshot())); imported.schemaVersion = 9 as typeof imported.schemaVersion; - imported.externalModel.source = { + (imported.externalModel as Record).source = { id: "legacy-source", projectId: "project-1", name: "Legacy", @@ -446,7 +447,7 @@ describe("project state snapshot model", () => { const previous = withoutVersionSixFields(protectedSnapshot()); previous.schemaVersion = 4 as typeof previous.schemaVersion; previous.circuits[0]!.isReserve = false; - previous.circuits[0]!.deviceRows = [{ + (previous.circuits[0] as Record).deviceRows = [{ id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, diff --git a/tests/project-version-history.test.ts b/tests/project-version-history.test.ts index 4e85d41..1f6c9b7 100644 --- a/tests/project-version-history.test.ts +++ b/tests/project-version-history.test.ts @@ -285,6 +285,10 @@ describe("project settings presentation", () => { "MSR", "SiBe", ], + siteAddress: null, + workStage: null, + projectLead: null, + projectEditor: null, currentRevision: 0, }, usedDistributionBoardSupplyTypes: ["SiBe"], diff --git a/tests/revit-xlsx-end-to-end.test.ts b/tests/revit-xlsx-end-to-end.test.ts index 45569be..ec6f5f5 100644 --- a/tests/revit-xlsx-end-to-end.test.ts +++ b/tests/revit-xlsx-end-to-end.test.ts @@ -4,7 +4,6 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { randomUUID } from "node:crypto"; import { describe, it } from "node:test"; -import ExcelJS from "exceljs"; import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext } from "../src/db/database-context.js"; @@ -39,7 +38,7 @@ import { createExternalXlsxInitialImportPlan } from "../src/external-model/appli import { projectExternalXlsxReturn } from "../src/external-model/application/external-xlsx-return-projection.js"; import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js"; import { createExternalXlsxReturn } from "../src/external-model/xlsx/external-xlsx-transport.js"; -import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId, secondWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; +import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook, firstWorksharingId, secondWorksharingId } from "./fixtures/revit-xlsx-fixtures.js"; const projectId = "project-e2e"; const boardId = "board-e2e"; @@ -80,7 +79,7 @@ describe("Linear XLSX end-to-end", () => { const projected = state.objects.map((object) => projectExternalXlsxReturn({ object, row: object.circuitDeviceRowId ? { rowId, displayName: "Steckdose", powerPerUnitKw: 0.125, cosPhi: 0.9, phaseType: "single_phase", category: "single_phase", circuitIdentifier: "-2F1.1", circuitVoltageV: 230, distributionBoardName: "UV 1" } : null, circuitMode: configuration.circuitMode, singlePhaseVoltageV: 230, threePhaseVoltageV: 400 })); const latestBatch = state.importBatches.at(-1)!; const returned = await createExternalXlsxReturn({ fileName: latestBatch.fileName, bytes: Buffer.from(latestBatch.originalContentBase64!, "base64"), configuration, selectedWorksharingIds: projected.map((entry) => entry.worksharingId), updates: projected.map((entry) => ({ worksharingId: entry.worksharingId, values: entry.values })) }); - const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(returned.bytes); workbook.worksheets[0]!.getCell(3, 6).value = 3; + const workbook = await loadXlsxWorkbook(returned.bytes); workbook.worksheets[0]!.getCell(3, 6).value = 3; const followBytes = new Uint8Array(await workbook.xlsx.writeBuffer()); const returnValues = new Map(projected.map((entry) => [entry.objectId, entry.values])); const follow = await createExternalXlsxFollowUpImportPlan({ fileName: "follow.xlsx", bytes: followBytes, configuration, catalogs: emptyCatalogs(), currentState: state, currentReturnValuesByObjectId: returnValues }); @@ -139,5 +138,5 @@ function seedProject(database: ReturnType["db"]) { database.insert(circuitDeviceRows).values({ id: rowId, circuitId, sortOrder: 10, name: "Steckdose", displayName: "Steckdose", phaseType: "single_phase", category: "single_phase", quantity: 1, manualQuantity: 1, powerPerUnit: 0.125, simultaneityFactor: 1, cosPhi: 0.9 }).run(); } function currentRow(database: ReturnType["db"]) { return toCircuitDeviceRowSnapshot(database.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).get()!); } -function emptyCatalogs() { return { floors: [], rooms: [], distributionBoards: [{ id: boardId, name: "UV 1" }], projectDevices: [] }; } +function emptyCatalogs() { return { floors: [], rooms: [], distributionBoards: [{ id: boardId, name: "UV 1", floorId: null }], projectDevices: [] }; } function nextId(prefix: string, sequence: number) { return `${prefix}0000000-0000-4000-8000-${String(sequence).padStart(12, "0")}`; } diff --git a/tests/security/security.config.ts b/tests/security/security.config.ts index 2f03ab2..cba0c38 100644 --- a/tests/security/security.config.ts +++ b/tests/security/security.config.ts @@ -6,8 +6,12 @@ * Die App wird nur DYNAMISCH in createApp() importiert, damit das Routen-Inventar greift. * Testdaten sind synthetisch; die Datenbank liegt nur im Speicher dieses Testprozesses. */ -import path from "node:path"; import type { Express } from "express"; +import type { collectRoutes } from "./route-inventory.ts"; + +// Express 5 typisiert app.router nur als Router; zur Laufzeit trägt er den +// Layer-Stack, den das generische Routen-Inventar durchläuft. +type InventoriedExpress = Express & Parameters[0]; type Headers = Record; type Case = { method: string; path: string; body?: unknown }; @@ -46,22 +50,22 @@ export async function signIn(baseUrl: string, username: string): Promise { + async createApp(): Promise { process.env.LB_DATABASE_PATH = ":memory:"; process.env.LOG_LEVEL ??= "error"; delete process.env.AUTH_BOOTSTRAP_USERNAME; delete process.env.AUTH_BOOTSTRAP_PASSWORT; - const { migrate } = await import("drizzle-orm/better-sqlite3/migrator"); const { db } = await import("../../src/db/client.js"); - migrate(db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + const { migrateDatabase } = await import("../support/migrate-database.js"); + migrateDatabase(db); const { userService } = await import("../../src/server/composition/auth.js"); for (const username of TEST_USERS) { await userService.createUser({ username, name: username, role: "user", initialPassword: START_PASSWORD }); } const { createApp } = await import("../../src/server/app.js"); - return createApp(); + return createApp() as InventoriedExpress; }, // Ohne Login erreichbar: Healthcheck und die offenen Pfade von createAuthGuard. diff --git a/tests/support/migrate-database.ts b/tests/support/migrate-database.ts new file mode 100644 index 0000000..92fb2f1 --- /dev/null +++ b/tests/support/migrate-database.ts @@ -0,0 +1,11 @@ +import path from "node:path"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import type { AppDatabase } from "../../src/db/database-context.js"; + +/** + * Applies all migrations. ES-module tests (tests/security) use this helper so + * the migrator resolves the same Drizzle declarations as the application. + */ +export function migrateDatabase(database: AppDatabase): void { + migrate(database, { migrationsFolder: path.resolve("src", "db", "migrations") }); +} diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 0000000..7bb6b5d --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2022"] + }, + "include": ["tests/**/*.ts", "src/**/*.ts"], + "exclude": [ + "node_modules", + "dist", + "src/proxy.ts", + "src/instrumentation.ts", + "src/instrumentation-node.ts" + ] +}