O módulo usa a identidade Supabase, isola cada workspace e registra storage, TTL, runner e biblioteca XLSX como decisões ainda abertas. Co-authored-by: Cursor <cursoragent@cursor.com>
79 lines
4.6 KiB
TypeScript
79 lines
4.6 KiB
TypeScript
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([]);
|
|
});
|
|
});
|