574 lines
23 KiB
JavaScript
574 lines
23 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { openDb, addOptOut, createLeadInserter, setSetting } from '../src/db.mjs';
|
|
import { createRouter } from '../src/server/router.mjs';
|
|
import { registerOutreachRoutes } from '../src/server/routes/outreach.mjs';
|
|
|
|
function setup({ state = 'READY' } = {}) {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
const sent = [];
|
|
const whatsapp = {
|
|
getStatus: () => ({ state, connected: state === 'READY' }),
|
|
sendText: async (args) => {
|
|
const context = await args.beforeSend?.();
|
|
sent.push(args);
|
|
const result = { messageId: 'local-123' };
|
|
await args.afterSend?.(result, null, context);
|
|
return result;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
return { db, router, sent };
|
|
}
|
|
|
|
async function invoke(router, method, pathname, { body, user = { role: 'comercial' } } = {}) {
|
|
const calls = [];
|
|
const matched = router.match(method, pathname);
|
|
assert.ok(matched, `rota ausente: ${method} ${pathname}`);
|
|
await matched.handler(null, null, {
|
|
params: matched.params,
|
|
body,
|
|
user,
|
|
json: (status, data) => calls.push({ status, data })
|
|
});
|
|
return calls[0];
|
|
}
|
|
|
|
test('status de outreach informa conexao WhatsApp local', async () => {
|
|
const { db, router } = setup();
|
|
const response = await invoke(router, 'GET', '/api/outreach/status');
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.data.whatsapp.state, 'READY');
|
|
assert.equal(response.data.configurado, true);
|
|
db.close();
|
|
});
|
|
|
|
test('resultado desconhecido conta no limite diario', async () => {
|
|
const { db, router, sent } = setup();
|
|
setSetting(db, 'outreach.mensagensPorDia', '1');
|
|
db.prepare("INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem) VALUES ('ERP', '5511999990001', 'oi', 'desconhecido', 'manual')").run();
|
|
const status = await invoke(router, 'GET', '/api/outreach/status');
|
|
assert.equal(status.data.enviadosHoje, 1);
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990002', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 429);
|
|
assert.equal(sent.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('envio manual exige READY, numero valido e mensagem', async () => {
|
|
const disconnected = setup({ state: 'DISCONNECTED' });
|
|
const response = await invoke(disconnected.router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 409);
|
|
disconnected.db.close();
|
|
|
|
const ready = setup();
|
|
assert.equal((await invoke(ready.router, 'POST', '/api/outreach/enviar-manual', { body: { numero: '123', mensagem: 'Oi' } })).status, 400);
|
|
assert.equal((await invoke(ready.router, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990001', mensagem: '' } })).status, 400);
|
|
ready.db.close();
|
|
});
|
|
|
|
test('envio manual renderiza lead, envia local e registra messageId generico', async () => {
|
|
const { db, router, sent } = setup();
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: {
|
|
numero: '+55 (11) 99999-0001',
|
|
mensagem: 'Ola {empresa}, oferta de {produto} em {cidade}',
|
|
lead: { empresa: 'Padaria Sol', produto_principal: 'ERP', cidade: 'Ilheus' }
|
|
}
|
|
});
|
|
assert.deepEqual(response, { status: 202, data: { ok: true, status: 'aceito', messageId: 'local-123' } });
|
|
assert.equal(sent[0].number, '5511999990001');
|
|
assert.equal(sent[0].message, 'Ola Padaria Sol, oferta de ERP em Ilheus');
|
|
const row = db.prepare('SELECT * FROM outreach_sends').get();
|
|
assert.equal(row.provider_message_id, 'local-123');
|
|
assert.equal(row.status, 'aceito');
|
|
assert.equal(row.canal, 'local');
|
|
assert.equal(row.origem, 'manual');
|
|
db.close();
|
|
});
|
|
|
|
test('envio manual bloqueia opt-out e limite diario', async () => {
|
|
const opted = setup();
|
|
addOptOut(opted.db, '5511999990001', 'nao contatar');
|
|
const blocked = await invoke(opted.router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(blocked.status, 409);
|
|
assert.equal(opted.sent.length, 0);
|
|
opted.db.close();
|
|
|
|
const limited = setup();
|
|
setSetting(limited.db, 'outreach.mensagensPorDia', '0');
|
|
const response = await invoke(limited.router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 429);
|
|
limited.db.close();
|
|
});
|
|
|
|
test('rotas opt-out listam e restringem mutacoes a admin', async () => {
|
|
const { db, router } = setup();
|
|
const denied = await invoke(router, 'POST', '/api/outreach/opt-outs', {
|
|
body: { numero: '5511999990001' }, user: { role: 'comercial' }
|
|
});
|
|
assert.equal(denied.status, 403);
|
|
|
|
const created = await invoke(router, 'POST', '/api/outreach/opt-outs', {
|
|
body: { numero: '5511999990001', motivo: 'solicitado' }, user: { role: 'admin' }
|
|
});
|
|
assert.equal(created.status, 201);
|
|
const listed = await invoke(router, 'GET', '/api/outreach/opt-outs');
|
|
assert.equal(listed.data.optOuts.length, 1);
|
|
const removed = await invoke(router, 'DELETE', '/api/outreach/opt-outs/5511999990001', { user: { role: 'admin' } });
|
|
assert.equal(removed.status, 200);
|
|
db.close();
|
|
});
|
|
|
|
test('envio manual bloqueia numero compartilhado por leads distintos ate liberacao', async () => {
|
|
const { db, router } = setup();
|
|
const insert = createLeadInserter(db);
|
|
insert({
|
|
dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5511999990009', cidade: 'Salvador',
|
|
produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
|
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
|
});
|
|
insert({
|
|
dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '5511999990009', cidade: 'Salvador',
|
|
produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
|
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
|
});
|
|
|
|
const blocked = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990009', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(blocked.status, 409);
|
|
|
|
await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', {
|
|
body: { motivo: 'confirmado contato direto' }, user: { role: 'admin' }
|
|
});
|
|
const allowed = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990009', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(allowed.status, 202);
|
|
db.close();
|
|
});
|
|
|
|
test('rotas de numeros compartilhados listam e restringem liberacao a admin', async () => {
|
|
const { db, router } = setup();
|
|
const insert = createLeadInserter(db);
|
|
insert({
|
|
dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5511999990009', cidade: 'Salvador',
|
|
produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
|
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
|
});
|
|
insert({
|
|
dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '5511999990009', cidade: 'Salvador',
|
|
produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
|
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
|
});
|
|
|
|
const listed = await invoke(router, 'GET', '/api/outreach/shared-numbers');
|
|
assert.equal(listed.data.groups.length, 1);
|
|
assert.equal(listed.data.groups[0].leads.length, 2);
|
|
|
|
const denied = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', {
|
|
body: { motivo: 'teste' }, user: { role: 'comercial' }
|
|
});
|
|
assert.equal(denied.status, 403);
|
|
|
|
const missingMotivo = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', {
|
|
body: {}, user: { role: 'admin' }
|
|
});
|
|
assert.equal(missingMotivo.status, 400);
|
|
|
|
const created = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', {
|
|
body: { motivo: 'confirmado' }, user: { role: 'admin' }
|
|
});
|
|
assert.equal(created.status, 201);
|
|
|
|
const removeDenied = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'comercial' } });
|
|
assert.equal(removeDenied.status, 403);
|
|
|
|
const removed = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'admin' } });
|
|
assert.equal(removed.status, 200);
|
|
|
|
const removedAgain = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'admin' } });
|
|
assert.equal(removedAgain.status, 404);
|
|
db.close();
|
|
});
|
|
|
|
test('envio com leadId honra destino visivel e usa contexto completo do banco', async () => {
|
|
const { db, router, sent } = setup();
|
|
createLeadInserter(db)({
|
|
dedup_key: 'real-1', empresa: 'Empresa Real', whatsapp: '5511999990002', 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/outreach/enviar-manual', {
|
|
body: { leadId: 1, numero: '5511999990001', mensagem: 'Oi {empresa} de {cidade}' }
|
|
});
|
|
assert.equal(response.status, 202);
|
|
assert.equal(sent[0].number, '5511999990001');
|
|
assert.equal(sent[0].message, 'Oi Empresa Real de Santos');
|
|
assert.equal(db.prepare('SELECT lead_id FROM outreach_sends').get().lead_id, 1);
|
|
assert.equal(db.prepare('SELECT numero FROM outreach_sends').get().numero, '5511999990001');
|
|
assert.equal(db.prepare('SELECT whatsapp FROM leads WHERE id = 1').get().whatsapp, '5511999990002');
|
|
assert.equal((await invoke(router, 'POST', '/api/outreach/enviar-manual', { body: { leadId: 999, numero: '5511999990001', mensagem: 'Oi' } })).status, 404);
|
|
db.close();
|
|
});
|
|
|
|
test('envio manual rejeita leadId coercivel antes de cache, reserva ou envio', async () => {
|
|
const { db, router, sent } = setup();
|
|
createLeadInserter(db)({
|
|
dedup_key: 'canonical-1', empresa: 'Empresa Real', whatsapp: '5511999990002', 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/outreach/enviar-manual', {
|
|
body: { leadId, numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.deepEqual(response, { status: 404, data: { erro: 'lead nao encontrado' } });
|
|
}
|
|
|
|
assert.equal(sent.length, 0);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM whatsapp_number_verifications').get().total, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('envio manual persiste numero confirmado e devolve metadados resolvidos', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async (args) => {
|
|
const sendId = await args.beforeSend();
|
|
const result = {
|
|
messageId: 'id-1',
|
|
numero: '5511888880000',
|
|
status: 'erro_temporario',
|
|
resolvedNumber: '557599091780',
|
|
variant: 'sem_nono_digito'
|
|
};
|
|
await args.afterSend(result, null, sendId);
|
|
return result;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5575999091780', mensagem: 'Oi' }
|
|
});
|
|
assert.deepEqual(response, {
|
|
status: 202,
|
|
data: {
|
|
ok: true,
|
|
status: 'aceito',
|
|
messageId: 'id-1',
|
|
resolvedNumber: '557599091780',
|
|
variant: 'sem_nono_digito'
|
|
}
|
|
});
|
|
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('numero nao cadastrado no envio retorna 422 e persiste verificacao', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async () => {
|
|
throw Object.assign(new Error('numero nao esta cadastrado no WhatsApp'), {
|
|
numero: '5511888880000',
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null
|
|
});
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 422);
|
|
assert.deepEqual(
|
|
{ ...db.prepare('SELECT numero, status FROM whatsapp_number_verifications').get() },
|
|
{ numero: '5511999990001', status: 'nao_cadastrado' }
|
|
);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('timeout de consulta no envio persiste erro temporario antes de retornar 504', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: 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
|
|
});
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 504);
|
|
assert.deepEqual(
|
|
{ ...db.prepare('SELECT status, erro FROM whatsapp_number_verifications').get() },
|
|
{ status: 'erro_temporario', erro: 'consulta excedeu o prazo' }
|
|
);
|
|
db.close();
|
|
});
|
|
|
|
test('falha de cache no numero nao cadastrado preserva 422 e aviso local', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
db.exec(`
|
|
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
|
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
|
`);
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async () => {
|
|
throw Object.assign(new Error('numero nao cadastrado'), {
|
|
status: 'nao_cadastrado', registered: false, resolvedNumber: null, variant: null
|
|
});
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
|
|
assert.equal(response.status, 422);
|
|
assert.equal(response.data.ok, false);
|
|
assert.match(response.data.erro, /nao cadastrado/);
|
|
assert.match(response.data.aviso, /verificacao local/i);
|
|
assert.doesNotMatch(response.data.aviso, /tente.*enviar/i);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('falha de cache no timeout de lookup preserva 504 e aviso local', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
db.exec(`
|
|
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
|
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
|
`);
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async () => {
|
|
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
|
statusCode: 504, status: 'erro_temporario', registered: false, resolvedNumber: null, variant: null
|
|
});
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
|
|
assert.equal(response.status, 504);
|
|
assert.equal(response.data.ok, false);
|
|
assert.match(response.data.erro, /prazo/);
|
|
assert.match(response.data.aviso, /verificacao local/i);
|
|
assert.doesNotMatch(response.data.aviso, /tente.*enviar/i);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('timeout apos reserva finaliza uma vez como desconhecido e orienta a nao reenviar', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
db.exec(`
|
|
CREATE TABLE outreach_send_update_audit (send_id INTEGER, status TEXT);
|
|
CREATE TRIGGER audit_outreach_send_update AFTER UPDATE ON outreach_sends
|
|
BEGIN
|
|
INSERT INTO outreach_send_update_audit (send_id, status) VALUES (NEW.id, NEW.status);
|
|
END;
|
|
`);
|
|
let callbackCalls = 0;
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async (args) => {
|
|
const sendId = await args.beforeSend();
|
|
const error = Object.assign(new Error('envio excedeu o prazo'), {
|
|
statusCode: 504,
|
|
code: 'WHATSAPP_SEND_TIMEOUT',
|
|
deliveryUnknown: true,
|
|
numero: '5511888880000',
|
|
status: 'erro_temporario',
|
|
resolvedNumber: '557599091780',
|
|
variant: 'sem_nono_digito'
|
|
});
|
|
callbackCalls += 1;
|
|
await args.afterSend(null, error, sendId);
|
|
throw error;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5575999091780', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 202);
|
|
assert.equal(response.data.status, 'resultado_desconhecido');
|
|
assert.match(response.data.aviso, /nao reenvie/i);
|
|
assert.equal(callbackCalls, 1);
|
|
const row = db.prepare('SELECT status, erro FROM outreach_sends').get();
|
|
assert.equal(row.status, 'desconhecido');
|
|
assert.match(row.erro, /prazo/);
|
|
assert.deepEqual(
|
|
{ ...db.prepare('SELECT numero, status FROM whatsapp_number_verifications').get() },
|
|
{ numero: '5575999091780', status: 'confirmado' }
|
|
);
|
|
assert.deepEqual(
|
|
db.prepare('SELECT status FROM outreach_send_update_audit').all().map((audit) => audit.status),
|
|
['desconhecido']
|
|
);
|
|
db.close();
|
|
});
|
|
|
|
test('falha secundaria no cache preserva resposta aceita e orienta a nao reenviar', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
db.exec(`
|
|
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
|
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
|
`);
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async (args) => {
|
|
const sendId = await args.beforeSend();
|
|
const result = { messageId: 'aceita-1', resolvedNumber: '5511999990001', variant: 'exata' };
|
|
await args.afterSend(result, null, sendId);
|
|
return result;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 202);
|
|
assert.equal(response.data.status, 'aceito');
|
|
assert.equal(response.data.messageId, 'aceita-1');
|
|
assert.match(response.data.aviso, /verificacao local/i);
|
|
assert.match(response.data.aviso, /nao reenvie/i);
|
|
assert.equal(db.prepare('SELECT status FROM outreach_sends').get().status, 'aceito');
|
|
db.close();
|
|
});
|
|
|
|
test('falha secundaria no cache preserva resultado desconhecido e aviso de nao reenvio', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
db.exec(`
|
|
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
|
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
|
`);
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async (args) => {
|
|
const sendId = await args.beforeSend();
|
|
const error = Object.assign(new Error('envio excedeu o prazo'), {
|
|
deliveryUnknown: true,
|
|
numero: '5511888880000',
|
|
status: 'erro_temporario',
|
|
resolvedNumber: '5511999990001',
|
|
variant: 'exata'
|
|
});
|
|
await args.afterSend(null, error, sendId);
|
|
throw error;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'Oi' }
|
|
});
|
|
assert.equal(response.status, 202);
|
|
assert.equal(response.data.status, 'resultado_desconhecido');
|
|
assert.match(response.data.aviso, /verificacao local/i);
|
|
assert.match(response.data.aviso, /nao reenvie/i);
|
|
assert.equal(db.prepare('SELECT status FROM outreach_sends').get().status, 'desconhecido');
|
|
db.close();
|
|
});
|
|
|
|
test('duas requisicoes manuais concorrentes respeitam limite dentro da fila', async () => {
|
|
const { db, router } = setup();
|
|
setSetting(db, 'outreach.mensagensPorDia', '1');
|
|
const calls = [];
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async (args) => { const context = await args.beforeSend(); const result = { messageId: 'id' }; await args.afterSend(result, null, context); return result; }
|
|
};
|
|
const concurrentRouter = createRouter();
|
|
registerOutreachRoutes(concurrentRouter, { db, whatsapp });
|
|
const responses = await Promise.all([
|
|
invoke(concurrentRouter, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990001', mensagem: 'um' } }),
|
|
invoke(concurrentRouter, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990002', mensagem: 'dois' } })
|
|
]);
|
|
calls.push(...responses.map((r) => r.status));
|
|
assert.deepEqual(calls.sort(), [202, 429]);
|
|
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_sends WHERE status = 'aceito'").get().total, 1);
|
|
db.close();
|
|
});
|
|
|
|
test('aceite com falha de persistencia orienta a nao reenviar', async () => {
|
|
const db = openDb(':memory:');
|
|
const whatsapp = {
|
|
getStatus: () => ({ state: 'READY' }),
|
|
sendText: async () => {
|
|
const error = new Error('banco indisponivel');
|
|
error.deliverySucceeded = true;
|
|
error.messageId = 'possibly-sent-1';
|
|
throw error;
|
|
}
|
|
};
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db, whatsapp });
|
|
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
|
body: { numero: '5511999990001', mensagem: 'oi' }
|
|
});
|
|
assert.equal(response.status, 202);
|
|
assert.equal(response.data.status, 'persistencia_incerta');
|
|
assert.match(response.data.aviso, /nao reenvie/i);
|
|
db.close();
|
|
});
|