chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createLeadInserter, openDb } from '../src/db.mjs';
|
||||
import { createRouter } from '../src/server/router.mjs';
|
||||
import { registerWhatsAppRoutes } from '../src/server/routes/whatsapp.mjs';
|
||||
|
||||
function setup({ qr = 'raw-qr', checkNumber } = {}) {
|
||||
const db = openDb(':memory:');
|
||||
const calls = [];
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'QR_REQUIRED', connected: false, qrExpiresAt: '2026-07-28T12:00:00.000Z' }),
|
||||
getQr: () => qr,
|
||||
connect: async () => { calls.push('connect'); },
|
||||
refreshQr: async () => { calls.push('refreshQr'); },
|
||||
checkNumber: checkNumber || (async (numero) => ({
|
||||
numero,
|
||||
status: 'confirmado',
|
||||
registered: true,
|
||||
resolvedNumber: '557599091780',
|
||||
variant: 'sem_nono_digito',
|
||||
candidatesChecked: 2
|
||||
})),
|
||||
disconnect: async (options) => { calls.push(options); }
|
||||
};
|
||||
const router = createRouter();
|
||||
registerWhatsAppRoutes(router, {
|
||||
db,
|
||||
whatsapp,
|
||||
toDataURL: async (value) => `data:image/png;base64,${value === 'raw-qr' ? 'encoded' : 'bad'}`
|
||||
});
|
||||
return { db, router, calls };
|
||||
}
|
||||
|
||||
async function invoke(router, method, pathname, context = {}) {
|
||||
const responses = [];
|
||||
const matched = router.match(method, pathname);
|
||||
assert.ok(matched, `rota ausente: ${method} ${pathname}`);
|
||||
await matched.handler(null, null, {
|
||||
params: matched.params,
|
||||
body: null,
|
||||
json: (status, data) => responses.push({ status, data }),
|
||||
...context
|
||||
});
|
||||
return responses[0];
|
||||
}
|
||||
|
||||
test('status pode ser consultado por qualquer usuario autenticado sem segredos', async () => {
|
||||
const { router } = setup();
|
||||
const response = await invoke(router, 'GET', '/api/whatsapp/status', { user: { role: 'comercial' } });
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.state, 'QR_REQUIRED');
|
||||
assert.equal(JSON.stringify(response.data).includes('raw-qr'), false);
|
||||
assert.equal(JSON.stringify(response.data).includes('auth'), false);
|
||||
});
|
||||
|
||||
test('acoes WhatsApp rejeitam usuario nao-admin', async () => {
|
||||
const { router, calls } = setup();
|
||||
for (const [method, pathname] of [
|
||||
['POST', '/api/whatsapp/connect'],
|
||||
['POST', '/api/whatsapp/refresh-qr'],
|
||||
['POST', '/api/whatsapp/check-number'],
|
||||
['GET', '/api/whatsapp/qr'],
|
||||
['POST', '/api/whatsapp/disconnect'],
|
||||
['POST', '/api/whatsapp/logout']
|
||||
]) {
|
||||
const response = await invoke(router, method, pathname, { user: { role: 'comercial' } });
|
||||
assert.equal(response.status, 403);
|
||||
}
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('admin diagnostica numero sem enviar mensagem', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => ({
|
||||
numero: '5511888880000',
|
||||
status: 'erro_temporario',
|
||||
registered: true,
|
||||
resolvedNumber: '557599091780',
|
||||
variant: 'sem_nono_digito',
|
||||
candidatesChecked: 2
|
||||
})
|
||||
});
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '5575999091780' }
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.numero, '5575999091780');
|
||||
assert.equal(response.data.status, 'confirmado');
|
||||
assert.equal(response.data.registered, true);
|
||||
assert.equal(response.data.resolvedNumber, '557599091780');
|
||||
assert.equal(response.data.variant, 'sem_nono_digito');
|
||||
assert.equal(response.data.candidatesChecked, 2);
|
||||
assert.match(response.data.checked_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status, numero_resolvido, variante FROM whatsapp_number_verifications').get() },
|
||||
{
|
||||
numero: '5575999091780',
|
||||
status: 'confirmado',
|
||||
numero_resolvido: '557599091780',
|
||||
variante: 'sem_nono_digito'
|
||||
}
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('diagnostico definitivo nao cadastrado retorna 422 e persiste o resultado', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async (numero) => ({
|
||||
numero,
|
||||
status: 'nao_cadastrado',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null,
|
||||
candidatesChecked: 2
|
||||
})
|
||||
});
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '5511999990001' }
|
||||
});
|
||||
assert.equal(response.status, 422);
|
||||
assert.equal(response.data.status, 'nao_cadastrado');
|
||||
assert.match(response.data.checked_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
assert.equal(db.prepare('SELECT status FROM whatsapp_number_verifications').get().status, 'nao_cadastrado');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('timeout de diagnostico retorna 504 e persiste erro temporario solicitado', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => {
|
||||
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
||||
statusCode: 504,
|
||||
code: 'WHATSAPP_LOOKUP_TIMEOUT',
|
||||
status: 'erro_temporario',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null,
|
||||
candidatesChecked: 2
|
||||
});
|
||||
}
|
||||
});
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '5511999990001' }
|
||||
});
|
||||
assert.equal(response.status, 504);
|
||||
assert.match(response.data.erro, /prazo/);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status, erro FROM whatsapp_number_verifications').get() },
|
||||
{ numero: '5511999990001', status: 'erro_temporario', erro: 'consulta excedeu o prazo' }
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('erro definitivo sem statusCode retorna 422 e ignora numero conflitante do provedor', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => {
|
||||
throw Object.assign(new Error('numero nao cadastrado'), {
|
||||
numero: '5511888880000',
|
||||
status: 'nao_cadastrado',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null
|
||||
});
|
||||
}
|
||||
});
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '+55 (11) 99999-0001' }
|
||||
});
|
||||
assert.equal(response.status, 422);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status FROM whatsapp_number_verifications').get() },
|
||||
{ numero: '5511999990001', status: 'nao_cadastrado' }
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('check-number valida leadId opcional antes de consultar provedor ou cache', async () => {
|
||||
let providerCalls = 0;
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => {
|
||||
providerCalls += 1;
|
||||
return { registered: true };
|
||||
}
|
||||
});
|
||||
createLeadInserter(db)({
|
||||
dedup_key: 'lead-check-1', empresa: 'Empresa', whatsapp: '5511999990001', cidade: 'Santos',
|
||||
produto_principal: 'ERP', produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
||||
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
||||
});
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache nao deveria ser tocado'); END;
|
||||
`);
|
||||
|
||||
for (const leadId of [true, '1', '01', '1e0', 1.5, 0, -1, NaN, 999]) {
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { leadId, numero: '5511999990001' }
|
||||
});
|
||||
assert.deepEqual(response, { status: 404, data: { erro: 'lead nao encontrado' } });
|
||||
}
|
||||
assert.equal(providerCalls, 0);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM whatsapp_number_verifications').get().total, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('check-number usa numero do corpo mesmo com contexto de lead valido', async () => {
|
||||
let checkedNumber;
|
||||
const { db, router } = setup({
|
||||
checkNumber: async (numero) => {
|
||||
checkedNumber = numero;
|
||||
return { registered: false, resolvedNumber: null, variant: null, candidatesChecked: 2 };
|
||||
}
|
||||
});
|
||||
createLeadInserter(db)({
|
||||
dedup_key: 'lead-check-2', empresa: 'Empresa', whatsapp: '5511888880000', cidade: 'Santos',
|
||||
produto_principal: 'ERP', produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
||||
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
||||
});
|
||||
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { leadId: 1, numero: '5511999990001' }
|
||||
});
|
||||
|
||||
assert.equal(response.status, 422);
|
||||
assert.equal(checkedNumber, '5511999990001');
|
||||
assert.equal(response.data.numero, '5511999990001');
|
||||
assert.equal(db.prepare('SELECT numero FROM whatsapp_number_verifications').get().numero, '5511999990001');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha de cache no nao cadastrado preserva 422, JSON e timestamp nulo', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => ({ registered: false, resolvedNumber: null, variant: null, candidatesChecked: 2 })
|
||||
});
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '5511999990001' }
|
||||
});
|
||||
|
||||
assert.equal(response.status, 422);
|
||||
assert.equal(response.data.status, 'nao_cadastrado');
|
||||
assert.equal(response.data.checked_at, null);
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.doesNotMatch(response.data.aviso, /tente.*enviar|reenvie/i);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha de cache no timeout de lookup preserva 504, JSON e timestamp nulo', async () => {
|
||||
const { db, router } = setup({
|
||||
checkNumber: async () => {
|
||||
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
||||
statusCode: 504,
|
||||
status: 'erro_temporario',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null,
|
||||
candidatesChecked: 2
|
||||
});
|
||||
}
|
||||
});
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
|
||||
const response = await invoke(router, 'POST', '/api/whatsapp/check-number', {
|
||||
user: { role: 'admin' }, body: { numero: '5511999990001' }
|
||||
});
|
||||
|
||||
assert.equal(response.status, 504);
|
||||
assert.equal(response.data.status, 'erro_temporario');
|
||||
assert.equal(response.data.checked_at, null);
|
||||
assert.match(response.data.erro, /prazo/);
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.doesNotMatch(response.data.aviso, /tente.*enviar|reenvie/i);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('admin conecta, obtem QR vigente como data URL e encerra ou remove sessao', async () => {
|
||||
const { router, calls } = setup();
|
||||
const user = { role: 'admin' };
|
||||
assert.equal((await invoke(router, 'POST', '/api/whatsapp/connect', { user })).status, 202);
|
||||
assert.equal((await invoke(router, 'POST', '/api/whatsapp/refresh-qr', { user })).status, 202);
|
||||
const qr = await invoke(router, 'GET', '/api/whatsapp/qr', { user });
|
||||
assert.deepEqual(qr, {
|
||||
status: 200,
|
||||
data: { qr: 'data:image/png;base64,encoded', expiresAt: '2026-07-28T12:00:00.000Z' }
|
||||
});
|
||||
assert.equal((await invoke(router, 'POST', '/api/whatsapp/disconnect', { user })).status, 200);
|
||||
assert.equal((await invoke(router, 'POST', '/api/whatsapp/logout', { user })).status, 200);
|
||||
assert.deepEqual(calls, ['connect', 'refreshQr', { logout: false }, { logout: true }]);
|
||||
});
|
||||
|
||||
test('QR expirado retorna conflito sem chamar gerador', async () => {
|
||||
const { router } = setup({ qr: null });
|
||||
const response = await invoke(router, 'GET', '/api/whatsapp/qr', { user: { role: 'admin' } });
|
||||
assert.equal(response.status, 409);
|
||||
assert.match(response.data.erro, /QR/);
|
||||
});
|
||||
Reference in New Issue
Block a user