import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { openDb, createLeadInserter } from '../src/db.mjs'; import { exportExcel } from '../src/export/excel.mjs'; import { registerExportRoutes } from '../src/server/routes/exports.mjs'; import { createRouter } from '../src/server/router.mjs'; import { stripBom } from '../src/util/csv.mjs'; function seedLead(db) { createLeadInserter(db)({ lead_uid: 'P-1', dedup_key: 'p-1', empresa: 'PREMIUM EXPRESS', legal_name: '', whatsapp: '557193335451', ddd: '71', email: '', cidade: 'Salvador', uf: 'BA', segmento: 'restaurantes', segmento_label: 'Restaurantes', interesse: '', cnpj: '57557263000113', cnae: '', endereco: '', website: '', pains_json: '[]', origem: 'cnpj-base', telefone_valido: 1, cnpj_valido: 1, empresa_valida: 1, score: 100, classificacao: 'bom', motivo_classificacao: '', capture_run_id: null, captured_at: null, exported_at: null }); } test('exportExcel usa nome interno e neutraliza formulas no CSV/XLS', async () => { const db = openDb(':memory:'); seedLead(db); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pessoal-export-')); const escapedPath = path.resolve(dir, '../../escape.csv'); try { await fs.rm(escapedPath, { force: true }); const ids = []; db.prepare("UPDATE leads SET empresa = '=HYPERLINK(\"https://evil\")' WHERE id = 1").run(); const result = await exportExcel({ db, filtro: {}, dir, baseName: '../../escape', collectIds: ids }); assert.deepEqual(result.files, ['leads-excel.csv', 'leads-excel.xls']); const csv = stripBom(await fs.readFile(path.join(dir, 'leads-excel.csv'), 'utf8')); const xls = await fs.readFile(path.join(dir, 'leads-excel.xls'), 'utf8'); assert.match(csv, /'=HYPERLINK/); assert.match(xls, /'=HYPERLINK/); await assert.rejects(fs.stat(escapedPath)); assert.doesNotMatch(csv, /(^|;)=HYPERLINK/m); assert.deepEqual(ids, [1]); } finally { await fs.rm(escapedPath, { force: true }); await fs.rm(dir, { recursive: true, force: true }); db.close(); } }); test('rota de exportacao rejeita tipos corporativos removidos', async () => { const db = openDb(':memory:'); seedLead(db); const router = createRouter(); registerExportRoutes(router, { db, jobs: { startJob: () => 'job-1' } }); const calls = []; await router.match('POST', '/api/exports').handler(null, null, { body: { tipo: 'zpro-oportunidades' }, json: (status, data) => calls.push({ status, data }) }); assert.equal(calls[0].status, 400); assert.match(calls[0].data.erro, /excel/); db.close(); }); test('rota de exportacao persiste somente marcarExportado nos params', async () => { const db = openDb(':memory:'); seedLead(db); const router = createRouter(); let started; registerExportRoutes(router, { db, jobs: { startJob: (options) => { started = options; return 'job-1'; } } }); await router.match('POST', '/api/exports').handler(null, null, { body: { tipo: 'excel', params: { baseName: '../../evil', marcarExportado: false, outro: true } }, json: () => {} }); assert.deepEqual(JSON.parse(db.prepare('SELECT params_json FROM exports').get().params_json), { marcarExportado: false }); const row = db.prepare('SELECT job_id, owner_token, heartbeat_at FROM exports').get(); assert.equal(row.job_id, 'job-1'); assert.ok(row.owner_token); assert.ok(row.heartbeat_at); assert.equal(started.ownerToken, row.owner_token); db.close(); }); test('rota de exportacao rejeita nova exportacao enquanto outra esta pendente', async () => { const db = openDb(':memory:'); seedLead(db); db.prepare("INSERT INTO exports (tipo, status) VALUES ('excel', 'pending')").run(); const router = createRouter(); registerExportRoutes(router, { db, jobs: { startJob: () => 'job-1' } }); let response; await router.match('POST', '/api/exports').handler(null, null, { body: { tipo: 'excel' }, json: (status, data) => { response = { status, data }; } }); assert.equal(response.status, 409); assert.equal(db.prepare('SELECT COUNT(*) total FROM exports').get().total, 1); db.close(); }); test('rota de exportacao converte corrida de constraint em 409 sem row parcial', async () => { const db = openDb(':memory:'); seedLead(db); const raceDb = { prepare(sql) { if (/INSERT INTO exports/.test(sql)) { const error = new Error('UNIQUE constraint failed: active export'); error.errcode = 2067; throw error; } return db.prepare(sql); } }; const router = createRouter(); let started = false; registerExportRoutes(router, { db: raceDb, jobs: { startJob: () => { started = true; } } }); let response; await router.match('POST', '/api/exports').handler(null, null, { body: { tipo: 'excel' }, json: (status, data) => { response = { status, data }; } }); assert.equal(response.status, 409); assert.match(response.data.erro, /simultaneamente/i); assert.equal(db.prepare('SELECT COUNT(*) total FROM exports').get().total, 0); assert.equal(started, false); db.close(); });