Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
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/);
|
|
});
|