forked from jappel/leistungsbilanz-ts
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:
parent
a8cc7eca5e
commit
7222398628
37 changed files with 166 additions and 56 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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<typeof circuitLists.$inferSelect | null> {
|
||||
const db = this.database;
|
||||
const [row] = await db
|
||||
.select()
|
||||
|
|
|
|||
|
|
@ -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<typeof circuitSections.$inferSelect | null> {
|
||||
const db = this.database;
|
||||
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
||||
return row ?? null;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class GlobalDeviceRepository {
|
|||
.where(eq(globalDevices.id, globalDeviceId));
|
||||
}
|
||||
|
||||
async findById(globalDeviceId: string) {
|
||||
async findById(globalDeviceId: string): Promise<typeof globalDevices.$inferSelect | null> {
|
||||
const db = this.database;
|
||||
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
||||
return row ?? null;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class ProjectRepository {
|
|||
return project;
|
||||
}
|
||||
|
||||
async findById(projectId: string) {
|
||||
async findById(projectId: string): Promise<typeof projects.$inferSelect | null> {
|
||||
const db = this.database;
|
||||
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
||||
return row ?? null;
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ export class UserRepository implements UserStore {
|
|||
return rows.length;
|
||||
}
|
||||
|
||||
async findById(userId: string) {
|
||||
async findById(userId: string): Promise<typeof users.$inferSelect | null> {
|
||||
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<typeof users.$inferSelect | null> {
|
||||
const db = this.database;
|
||||
const [row] = await db
|
||||
.select()
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ function circuit(
|
|||
sortOrder,
|
||||
isReserve: true,
|
||||
circuitTotalPower: 0,
|
||||
circuitApparentPower: 0,
|
||||
circuitCosPhi: 1,
|
||||
deviceRows: [],
|
||||
downstreamBoardLoads: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }]));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
13
tests/fixtures/revit-xlsx-fixtures.ts
vendored
13
tests/fixtures/revit-xlsx-fixtures.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }>;
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ describe("project settings presentation", () => {
|
|||
"MSR",
|
||||
"SiBe",
|
||||
],
|
||||
siteAddress: null,
|
||||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
currentRevision: 0,
|
||||
},
|
||||
usedDistributionBoardSupplyTypes: ["SiBe"],
|
||||
|
|
|
|||
|
|
@ -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")}`; }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
11
tests/support/migrate-database.ts
Normal file
11
tests/support/migrate-database.ts
Normal 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") });
|
||||
}
|
||||
18
tsconfig.tests.json
Normal file
18
tsconfig.tests.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue