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" + ] +}