import { describe, expect, it } from "vitest"; import { crc16Hex, crc16Kermit, normalizeAfdCpf, parseAfdFile, parseAfdLine } from "../src/hr-timeclock/afd-parser.js"; /** * Pure-function unit tests for the AFD parser — no DB, no Nest app, unlike * every other spec in this directory (which are DB-backed integration * tests per vitest.config.ts's own doc comment). Placed here anyway * because `include: ["test/**\/*.spec.ts"]` is the only pattern vitest * picks up for @eden/api; these tests just happen not to need Postgres. */ describe("crc16Kermit", () => { it("matches the known CRC-16/KERMIT test vector for ASCII '123456789'", () => { expect(crc16Kermit("123456789")).toBe(0x2189); expect(crc16Hex("123456789")).toBe("2189"); }); }); describe("normalizeAfdCpf", () => { it("takes the last 11 digits of a wider raw field", () => { expect(normalizeAfdCpf("01234567890X")).toBe("01234567890"); expect(normalizeAfdCpf(" 123.456.789-01 ")).toBe("12345678901"); }); it("never invents padding for a short field", () => { expect(normalizeAfdCpf("123")).toBe("123"); expect(normalizeAfdCpf(null)).toBeNull(); expect(normalizeAfdCpf("")).toBeNull(); }); }); function buildType3Line(nsr: string, dh: string, cpf: string): string { const dataPart = `${nsr}3${dh}${cpf}`; return dataPart + crc16Hex(dataPart); } describe("parseAfdLine — type 3 (marcação REP-C/REP-A)", () => { it("parses a well-formed, CRC-valid punch line", () => { const line = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); const record = parseAfdLine(line, 1); expect(record.nsr).toBe(1); expect(record.recordType).toBe("3"); expect(record.crcValid).toBe("VALID"); expect(record.validationStatus).toBe("VALID"); expect(record.employeeCpfRaw).toBe("01234567890X"); expect(record.eventDatetime?.toISOString()).toBe(new Date("2026-08-06T08:30:00-03:00").toISOString()); }); it("flags a tampered CRC as INVALID but still preserves the raw line", () => { const line = buildType3Line("000000002", "2026-08-06T08:30:00-0300", "01234567890X"); const tampered = line.slice(0, -1) + (line.at(-1) === "0" ? "1" : "0"); const record = parseAfdLine(tampered, 2); expect(record.crcValid).toBe("INVALID"); expect(record.validationStatus).toBe("INVALID"); expect(record.rawLine).toBe(tampered); expect(record.validationNotes).toMatch(/CRC divergente/); }); it("never invents an NSR — a non-numeric NSR field stays null", () => { const line = buildType3Line("ABCDEFGHI", "2026-08-06T08:30:00-0300", "01234567890X"); const record = parseAfdLine(line, 3); expect(record.nsr).toBeNull(); expect(record.nsrRaw).toBe("ABCDEFGHI"); expect(record.validationStatus).toBe("INVALID"); }); }); describe("parseAfdLine — type 7 (marcação REP-P)", () => { it("never recomputes the type-7 hash chain — crcValid is always NOT_APPLICABLE", () => { const dh = "2026-08-06T08:30:00-0300"; const line = "0000000037" + dh + "01234567890X" + dh + "01" + "O" + "a".repeat(64); const record = parseAfdLine(line, 1); expect(record.recordType).toBe("7"); expect(record.crcValid).toBe("NOT_APPLICABLE"); expect(record.crcCalculated).toBeNull(); }); }); describe("parseAfdLine — unsupported/malformed", () => { it("captures an unrecognized record type as UNSUPPORTED_LAYOUT, never dropped", () => { const record = parseAfdLine("000000005X" + "resto-nao-documentado", 5); expect(record.validationStatus).toBe("UNSUPPORTED_LAYOUT"); expect(record.parsedData.raw_tail).toBe("resto-nao-documentado"); }); it("preserves a too-short line instead of throwing", () => { const record = parseAfdLine("12345", 6); expect(record.validationStatus).toBe("INVALID"); expect(record.rawLine).toBe("12345"); expect(record.nsr).toBeNull(); }); }); describe("parseAfdFile — partial-error policy", () => { it("keeps parsing all lines even when some are malformed, never dropping raw data", () => { const good = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); const bad = "short"; const content = [good, bad].join("\r\n"); const result = parseAfdFile(content); expect(result.records).toHaveLength(2); expect(result.failures).toHaveLength(0); // "bad" is malformed-but-handled, not an unexpected exception expect(result.summary.byValidationStatus.VALID).toBe(1); expect(result.summary.byValidationStatus.INVALID).toBe(1); expect(result.summary.firstNsr).toBe(1); expect(result.summary.lastNsr).toBe(1); }); it("skips blank trailing lines without generating a phantom record", () => { const good = buildType3Line("000000001", "2026-08-06T08:30:00-0300", "01234567890X"); const result = parseAfdFile(good + "\r\n"); expect(result.records).toHaveLength(1); }); });