feat(import): harden workbook parser boundaries
This commit is contained in:
@@ -5,12 +5,23 @@ import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
MAX_DISPO_WORKBOOK_BYTES,
|
||||
MAX_DISPO_WORKBOOK_COLUMNS,
|
||||
MAX_DISPO_WORKBOOK_ROWS,
|
||||
readWorksheetMatrix,
|
||||
} from "../use-cases/dispo-import/read-workbook.js";
|
||||
|
||||
const referenceWorkbookPath = fileURLToPath(
|
||||
new URL("../../../../samples/Dispov2/MandatoryDispoCategories_V3.xlsx", import.meta.url),
|
||||
);
|
||||
const chargeabilityWorkbookPath = fileURLToPath(
|
||||
new URL(
|
||||
"../../../../samples/Dispov2/20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const planningWorkbookPath = fileURLToPath(
|
||||
new URL("../../../../samples/Dispov2/DISPO_2026.xlsx", import.meta.url),
|
||||
);
|
||||
|
||||
const tempDirectories: string[] = [];
|
||||
|
||||
@@ -28,6 +39,18 @@ async function makeTempDirectory(): Promise<string> {
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function writeWorkbook(filePath: string, rows: unknown[][], sheetName = "Sheet1"): Promise<void> {
|
||||
const ExcelJS = await import("exceljs");
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet(sheetName);
|
||||
|
||||
for (const row of rows) {
|
||||
worksheet.addRow(row);
|
||||
}
|
||||
|
||||
await workbook.xlsx.writeFile(filePath);
|
||||
}
|
||||
|
||||
describe("readWorksheetMatrix", () => {
|
||||
it("reads trusted xlsx worksheets through the hardened reader", async () => {
|
||||
const rows = await readWorksheetMatrix(referenceWorkbookPath, "EID-Attr");
|
||||
@@ -36,6 +59,21 @@ describe("readWorksheetMatrix", () => {
|
||||
expect(rows.some((row) => row.length > 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("tolerates workbook tables that contain unsupported exceljs date group filters", async () => {
|
||||
const rows = await readWorksheetMatrix(chargeabilityWorkbookPath, "ChgFC");
|
||||
|
||||
expect(rows.length).toBeGreaterThan(300);
|
||||
expect(rows[0]?.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it("accepts real dispo planning worksheets within the supported width envelope", async () => {
|
||||
const rows = await readWorksheetMatrix(planningWorkbookPath, "Dispo");
|
||||
|
||||
expect(rows.length).toBeGreaterThan(500);
|
||||
expect(rows.some((row) => row.length > 256)).toBe(true);
|
||||
expect(rows.every((row) => row.length <= MAX_DISPO_WORKBOOK_COLUMNS)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects legacy .xls workbook paths", async () => {
|
||||
const directory = await makeTempDirectory();
|
||||
const legacyPath = path.join(directory, "legacy-input.xls");
|
||||
@@ -55,4 +93,30 @@ describe("readWorksheetMatrix", () => {
|
||||
"Workbook file exceeds the",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects worksheets that exceed the row limit", async () => {
|
||||
const directory = await makeTempDirectory();
|
||||
const workbookPath = path.join(directory, "too-many-rows.xlsx");
|
||||
await writeWorkbook(
|
||||
workbookPath,
|
||||
Array.from({ length: MAX_DISPO_WORKBOOK_ROWS + 1 }, (_, index) => [`row-${index + 1}`]),
|
||||
);
|
||||
|
||||
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
||||
`exceeds the ${MAX_DISPO_WORKBOOK_ROWS} row import limit`,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects worksheets that exceed the column limit", async () => {
|
||||
const directory = await makeTempDirectory();
|
||||
const workbookPath = path.join(directory, "too-many-columns.xlsx");
|
||||
await writeWorkbook(
|
||||
workbookPath,
|
||||
[Array.from({ length: MAX_DISPO_WORKBOOK_COLUMNS + 1 }, (_, index) => `col-${index + 1}`)],
|
||||
);
|
||||
|
||||
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
||||
`exceeds the ${MAX_DISPO_WORKBOOK_COLUMNS} column import limit`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user