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/); });