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 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-10-29 13:17:45 +01:00
parent a8cc7eca5e
commit 7222398628
37 changed files with 166 additions and 56 deletions

View file

@ -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<string, string> = {
...(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 = {

View file

@ -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: [],
};
}

View file

@ -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: [],
};
}

View file

@ -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", () => {

View file

@ -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: [],
},
];

View file

@ -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,

View file

@ -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,

View file

@ -24,7 +24,10 @@ function circuit(
sortOrder,
isReserve: true,
circuitTotalPower: 0,
circuitApparentPower: 0,
circuitCosPhi: 1,
deviceRows: [],
downstreamBoardLoads: [],
};
}

View file

@ -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,

View file

@ -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);

View file

@ -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/
);

View file

@ -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",

View file

@ -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 }]));

View file

@ -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();

View file

@ -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;

View file

@ -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[] = [];

View file

@ -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<typeof workbook.xlsx.load>[0]
);
return workbook;
}
export async function createRevitXlsxFixture(options?: {
duplicateWorksharingId?: boolean;
secondWorksharingId?: string;

View file

@ -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 }>;

View file

@ -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]));

View file

@ -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 });

View file

@ -63,11 +63,13 @@ function getSettings(context: DatabaseContext) {
.get();
}
type ProjectSettingsInput = Parameters<
typeof createProjectSettingsUpdateProjectCommand
>[0];
function settings(
overrides: Partial<Parameters<
typeof createProjectSettingsUpdateProjectCommand
>[0]> = {}
) {
overrides: Partial<ProjectSettingsInput> = {}
): ProjectSettingsInput {
return {
name: "Testprojekt",
internalProjectNumber: "INT-1",

View file

@ -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<string, unknown>).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<string, unknown>).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<string, unknown>).deviceRows = [{
id: "row-1",
circuitId: "circuit-1",
linkedProjectDeviceId: null,

View file

@ -285,6 +285,10 @@ describe("project settings presentation", () => {
"MSR",
"SiBe",
],
siteAddress: null,
workStage: null,
projectLead: null,
projectEditor: null,
currentRevision: 0,
},
usedDistributionBoardSupplyTypes: ["SiBe"],

View file

@ -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<typeof createDatabaseContext>["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<typeof createDatabaseContext>["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")}`; }

View file

@ -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<typeof collectRoutes>[0];
type Headers = Record<string, string>;
type Case = { method: string; path: string; body?: unknown };
@ -46,22 +50,22 @@ export async function signIn(baseUrl: string, username: string): Promise<Headers
}
const config = {
async createApp(): Promise<Express> {
async createApp(): Promise<InventoriedExpress> {
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.

View file

@ -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") });
}