Implementa o MVP do Adaptador de Planilhas no Elo Pro.

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>
This commit is contained in:
camilafagundesandCursor committed 2026-10-02 23:02:00 -03:00
commit 6e23f1533b
144 files changed
+19060

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
/**
* @vitest-environment jsdom
*/
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Dialog, JourneyTrail, Select, Switch } from "@/components/ui";
import { EmptyState, ErrorState, LoadingBlock } from "@/components/states";
describe("estados e controles", () => {
it("mostra loading, vazio, erro e switch acessível", async () => {
const user = userEvent.setup();
let checked = false;
const view = render(
<div>
<LoadingBlock />
<EmptyState title="Nada aqui" description="Cadastre um modelo." />
<ErrorState message="Falha recuperável" onRetry={() => undefined} />
<Switch label="Permitir upload" checked={checked} onCheckedChange={(value) => { checked = value; }} />
<JourneyTrail steps={["Modelo", "Fontes"]} current={0} />
<Select aria-label="Coluna fonte">
<option value="">Não mapear</option>
<option value="Nome">Nome</option>
</Select>
<Dialog open title="Critérios" onClose={() => undefined}>Formatos XLSX e CSV.</Dialog>
</div>,
);
expect(screen.getByRole("status")).toBeTruthy();
expect(screen.getByText("Nada aqui")).toBeTruthy();
expect(screen.getByRole("alert")).toBeTruthy();
expect(screen.getByRole("switch", { name: "Permitir upload" }).getAttribute("aria-checked")).toBe("false");
await user.click(screen.getByRole("switch", { name: "Permitir upload" }));
expect(checked).toBe(true);
expect(screen.getByRole("dialog", { name: "Critérios" })).toBeTruthy();
await user.selectOptions(screen.getByRole("combobox", { name: "Coluna fonte" }), "Nome");
expect((screen.getByRole("combobox", { name: "Coluna fonte" }) as HTMLSelectElement).value).toBe("Nome");
view.unmount();
});
});
+1
View File
@@ -0,0 +1 @@
export {};
+56
View File
@@ -0,0 +1,56 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { createMemoryDb } from "@/infra/db/pglite";
import { migrate } from "@/infra/db/migrate";
import type { Db } from "@/infra/db/types";
import { MemoryCoordinator } from "@/infra/redis/memory";
import { MemoryStorage } from "@/infra/storage/memory";
import type { AppDeps } from "@/server/spreadsheet-adapter/deps";
import { FIXTURES } from "@/dev/fixtures";
import type { RequestContext, Role } from "@/domain/spreadsheet-adapter/types";
export const IDS = FIXTURES;
export async function createReadyDb(): Promise<Db> {
const db = await createMemoryDb();
await migrate(db);
await db.withService(async (query) => {
await query.query(`insert into public.profiles (id, display_name, is_superadmin) values
($1,'Ana Owner', false), ($2,'Bruno Agent', false), ($3,'Carla Gestor', false), ($4,'Diego Admin', false), ($5,'Eva Agent', true)`, [
IDS.ana, IDS.bruno, IDS.carla, IDS.diego, IDS.eva,
]);
await query.query(`insert into public.workspaces (id, name) values ($1,'Alpha'), ($2,'Beta')`, [IDS.alpha, IDS.beta]);
await query.query(
`insert into public.workspace_members (workspace_id, user_id, role) values
($1,$2,'owner'), ($1,$3,'agent'), ($1,$4,'gestor'), ($5,$6,'admin'), ($5,$7,'agent')`,
[IDS.alpha, IDS.ana, IDS.bruno, IDS.carla, IDS.beta, IDS.diego, IDS.eva],
);
await query.query(
`insert into public.workspace_module_flags (workspace_id, module_key, enabled) values
($1,'spreadsheet_adapter', true), ($2,'spreadsheet_adapter', false)`,
[IDS.alpha, IDS.beta],
);
});
return db;
}
export function depsOf(db: Db, coordinator = new MemoryCoordinator()): AppDeps & { storage: MemoryStorage } {
return { db, storage: new MemoryStorage(), coordinator, ttlSeconds: 3600, now: () => new Date("2026-10-02T12:00:00.000Z") };
}
export function ctxOf(user: keyof typeof IDS, workspace: "alpha" | "beta", role: Role, moduleEnabled: boolean, allow = false): RequestContext {
return {
userId: IDS[user],
displayName: user,
isSuperadmin: user === "eva",
workspaceId: IDS[workspace],
workspaceName: workspace,
role,
moduleEnabled,
allowNonAdminModelUpload: allow,
};
}
export async function readDownSql(): Promise<string> {
return readFile(path.join(process.cwd(), "migrations", "down", "0002_spreadsheet_adapter_down.sql"), "utf8");
}
+157
View File
@@ -0,0 +1,157 @@
import ExcelJS from "exceljs";
import { describe, expect, it } from "vitest";
import {
addSource,
createJob,
createModel,
deleteModel,
getSettings,
previewSource,
rejectWorkspaceForgery,
saveMappings,
startProcess,
updateSettings,
updateSource,
} from "@/server/spreadsheet-adapter/service";
import { cleanupExpired, downloadOutput } from "@/server/spreadsheet-adapter/process-job";
import { UnavailableCoordinator } from "@/infra/redis/memory";
import { ctxOf, createReadyDb, depsOf, IDS } from "../helpers/world";
async function workbook(sheets: Record<string, string[][]>): Promise<Buffer> {
const book = new ExcelJS.Workbook();
for (const [name, rows] of Object.entries(sheets)) {
const sheet = book.addWorksheet(name);
rows.forEach((row) => sheet.addRow(row));
}
return Buffer.from(await book.xlsx.writeBuffer());
}
describe("jornada do adaptador", () => {
it("cadastra modelo, mapeia, revisa, processa e baixa sem alterar o modelo", async () => {
const db = await createReadyDb();
const deps = depsOf(db);
const ana = ctxOf("ana", "alpha", "owner", true);
const model = await createModel(deps, ana, {
name: "Clientes",
filename: "modelo.csv",
bytes: Buffer.from("Nome,Valor\n", "utf8"),
requiredColumns: ["Nome", "Valor"],
});
const original = await deps.storage.get(`workspaces/${IDS.alpha}/models/${model.id}/original`);
const job = await createJob(deps, ana, model.id, "job-1");
const source = await addSource(deps, ana, job.id, {
filename: "fonte.csv",
bytes: Buffer.from("Nome,Valor\n=CMD(),10\nBruno,20\n", "utf8"),
});
expect(source.httpStatus).toBe(200);
const sourceId = source.job.sources[0].id;
await saveMappings(deps, ana, job.id, sourceId, [
{ sourceColumn: "Nome", modelColumn: "Nome" },
{ sourceColumn: "Valor", modelColumn: "Valor" },
]);
const preview = await previewSource(deps, ana, job.id, sourceId);
expect(preview.preview).toBe(true);
expect(preview.columns.map((column) => column.name)).toEqual(["Nome", "Valor"]);
expect(preview.rows[0]).toEqual(["=CMD()", "10"]);
const done = await startProcess(deps, ana, job.id, sourceId, "proc-1");
expect(done.status).toBe("completed");
expect(done.progress?.percent).toBe(100);
const again = await startProcess(deps, ana, job.id, sourceId, "proc-1");
expect(again.status).toBe("completed");
const csv = await downloadOutput(deps, ana, job.id, sourceId, "csv");
expect(csv.body.toString("utf8")).toContain("'=CMD()");
expect(csv.filename.endsWith(".csv")).toBe(true);
const xlsx = await downloadOutput(deps, ana, job.id, sourceId, "xlsx");
expect(xlsx.body.subarray(0, 2).toString()).toBe("PK");
const pdf = await downloadOutput(deps, ana, job.id, sourceId, "pdf");
expect(pdf.body.subarray(0, 4).toString()).toBe("%PDF");
const after = await deps.storage.get(`workspaces/${IDS.alpha}/models/${model.id}/original`);
expect(after.equals(original)).toBe(true);
const audits = await db.withService((query) => query.query<{ metadata: unknown }>(`select metadata::text as metadata from public.audit_log`));
const blob = JSON.stringify(audits);
expect(blob).not.toContain("=CMD()");
expect(blob).not.toContain("CELULA");
await db.close();
});
it("bloqueia limites, duplicidade, papel, aba e job ativo", async () => {
const db = await createReadyDb();
const deps = depsOf(db);
const ana = ctxOf("ana", "alpha", "owner", true);
const bruno = ctxOf("bruno", "alpha", "agent", true, false);
await expect(createModel(deps, bruno, { name: "X", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toMatchObject({
code: "FORBIDDEN",
});
await updateSettings(deps, ana, true);
const settings = await getSettings(deps, ana);
expect(settings.allowNonAdminModelUpload).toBe(true);
const brunoAllowed = ctxOf("bruno", "alpha", "agent", true, true);
const uploaded = await createModel(deps, brunoAllowed, { name: "Operacional", filename: "a.csv", bytes: Buffer.from("A\n1\n") });
await expect(createModel(deps, ana, { name: "Operacional", filename: "a.csv", bytes: Buffer.from("B\n1\n") })).rejects.toMatchObject({
code: "DUPLICATE_MODEL_NAME",
});
await expect(deleteModel(deps, brunoAllowed, uploaded.id)).rejects.toMatchObject({ code: "FORBIDDEN" });
for (let index = 0; index < 9; index += 1) {
await createModel(deps, ana, { name: `Modelo ${index}`, filename: "a.csv", bytes: Buffer.from("A\n1\n") });
}
await expect(createModel(deps, ana, { name: "Modelo extra", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toMatchObject({
code: "TOO_MANY_MODELS",
});
const multi = await workbook({ Um: [["A"], ["1"]], Dois: [["B"], ["2"]] });
const fresh = await createReadyDb();
const deps2 = depsOf(fresh);
const model = await createModel(deps2, ana, { name: "Multi", filename: "livro.xlsx", bytes: multi, sheet: "Dois" });
expect(model.sheetName).toBe("Dois");
const job = await createJob(deps2, ana, model.id);
const pending = await addSource(deps2, ana, job.id, { filename: "fonte.xlsx", bytes: multi });
expect(pending.httpStatus).toBe(409);
expect(pending.code).toBe("SHEET_REQUIRED");
const chosen = await updateSource(deps2, ana, job.id, pending.job.sources[0].id, { sheet: "Um", headerRow: 1 });
expect(chosen.httpStatus).toBe(200);
expect(chosen.job.sources[0].selectedSheet).toBe("Um");
await saveMappings(deps2, ana, job.id, chosen.job.sources[0].id, [{ sourceColumn: "A", modelColumn: "B" }]);
await fresh.withUser(ana.userId, (query) =>
query.query(`update public.spreadsheet_jobs set status = 'processing' where id = $1`, [job.id]),
);
await expect(deleteModel(deps2, ana, model.id)).rejects.toMatchObject({ code: "MODEL_IN_USE" });
await fresh.close();
await db.close();
});
it("não processa quando o coordenador está indisponível", async () => {
const db = await createReadyDb();
const deps = depsOf(db, new UnavailableCoordinator());
const ana = ctxOf("ana", "alpha", "owner", true);
const model = await createModel(deps, ana, { name: "Base", filename: "a.csv", bytes: Buffer.from("A\n1\n") });
const job = await createJob(deps, ana, model.id);
const source = await addSource(deps, ana, job.id, { filename: "f.csv", bytes: Buffer.from("A\n9\n") });
await saveMappings(deps, ana, job.id, source.job.sources[0].id, [{ sourceColumn: "A", modelColumn: "A" }]);
await expect(startProcess(deps, ana, job.id, source.job.sources[0].id, "x")).rejects.toMatchObject({ code: "REDIS_UNAVAILABLE" });
const stored = await db.withService((query) => query.query<{ status: string }>(`select status from public.spreadsheet_jobs where id = $1`, [job.id]));
expect(stored[0].status).not.toBe("completed");
await db.close();
});
it("expira temporários sem apagar o modelo", async () => {
const db = await createReadyDb();
const deps = depsOf(db);
deps.ttlSeconds = -1;
const ana = ctxOf("ana", "alpha", "owner", true);
const model = await createModel(deps, ana, { name: "Permanente", filename: "a.csv", bytes: Buffer.from("A\n1\n") });
const modelKey = `workspaces/${IDS.alpha}/models/${model.id}/original`;
const job = await createJob(deps, ana, model.id);
await addSource(deps, ana, job.id, { filename: "f.csv", bytes: Buffer.from("A\n1\n") });
const removed = await cleanupExpired(deps);
expect(removed).toBe(1);
expect((await deps.storage.get(modelKey)).length).toBeGreaterThan(0);
await db.close();
});
it("rejeita workspace forjado", async () => {
const db = await createReadyDb();
const deps = depsOf(db);
const ana = ctxOf("ana", "alpha", "owner", true);
await expect(rejectWorkspaceForgery(deps, ana, IDS.beta)).rejects.toMatchObject({ code: "WORKSPACE_FORBIDDEN" });
await db.close();
});
});
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { createReadyDb, readDownSql } from "../helpers/world";
describe("migrations", () => {
it("sobe do zero e o rollback remove as tabelas do módulo", async () => {
const db = await createReadyDb();
const tables = await db.withService((query) =>
query.query<{ table_name: string }>(
`select table_name from information_schema.tables where table_schema = 'public' and table_name like 'spreadsheet_%'`,
),
);
expect(tables.map((row) => row.table_name).sort()).toEqual([
"spreadsheet_adapter_settings",
"spreadsheet_job_mappings",
"spreadsheet_job_sources",
"spreadsheet_jobs",
"spreadsheet_models",
]);
await db.exec(await readDownSql());
const after = await db.withService((query) =>
query.query<{ table_name: string }>(
`select table_name from information_schema.tables where table_schema = 'public' and table_name like 'spreadsheet_%'`,
),
);
expect(after).toHaveLength(0);
const profiles = await db.withService((query) => query.query(`select id from public.profiles`));
expect(profiles.length).toBe(5);
await db.close();
});
});
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { createModel } from "@/server/spreadsheet-adapter/service";
import { ctxOf, createReadyDb, depsOf, IDS } from "../helpers/world";
describe("RLS e permissões", () => {
it("impede leitura e escrita entre workspaces", async () => {
const db = await createReadyDb();
const deps = depsOf(db);
const ana = ctxOf("ana", "alpha", "owner", true);
const diego = ctxOf("diego", "beta", "admin", false);
await createModel(deps, ana, { name: "Somente Alpha", filename: "a.csv", bytes: Buffer.from("A\n1\n") });
const asDiego = await db.withUser(IDS.diego, (query) => query.query(`select name from public.spreadsheet_models`));
expect(asDiego).toHaveLength(0);
const asAna = await db.withUser(IDS.ana, (query) => query.query(`select name from public.spreadsheet_models where workspace_id = $1`, [IDS.beta]));
expect(asAna).toHaveLength(0);
await expect(
db.withUser(IDS.ana, (query) =>
query.query(
`insert into public.spreadsheet_models
(workspace_id, name, original_filename, format, storage_key, content_sha256, header_row, columns_schema, created_by)
values ($1,'Invasao','a.csv','csv','workspaces/x/models/y/original','abc',1,'[]'::jsonb,$2)`,
[IDS.beta, IDS.ana],
),
),
).rejects.toThrow(/row-level security/i);
await expect(createModel(deps, { ...diego, moduleEnabled: true }, { name: "Beta", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toMatchObject({
code: "FORBIDDEN",
});
const eva = ctxOf("eva", "beta", "agent", true, true);
await expect(createModel(deps, eva, { name: "Super", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toThrow();
await db.close();
});
it("agent não altera configuração e owner altera", async () => {
const db = await createReadyDb();
const ana = ctxOf("ana", "alpha", "owner", true);
const bruno = ctxOf("bruno", "alpha", "agent", true);
await expect(
db.withUser(bruno.userId, (query) =>
query.query(`insert into public.spreadsheet_adapter_settings (workspace_id, allow_non_admin_model_upload) values ($1, true)`, [IDS.alpha]),
),
).rejects.toThrow(/row-level security/i);
await db.withUser(ana.userId, (query) =>
query.query(`insert into public.spreadsheet_adapter_settings (workspace_id, allow_non_admin_model_upload) values ($1, true)`, [IDS.alpha]),
);
const seen = await db.withUser(bruno.userId, (query) =>
query.query<{ allow_non_admin_model_upload: boolean }>(`select allow_non_admin_model_upload from public.spreadsheet_adapter_settings`),
);
expect(seen[0].allow_non_admin_model_upload).toBe(true);
await db.close();
});
});
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { parseCsvText } from "@/domain/spreadsheet-adapter/csv";
import { gridFromRows } from "@/domain/spreadsheet-adapter/csv";
import { transformRows } from "@/domain/spreadsheet-adapter/transform";
import { buildCsv } from "@/infra/export/tabular";
describe("capacidade", () => {
it("transforma 50.000 linhas úteis sem perder registros", () => {
const lines = ["Origem"];
for (let index = 0; index < 50_000; index += 1) lines.push(String(index));
const grid = gridFromRows(parseCsvText(lines.join("\n")), 1, ["CSV"], "CSV");
expect(grid.rows).toHaveLength(50_000);
const output = transformRows(["Destino"], grid.headers, grid.rows, [{ sourceColumn: "Origem", modelColumn: "Destino" }]);
expect(output).toHaveLength(50_000);
expect(output[49_999][0]).toBe("49999");
const csv = buildCsv(["Destino"], output.slice(0, 2));
expect(csv.toString("utf8")).toContain("0");
});
});
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { MemoryStorage } from "@/infra/storage/memory";
import { detectSpreadsheetFormat } from "@/domain/spreadsheet-adapter/file-signature";
import { assertZipSafe } from "@/domain/spreadsheet-adapter/file-signature";
describe("hardening de arquivo", () => {
it("rejeita path traversal, extensão enganosa e zip anômalo", async () => {
const storage = new MemoryStorage();
await expect(storage.put("../segredo", Buffer.from("x"))).rejects.toMatchObject({ code: "PATH_TRAVERSAL" });
const zip = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
expect(() => detectSpreadsheetFormat("dados.csv", zip)).toThrow(/CSV/);
expect(() => detectSpreadsheetFormat("legado.xls", Buffer.from("a,b"))).toThrow(/XLS/);
const bomb = Buffer.alloc(64);
bomb.writeUInt32LE(0x04034b50, 0);
bomb.writeUInt16LE(0, 8);
bomb.writeUInt32LE(10, 18);
bomb.writeUInt32LE(400_000_000, 22);
bomb.writeUInt16LE(1, 26);
bomb.writeUInt16LE(0, 28);
bomb.write("a", 30);
expect(() => assertZipSafe(bomb)).toThrow(/limite seguro|compressão/i);
});
});
+3
View File
@@ -0,0 +1,3 @@
process.env.NODE_ENV = "test";
process.env.ADAPTER_JOB_MODE = "inline";
process.env.SPREADSHEET_TEMP_TTL_SECONDS = "3600";
+78
View File
@@ -0,0 +1,78 @@
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([]);
});
});