import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { ExternalCsvParseError, parseExternalCsv, serializeExternalCsv, } from "../src/external-model/csv/external-csv-transport.js"; import { externalCsvTestConfiguration, firstIfcGuid, quotedRevitCsv, utf8Bytes, } from "./fixtures/revit-csv-fixtures.js"; describe("external CSV transport", () => { it("detects the observed Revit dialect and classifies every row", () => { const document = parseExternalCsv( utf8Bytes(quotedRevitCsv, true), externalCsvTestConfiguration ); assert.deepEqual(document.dialect, { encoding: "utf-8", hasBom: true, delimiter: ";", lineEnding: "\r\n", quoteCharacter: '"', quoteAllFields: true, hasTrailingLineEnding: true, }); assert.equal(document.headerRowIndex, 1); assert.deepEqual( document.rows.map((row) => row.classification), [ "metadata", "header", "passthrough", "object", "passthrough", "suspect-object", "object", ] ); assert.equal(document.rows[6].cells[2].value, 'Büro "Nord"'); }); it("round-trips BOM, CRLF, quotation, order and cell values byte for byte", () => { const source = utf8Bytes(quotedRevitCsv, true); const document = parseExternalCsv(source, externalCsvTestConfiguration); assert.deepEqual(serializeExternalCsv(document), source); }); it("parses exports without an optional quantity column", () => { const configuration = { ...externalCsvTestConfiguration, columns: { ...externalCsvTestConfiguration.columns, quantity: null, }, }; const csv = [ '"Projekt-Export";"";"";"";"";"";""', '"Stromkreis";"Raumnummer";"Raumname";"Familie und Typ";"CAx_Auswahlkenner";"Elektrische Leistung";"IfcGUID"', `"UV_AV_01-2F1";"01/101";"Technik";"Steckdose: Standard";"Arbeitsplatz";"100";"${firstIfcGuid}"`, ].join("\r\n") + "\r\n"; const document = parseExternalCsv(utf8Bytes(csv, true), configuration); assert.equal( document.rows.filter((row) => row.classification === "object").length, 1 ); }); it("preserves embedded delimiters and line endings inside quoted cells", () => { const csv = quotedRevitCsv.replace( '"Steckdose: Doppelsteckdose"', '"Steckdose; Doppel\r\nsteckdose"' ); const source = utf8Bytes(csv, true); const document = parseExternalCsv(source, externalCsvTestConfiguration); assert.equal( document.rows[3].cells[3].value, "Steckdose; Doppel\r\nsteckdose" ); assert.deepEqual(serializeExternalCsv(document), source); }); it("blocks missing and ambiguous configured headers", () => { assertParseError( utf8Bytes(quotedRevitCsv.replace("IfcGUID", "Andere ID")), "header-not-found" ); const duplicatedHeader = quotedRevitCsv.replace( '"";"";"";"";"";"";"";""', quotedRevitCsv.split("\r\n")[1] ); assertParseError(utf8Bytes(duplicatedHeader), "ambiguous-header"); const duplicateConfiguredColumn = quotedRevitCsv.replace( '"Stromkreis";"Raumnummer"', '"Stromkreis";"Raumnummer";"Raumnummer"' ); assertParseError( utf8Bytes(duplicateConfiguredColumn), "ambiguous-header" ); }); it("blocks invalid and duplicate non-empty IfcGUIDs", () => { assertParseError( utf8Bytes(quotedRevitCsv.replace(firstIfcGuid, "not-an-ifc-guid")), "invalid-ifc-guid" ); assertParseError( utf8Bytes(quotedRevitCsv.replace(firstIfcGuid, ` ${firstIfcGuid}`)), "invalid-ifc-guid" ); const duplicate = quotedRevitCsv.replace( "1AbCdEfGhIjKlMnOpQrStu", firstIfcGuid ); assertParseError(utf8Bytes(duplicate), "duplicate-ifc-guid"); }); it("blocks invalid UTF-8, unterminated quotes and mixed row endings", () => { assertParseError(new Uint8Array([0xff, 0xfe, 0xfd]), "invalid-encoding"); assertParseError( utf8Bytes(quotedRevitCsv.replace(/"\r\n$/, "\r\n")), "invalid-csv" ); assertParseError( utf8Bytes(quotedRevitCsv.replace("\r\n", "\n")), "invalid-csv" ); }); }); function assertParseError(input: Uint8Array, code: ExternalCsvParseError["code"]) { assert.throws( () => parseExternalCsv(input, externalCsvTestConfiguration), (error: unknown) => error instanceof ExternalCsvParseError && error.code === code ); }