Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
122 lines
5.1 KiB
JavaScript
122 lines
5.1 KiB
JavaScript
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();
|
|
});
|