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:api`
|
||||||
- `npm run build:web`
|
- `npm run build:web`
|
||||||
- `npm run typecheck:scripts`
|
- `npm run typecheck:scripts`
|
||||||
|
- `npm run typecheck:tests`
|
||||||
- `npx tsc --noEmit -p tsconfig.next.json`
|
- `npx tsc --noEmit -p tsconfig.next.json`
|
||||||
- `make prepush` before every push, full `make check` before deploy and weekly (see Tests & Security Baseline)
|
- `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:api
|
||||||
npm run build:web
|
npm run build:web
|
||||||
npm run typecheck:scripts
|
npm run typecheck:scripts
|
||||||
|
npm run typecheck:tests
|
||||||
npx tsc --noEmit -p tsconfig.next.json
|
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 db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
||||||
- `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu
|
- `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu
|
||||||
erzeugen.
|
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,
|
- Die Baseline ersetzt alle vorigen Entwicklungsmigrationen. Datenbanken,
|
||||||
Snapshots und persistierte Commands aus Vor-Baseline-Ständen werden bewusst
|
Snapshots und persistierte Commands aus Vor-Baseline-Ständen werden bewusst
|
||||||
nicht unterstützt. Ab der ersten veröffentlichten Version sind angewendete
|
nicht unterstützt. Ab der ersten veröffentlichten Version sind angewendete
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ npm test
|
||||||
npm run build:api
|
npm run build:api
|
||||||
npm run build:web
|
npm run build:web
|
||||||
npm run typecheck:scripts
|
npm run typecheck:scripts
|
||||||
|
npm run typecheck:tests
|
||||||
npx tsc --noEmit -p tsconfig.next.json
|
npx tsc --noEmit -p tsconfig.next.json
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
||||||
|
"typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "node scripts/run-tests.mjs",
|
"test": "node scripts/run-tests.mjs",
|
||||||
"test:watch": "node scripts/run-tests.mjs --watch",
|
"test:watch": "node scripts/run-tests.mjs --watch",
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
"db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js",
|
||||||
"revit:verify-reference": "tsx scripts/verify-revit-reference-xlsx.ts",
|
"revit:verify-reference": "tsx scripts/verify-revit-reference-xlsx.ts",
|
||||||
"lint": "eslint .",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class CircuitListRepository {
|
||||||
return db.select().from(circuitLists).where(eq(circuitLists.projectId, projectId));
|
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 db = this.database;
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export function createDefaultCircuitSectionValues(circuitListId: string) {
|
||||||
export class CircuitSectionRepository {
|
export class CircuitSectionRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
async findById(sectionId: string) {
|
async findById(sectionId: string): Promise<typeof circuitSections.$inferSelect | null> {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
const [row] = await db.select().from(circuitSections).where(eq(circuitSections.id, sectionId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export class GlobalDeviceRepository {
|
||||||
.where(eq(globalDevices.id, globalDeviceId));
|
.where(eq(globalDevices.id, globalDeviceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(globalDeviceId: string) {
|
async findById(globalDeviceId: string): Promise<typeof globalDevices.$inferSelect | null> {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
const [row] = await db.select().from(globalDevices).where(eq(globalDevices.id, globalDeviceId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export class ProjectRepository {
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(projectId: string) {
|
async findById(projectId: string): Promise<typeof projects.$inferSelect | null> {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
const [row] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ export class UserRepository implements UserStore {
|
||||||
return rows.length;
|
return rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(userId: string) {
|
async findById(userId: string): Promise<typeof users.$inferSelect | null> {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
const [row] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
const [row] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByUsername(username: string) {
|
async findByUsername(username: string): Promise<typeof users.$inferSelect | null> {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
|
||||||
|
|
@ -160,16 +160,17 @@ describe("same-origin guard for writes", () => {
|
||||||
function run(method: string, fetchSite?: string, includeApplicationHeader = true) {
|
function run(method: string, fetchSite?: string, includeApplicationHeader = true) {
|
||||||
let status: number | undefined;
|
let status: number | undefined;
|
||||||
let passed = false;
|
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 = {
|
const req = {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers,
|
||||||
...(fetchSite ? { "sec-fetch-site": fetchSite } : {}),
|
|
||||||
...(includeApplicationHeader
|
|
||||||
? { [WRITE_REQUEST_HEADER_NAME.toLowerCase()]: WRITE_REQUEST_HEADER_VALUE }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
get(name: string) {
|
get(name: string) {
|
||||||
return this.headers[name.toLowerCase() as keyof typeof this.headers];
|
return headers[name.toLowerCase()];
|
||||||
},
|
},
|
||||||
} as unknown as Request;
|
} as unknown as Request;
|
||||||
const res = {
|
const res = {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ function row(
|
||||||
name: id,
|
name: id,
|
||||||
displayName: id,
|
displayName: id,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
powerPerUnit: 0.1,
|
powerPerUnit: 0.1,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
rowTotalPower: 0.1,
|
rowTotalPower: 0.1,
|
||||||
|
|
@ -36,7 +37,10 @@ function circuit(
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
isReserve: deviceRows.length === 0,
|
isReserve: deviceRows.length === 0,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows,
|
deviceRows,
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,18 @@ import type {
|
||||||
|
|
||||||
function tree(currentRevision: number): CircuitTreeResponseDto {
|
function tree(currentRevision: number): CircuitTreeResponseDto {
|
||||||
return {
|
return {
|
||||||
id: "list-1",
|
circuitListId: "list-1",
|
||||||
projectId: "project-1",
|
|
||||||
distributionBoardId: "board-1",
|
distributionBoardId: "board-1",
|
||||||
name: "Liste",
|
distributionBoardName: "UV 1",
|
||||||
currentRevision,
|
currentRevision,
|
||||||
|
singlePhaseVoltageV: 230,
|
||||||
|
threePhaseVoltageV: 400,
|
||||||
|
distributionBoardSimultaneityFactor: 1,
|
||||||
|
distributionBoardTotalPower: 0,
|
||||||
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
|
headerComponents: [],
|
||||||
sections: [],
|
sections: [],
|
||||||
|
footerComponents: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const device: CircuitTreeDeviceRowDto = {
|
||||||
roomNumberSnapshot: "1.01",
|
roomNumberSnapshot: "1.01",
|
||||||
roomNameSnapshot: "Office",
|
roomNameSnapshot: "Office",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
|
manualQuantity: 2,
|
||||||
powerPerUnit: 0.05,
|
powerPerUnit: 0.05,
|
||||||
simultaneityFactor: 0.8,
|
simultaneityFactor: 0.8,
|
||||||
rowTotalPower: 0.08,
|
rowTotalPower: 0.08,
|
||||||
|
|
@ -53,7 +54,10 @@ const circuit: CircuitTreeCircuitDto = {
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
remark: "Circuit remark",
|
remark: "Circuit remark",
|
||||||
circuitTotalPower: 0.08,
|
circuitTotalPower: 0.08,
|
||||||
|
circuitApparentPower: 0.08,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [device],
|
deviceRows: [device],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("circuit grid model", () => {
|
describe("circuit grid model", () => {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ function makeDevice(
|
||||||
displayName,
|
displayName,
|
||||||
roomNumberSnapshot,
|
roomNumberSnapshot,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
powerPerUnit: 0.1,
|
powerPerUnit: 0.1,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
rowTotalPower: 0.1,
|
rowTotalPower: 0.1,
|
||||||
|
|
@ -77,6 +78,8 @@ const sections: CircuitTreeSectionDto[] = [
|
||||||
displayName: "Lighting",
|
displayName: "Lighting",
|
||||||
prefix: "-1F",
|
prefix: "-1F",
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
|
sectionTotalPower: 0.3,
|
||||||
|
components: [],
|
||||||
circuits: [compactCircuit, multiDeviceCircuit, reserveCircuit],
|
circuits: [compactCircuit, multiDeviceCircuit, reserveCircuit],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -90,6 +93,8 @@ describe("circuit grid projection", () => {
|
||||||
displayName: "Lighting",
|
displayName: "Lighting",
|
||||||
prefix: "-1F",
|
prefix: "-1F",
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
|
sectionTotalPower: 0,
|
||||||
|
components: [],
|
||||||
circuits: [],
|
circuits: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -98,6 +103,8 @@ describe("circuit grid projection", () => {
|
||||||
displayName: "Single-phase circuits",
|
displayName: "Single-phase circuits",
|
||||||
prefix: "-2F",
|
prefix: "-2F",
|
||||||
sortOrder: 20,
|
sortOrder: 20,
|
||||||
|
sectionTotalPower: 0,
|
||||||
|
components: [],
|
||||||
circuits: [],
|
circuits: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,10 @@ describe("circuit group numbering", () => {
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
components: [
|
components: [
|
||||||
|
|
@ -235,7 +238,10 @@ describe("circuit group numbering", () => {
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -250,7 +256,10 @@ describe("circuit group numbering", () => {
|
||||||
sortOrder: 20,
|
sortOrder: 20,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -328,6 +337,9 @@ describe("circuit group numbering", () => {
|
||||||
voltage: 230,
|
voltage: 230,
|
||||||
isReserve: false,
|
isReserve: false,
|
||||||
circuitTotalPower: 0.1,
|
circuitTotalPower: 0.1,
|
||||||
|
circuitApparentPower: 0.1 / 0.95,
|
||||||
|
circuitCosPhi: 0.95,
|
||||||
|
downstreamBoardLoads: [],
|
||||||
protectionDevice: {
|
protectionDevice: {
|
||||||
type: "LS" as const,
|
type: "LS" as const,
|
||||||
ratedCurrentA: 10,
|
ratedCurrentA: 10,
|
||||||
|
|
@ -342,6 +354,7 @@ describe("circuit group numbering", () => {
|
||||||
displayName: "Leuchte Flur",
|
displayName: "Leuchte Flur",
|
||||||
phaseType: "single_phase",
|
phaseType: "single_phase",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
|
manualQuantity: 2,
|
||||||
powerPerUnit: 0.05,
|
powerPerUnit: 0.05,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
cosPhi: 0.95,
|
cosPhi: 0.95,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ function circuit(
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +42,8 @@ function section(
|
||||||
displayName: id,
|
displayName: id,
|
||||||
prefix,
|
prefix,
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
|
sectionTotalPower: 0,
|
||||||
|
components: [],
|
||||||
circuits: circuits.map((entry) => ({
|
circuits: circuits.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
sectionId: id,
|
sectionId: id,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ function circuit(
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ function circuit(
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
isReserve: true,
|
isReserve: true,
|
||||||
circuitTotalPower: 0,
|
circuitTotalPower: 0,
|
||||||
|
circuitApparentPower: 0,
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows: [],
|
deviceRows: [],
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +127,7 @@ describe("circuit structure projection", () => {
|
||||||
name: "Leuchte",
|
name: "Leuchte",
|
||||||
displayName: "Leuchte",
|
displayName: "Leuchte",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
powerPerUnit: 0.1,
|
powerPerUnit: 0.1,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
rowTotalPower: 0.1,
|
rowTotalPower: 0.1,
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ describe("application repository database contexts", () => {
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
installedPowerPerUnitKw: 0.2,
|
installedPowerPerUnitKw: 0.2,
|
||||||
demandFactor: 1,
|
demandFactor: 1,
|
||||||
|
phaseCount: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal((await firstGlobalDevices.list()).length, 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");
|
const undo = history.getNextCommand("project-1", "undo");
|
||||||
assert.ok(undo);
|
assert.ok(undo);
|
||||||
assertDistributionBoardFeederCircuitProjectCommand(undo.command);
|
const undoCommand = undo.command;
|
||||||
|
assertDistributionBoardFeederCircuitProjectCommand(undoCommand);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
repository.executeFeederCircuit({
|
repository.executeFeederCircuit({
|
||||||
|
|
@ -216,7 +217,7 @@ describe("distribution-board feeder-circuit project command", () => {
|
||||||
expectedRevision: 3,
|
expectedRevision: 3,
|
||||||
source: "undo",
|
source: "undo",
|
||||||
historyTargetChangeSetId: undo.changeSetId,
|
historyTargetChangeSetId: undo.changeSetId,
|
||||||
command: undo.command,
|
command: undoCommand,
|
||||||
}),
|
}),
|
||||||
/changed before history removal/
|
/changed before history removal/
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot {
|
||||||
simultaneityFactor: 0.8,
|
simultaneityFactor: 0.8,
|
||||||
feedingDistributionBoardId: "board-upstream",
|
feedingDistributionBoardId: "board-upstream",
|
||||||
feedingCircuitId: "circuit-upstream",
|
feedingCircuitId: "circuit-upstream",
|
||||||
|
boardNumber: null,
|
||||||
|
location: null,
|
||||||
|
fedFrom: null,
|
||||||
|
remark: null,
|
||||||
},
|
},
|
||||||
circuitList: {
|
circuitList: {
|
||||||
id: "list-1",
|
id: "list-1",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import ExcelJS from "exceljs";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import { createDatabaseContext } from "../src/db/database-context.js";
|
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 { 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 { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
|
||||||
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.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 deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
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)!;
|
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();
|
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 followBytes = new Uint8Array(await workbook.xlsx.writeBuffer());
|
||||||
const current = new ExternalModelStateRepository(context.db).getByProject("project-1").state;
|
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 }]));
|
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("o1", lowerId);
|
||||||
insert.run("o2", upperId);
|
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/);
|
assert.throws(() => insert.run("o3", lowerId), /UNIQUE constraint failed/);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import ExcelJS from "exceljs";
|
|
||||||
import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js";
|
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 { 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 { 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 { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
|
||||||
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.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 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 deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||||
|
|
@ -26,7 +25,7 @@ async function initialState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changedWorkbook(firstId = firstWorksharingId) {
|
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]!;
|
const sheet = workbook.worksheets[0]!;
|
||||||
sheet.getCell(3, 1).value = firstId; sheet.getCell(3, 6).value = 3;
|
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;
|
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("P4").value, "bleibt");
|
||||||
assert.equal(worksheet.getCell("L3").numFmt, "0.00");
|
assert.equal(worksheet.getCell("L3").numFmt, "0.00");
|
||||||
assert.equal(worksheet.getColumn(1).width, 12);
|
assert.equal(worksheet.getColumn(1).width, 12);
|
||||||
|
const worksharingNote = worksheet.getCell("A1").note;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof worksheet.getCell("A1").note === "string"
|
typeof worksharingNote === "string"
|
||||||
? worksheet.getCell("A1").note
|
? worksharingNote
|
||||||
: worksheet.getCell("A1").note?.texts?.[0]?.text,
|
: worksharingNote?.texts?.[0]?.text,
|
||||||
"Kommentar Worksharing-ID"
|
"Kommentar Worksharing-ID"
|
||||||
);
|
);
|
||||||
const commentedCells: string[] = [];
|
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 =
|
export const externalXlsxTestConfiguration: ExternalXlsxTransportConfiguration =
|
||||||
createDefaultExternalXlsxTransportConfiguration();
|
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?: {
|
export async function createRevitXlsxFixture(options?: {
|
||||||
duplicateWorksharingId?: boolean;
|
duplicateWorksharingId?: boolean;
|
||||||
secondWorksharingId?: string;
|
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,
|
(context.sqlite.prepare("SELECT quantity FROM circuit_device_rows WHERE id = 'r'").get() as { quantity: number }).quantity,
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
assert.equal(context.sqlite.prepare("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_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_configurations").get()!.count, 0);
|
||||||
assert.throws(() => context.sqlite.prepare("SELECT * FROM external_csv_configurations").all(), /no such table/);
|
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 }>;
|
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[] = [
|
const floors: FloorDto[] = [
|
||||||
{ id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2 },
|
{ id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2, buildingId: null },
|
||||||
{ id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1 },
|
{ id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1, buildingId: null },
|
||||||
];
|
];
|
||||||
const floorNameById = new Map(floors.map((floor) => [floor.id, floor.name]));
|
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(board.feeding_circuit_id, null);
|
||||||
assert.equal(floor.building_id, null);
|
assert.equal(floor.building_id, null);
|
||||||
assert.equal(room.area_m2, 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<[], { count: number }>("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<[], { count: number }>("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<[], { count: number }>("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<[], { count: number }>("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<[], { count: number }>("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 estimate_rows").get()!.count, 0);
|
||||||
assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []);
|
assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []);
|
||||||
assert.equal(context.sqlite.pragma("integrity_check", { simple: true }), "ok");
|
assert.equal(context.sqlite.pragma("integrity_check", { simple: true }), "ok");
|
||||||
migrate(context.db, { migrationsFolder: source });
|
migrate(context.db, { migrationsFolder: source });
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,13 @@ function getSettings(context: DatabaseContext) {
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectSettingsInput = Parameters<
|
||||||
|
typeof createProjectSettingsUpdateProjectCommand
|
||||||
|
>[0];
|
||||||
|
|
||||||
function settings(
|
function settings(
|
||||||
overrides: Partial<Parameters<
|
overrides: Partial<ProjectSettingsInput> = {}
|
||||||
typeof createProjectSettingsUpdateProjectCommand
|
): ProjectSettingsInput {
|
||||||
>[0]> = {}
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
name: "Testprojekt",
|
name: "Testprojekt",
|
||||||
internalProjectNumber: "INT-1",
|
internalProjectNumber: "INT-1",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
serializeProjectStateSnapshot,
|
serializeProjectStateSnapshot,
|
||||||
} from "../src/domain/models/project-state-snapshot.model.js";
|
} from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
import { remapProjectState } from "../src/domain/models/project-transfer.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 { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js";
|
||||||
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js";
|
||||||
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
||||||
|
|
@ -156,7 +157,7 @@ function protectedSnapshot() {
|
||||||
displayName: "Beleuchtung 1",
|
displayName: "Beleuchtung 1",
|
||||||
prefix: "-1F1.",
|
prefix: "-1F1.",
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
category: "lighting" as const,
|
category: "lighting" as CircuitGroupCategory,
|
||||||
groupNumber: 1,
|
groupNumber: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -370,7 +371,7 @@ describe("project state snapshot model", () => {
|
||||||
it("rejects legacy CSV configuration and imported state with a German migration error", () => {
|
it("rejects legacy CSV configuration and imported state with a German migration error", () => {
|
||||||
const configured = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
const configured = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
||||||
configured.schemaVersion = 9 as typeof configured.schemaVersion;
|
configured.schemaVersion = 9 as typeof configured.schemaVersion;
|
||||||
configured.externalCsvConfiguration = {
|
(configured as Record<string, unknown>).externalCsvConfiguration = {
|
||||||
id: "legacy",
|
id: "legacy",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
configurationVersion: 1,
|
configurationVersion: 1,
|
||||||
|
|
@ -380,7 +381,7 @@ describe("project state snapshot model", () => {
|
||||||
|
|
||||||
const imported = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
const imported = withoutVersionNineFields(structuredClone(minimalSnapshot()));
|
||||||
imported.schemaVersion = 9 as typeof imported.schemaVersion;
|
imported.schemaVersion = 9 as typeof imported.schemaVersion;
|
||||||
imported.externalModel.source = {
|
(imported.externalModel as Record<string, unknown>).source = {
|
||||||
id: "legacy-source",
|
id: "legacy-source",
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
name: "Legacy",
|
name: "Legacy",
|
||||||
|
|
@ -446,7 +447,7 @@ describe("project state snapshot model", () => {
|
||||||
const previous = withoutVersionSixFields(protectedSnapshot());
|
const previous = withoutVersionSixFields(protectedSnapshot());
|
||||||
previous.schemaVersion = 4 as typeof previous.schemaVersion;
|
previous.schemaVersion = 4 as typeof previous.schemaVersion;
|
||||||
previous.circuits[0]!.isReserve = false;
|
previous.circuits[0]!.isReserve = false;
|
||||||
previous.circuits[0]!.deviceRows = [{
|
(previous.circuits[0] as Record<string, unknown>).deviceRows = [{
|
||||||
id: "row-1",
|
id: "row-1",
|
||||||
circuitId: "circuit-1",
|
circuitId: "circuit-1",
|
||||||
linkedProjectDeviceId: null,
|
linkedProjectDeviceId: null,
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,10 @@ describe("project settings presentation", () => {
|
||||||
"MSR",
|
"MSR",
|
||||||
"SiBe",
|
"SiBe",
|
||||||
],
|
],
|
||||||
|
siteAddress: null,
|
||||||
|
workStage: null,
|
||||||
|
projectLead: null,
|
||||||
|
projectEditor: null,
|
||||||
currentRevision: 0,
|
currentRevision: 0,
|
||||||
},
|
},
|
||||||
usedDistributionBoardSupplyTypes: ["SiBe"],
|
usedDistributionBoardSupplyTypes: ["SiBe"],
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import ExcelJS from "exceljs";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import { createDatabaseContext } from "../src/db/database-context.js";
|
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 { projectExternalXlsxReturn } from "../src/external-model/application/external-xlsx-return-projection.js";
|
||||||
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
||||||
import { createExternalXlsxReturn } from "../src/external-model/xlsx/external-xlsx-transport.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 projectId = "project-e2e";
|
||||||
const boardId = "board-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 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 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 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 followBytes = new Uint8Array(await workbook.xlsx.writeBuffer());
|
||||||
const returnValues = new Map(projected.map((entry) => [entry.objectId, entry.values]));
|
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 });
|
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();
|
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 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")}`; }
|
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.
|
* Die App wird nur DYNAMISCH in createApp() importiert, damit das Routen-Inventar greift.
|
||||||
* Testdaten sind synthetisch; die Datenbank liegt nur im Speicher dieses Testprozesses.
|
* Testdaten sind synthetisch; die Datenbank liegt nur im Speicher dieses Testprozesses.
|
||||||
*/
|
*/
|
||||||
import path from "node:path";
|
|
||||||
import type { Express } from "express";
|
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 Headers = Record<string, string>;
|
||||||
type Case = { method: string; path: string; body?: unknown };
|
type Case = { method: string; path: string; body?: unknown };
|
||||||
|
|
@ -46,22 +50,22 @@ export async function signIn(baseUrl: string, username: string): Promise<Headers
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
async createApp(): Promise<Express> {
|
async createApp(): Promise<InventoriedExpress> {
|
||||||
process.env.LB_DATABASE_PATH = ":memory:";
|
process.env.LB_DATABASE_PATH = ":memory:";
|
||||||
process.env.LOG_LEVEL ??= "error";
|
process.env.LOG_LEVEL ??= "error";
|
||||||
delete process.env.AUTH_BOOTSTRAP_USERNAME;
|
delete process.env.AUTH_BOOTSTRAP_USERNAME;
|
||||||
delete process.env.AUTH_BOOTSTRAP_PASSWORT;
|
delete process.env.AUTH_BOOTSTRAP_PASSWORT;
|
||||||
|
|
||||||
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
|
||||||
const { db } = await import("../../src/db/client.js");
|
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");
|
const { userService } = await import("../../src/server/composition/auth.js");
|
||||||
for (const username of TEST_USERS) {
|
for (const username of TEST_USERS) {
|
||||||
await userService.createUser({ username, name: username, role: "user", initialPassword: START_PASSWORD });
|
await userService.createUser({ username, name: username, role: "user", initialPassword: START_PASSWORD });
|
||||||
}
|
}
|
||||||
const { createApp } = await import("../../src/server/app.js");
|
const { createApp } = await import("../../src/server/app.js");
|
||||||
return createApp();
|
return createApp() as InventoriedExpress;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Ohne Login erreichbar: Healthcheck und die offenen Pfade von createAuthGuard.
|
// 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