import { describe, expect, it } from "vitest"; import { extractHeaders } from "@/domain/spreadsheet-adapter/headers"; import { validateMappings } from "@/domain/spreadsheet-adapter/mapping"; import { inferColumns } from "@/domain/spreadsheet-adapter/typing"; import { transformRows } from "@/domain/spreadsheet-adapter/transform"; import { canManageSpreadsheetModels, canUploadSpreadsheetModel, canUseSpreadsheetAdapter } from "@/domain/spreadsheet-adapter/permissions"; import { neutralizeSpreadsheetFormula } from "@/domain/spreadsheet-adapter/csv-injection"; import { buildOutputFilename, sanitizeDisplayName } from "@/domain/spreadsheet-adapter/filenames"; import { canTransition } from "@/domain/spreadsheet-adapter/job-state"; import { detectSpreadsheetFormat } from "@/domain/spreadsheet-adapter/file-signature"; import { parseCsvText } from "@/domain/spreadsheet-adapter/csv"; import { queryKeys } from "@/domain/spreadsheet-adapter/query-keys"; import { MAX_FILE_BYTES } from "@/domain/spreadsheet-adapter/limits"; import { MemoryCoordinator } from "@/infra/redis/memory"; import { scrubSentryEvent } from "@/infra/observability/scrub"; describe("domínio", () => { it("normaliza cabeçalho e recusa vazio ou duplicado", () => { expect(extractHeaders([" Nome ", "Valor", ""])).toEqual(["Nome", "Valor"]); expect(() => extractHeaders(["A", "a"])).toThrow(/duplicado/i); expect(() => extractHeaders(["A", "", "B"])).toThrow(/cabeçalho/i); }); it("detecta tipo misto e transforma na ordem do modelo", () => { const columns = inferColumns(["Valor", "Nome"], [["10", "Ana"], ["texto", "Bia"]]); expect(columns[0].inferredType).toBe("mixed"); const rows = transformRows(["Nome", "Valor"], ["Valor", "Nome"], [["10", "Ana"]], [{ sourceColumn: "Nome", modelColumn: "Nome" }, { sourceColumn: "Valor", modelColumn: "Valor" }]); expect(rows).toEqual([["Ana", "10"]]); }); it("exige mapeamento 1:1 e obrigatórias", () => { const model = [{ name: "Nome", required: true }, { name: "Obs", required: false }]; expect(validateMappings(model, ["A", "B"], [{ sourceColumn: "A", modelColumn: "Nome" }]).ok).toBe(true); expect(validateMappings(model, ["A"], []).code).toBe("MAPPING_INCOMPLETE"); expect(validateMappings(model, ["A", "B"], [{ sourceColumn: "A", modelColumn: "Nome" }, { sourceColumn: "B", modelColumn: "Nome" }]).code).toBe("MAPPING_CONFLICT"); }); it("aplica permissões sem atalho de superadmin", () => { expect(canUseSpreadsheetAdapter({ role: "agent", moduleEnabled: false })).toBe(false); expect(canManageSpreadsheetModels({ role: "gestor", moduleEnabled: true })).toBe(false); expect(canManageSpreadsheetModels({ role: "admin", moduleEnabled: true })).toBe(true); expect(canUploadSpreadsheetModel({ role: "agent", moduleEnabled: true, allowNonAdminModelUpload: false })).toBe(false); expect(canUploadSpreadsheetModel({ role: "agent", moduleEnabled: true, allowNonAdminModelUpload: true })).toBe(true); expect(canTransition("ready", "queued")).toBe(true); expect(canTransition("completed", "processing")).toBe(false); }); it("neutraliza CSV injection e nomes perigosos", () => { expect(neutralizeSpreadsheetFormula("=1+1")).toBe("'=1+1"); expect(neutralizeSpreadsheetFormula("-12")).toBe("-12"); expect(sanitizeDisplayName("../etc/passwd.csv")).toBe("passwd.csv"); expect(buildOutputFilename("Relatório Ágil", "xlsx", new Date("2026-10-02T00:00:00.000Z"))).toMatch(/^adaptado_Relatorio_Agil_/); expect(() => detectSpreadsheetFormat("grande.csv", Buffer.alloc(MAX_FILE_BYTES + 1, 65))).toThrow(/10 MB/); expect(parseCsvText('a;b\n"1;2";3')).toEqual([["a", "b"], ["1;2", "3"]]); }); it("isola cache por workspace e o lock por dono", async () => { expect(queryKeys.models("a")[1]).toBe("a"); expect(queryKeys.models("a")).not.toEqual(queryKeys.models("b")); let now = 0; const locks = new MemoryCoordinator(() => now); expect(await locks.acquireLock("k", "job-1", 10)).toBe(true); expect(await locks.acquireLock("k", "job-2", 10)).toBe(false); now = 11_000; expect(await locks.acquireLock("k", "job-2", 10)).toBe(true); const event = scrubSentryEvent({ request: { data: "planilha", headers: { authorization: "secret", "x-request-id": "1" } }, extra: { cells: ["segredo"], job_id: "j" }, breadcrumbs: [{ message: "row" }], }); expect(event.request?.data).toBeUndefined(); expect(event.request?.headers?.authorization).toBeUndefined(); expect(event.request?.headers?.["x-request-id"]).toBe("1"); expect(event.extra?.cells).toBeUndefined(); expect(event.extra?.job_id).toBe("j"); expect(event.breadcrumbs).toEqual([]); }); });