Baseline commit of the existing Maquina Comercial Pessoal project

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.
This commit is contained in:
Gustavo
2026-07-31 10:54:07 -03:00
parent 6cd336a41e
commit d330bb5ff0
132 changed files with 29969 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
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 { splitCsvLine, csvEscape, excelTextCell, neutralizeSpreadsheetFormula, streamCsvRows, createCsvWriter, stripBom, BOM } from '../src/util/csv.mjs';
test('splitCsvLine separa campos com separador dentro de aspas', () => {
assert.deepEqual(splitCsvLine('a;"b;c";d'), ['a', 'b;c', 'd']);
});
test('splitCsvLine trata aspas duplas escapadas', () => {
assert.deepEqual(splitCsvLine('"x""y";z'), ['x"y', 'z']);
});
test('splitCsvLine aceita separador virgula', () => {
assert.deepEqual(splitCsvLine('a,b,"c,d"', ','), ['a', 'b', 'c,d']);
});
test('csvEscape protege valores com separador, aspas e quebra de linha', () => {
assert.equal(csvEscape('sem risco'), 'sem risco');
assert.equal(csvEscape('a;b'), '"a;b"');
assert.equal(csvEscape('diz "oi"'), '"diz ""oi"""');
assert.equal(csvEscape(null), '');
});
test('excelTextCell gera formula de texto para numeros longos', () => {
assert.equal(excelTextCell('557193335451'), '"=""557193335451"""');
});
test('neutraliza formula injection sem alterar numeros comuns', () => {
for (const value of ['=HYPERLINK("x")', '+cmd', '-1+2', '@SUM(A1)', '\tformula', '\rformula']) {
assert.equal(neutralizeSpreadsheetFormula(value), `'${value}`);
}
assert.equal(neutralizeSpreadsheetFormula('5511999990001'), '5511999990001');
});
test('stripBom remove BOM apenas quando presente', () => {
assert.equal(stripBom('abc'), 'abc');
assert.equal(stripBom('abc'), 'abc');
});
test('streamCsvRows le arquivo com BOM, auto-detecta separador e normaliza headers', async () => {
const file = path.join(os.tmpdir(), `gti-test-csv-${Date.now()}.csv`);
await fs.writeFile(file, `${BOM}Nome Empresa;Telefone 1\n"ACME; Ltda";7599999\nBeta;`, 'utf8');
try {
const rows = [];
for await (const row of streamCsvRows(file, {
normalizeHeader: (h) => h.toLowerCase().replace(/\s+/g, '_')
})) {
rows.push(row);
}
assert.equal(rows.length, 2);
assert.equal(rows[0].nome_empresa, 'ACME; Ltda');
assert.equal(rows[0].telefone_1, '7599999');
assert.equal(rows[1].telefone_1, '');
} finally {
await fs.rm(file, { force: true });
}
});
test('streamCsvRows suporta campo com quebra de linha dentro de aspas', async () => {
const file = path.join(os.tmpdir(), `gti-test-csvml-${Date.now()}.csv`);
await fs.writeFile(file, 'a;b\n1;"linha um\nlinha dois"\n2;simples', 'utf8');
try {
const rows = [];
for await (const row of streamCsvRows(file)) rows.push(row);
assert.equal(rows.length, 2);
assert.equal(rows[0].b, 'linha um\nlinha dois');
assert.equal(rows[1].b, 'simples');
} finally {
await fs.rm(file, { force: true });
}
});
test('createCsvWriter grava BOM, header e linhas', async () => {
const file = path.join(os.tmpdir(), `gti-test-csvw-${Date.now()}.csv`);
const writer = createCsvWriter(file, ['a', 'b']);
writer.writeRow(['1', csvEscape('x;y')]);
writer.writeRow(['2', excelTextCell('123')]);
await writer.end();
try {
const content = await fs.readFile(file, 'utf8');
assert.ok(content.startsWith(BOM));
const lines = stripBom(content).trim().split('\n');
assert.equal(lines[0], 'a;b');
assert.equal(lines[1], '1;"x;y"');
assert.equal(lines[2], '2;"=""123"""');
} finally {
await fs.rm(file, { force: true });
}
});