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: f5af038e19
git-subtree-split: a9ef4d776d
This commit is contained in:
Gustavo
2026-09-28 15:50:17 -03:00
co-authored by Claude Sonnet 5
150 changed files with 33449 additions and 0 deletions
@@ -0,0 +1,476 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { openDb, setSetting } from '../src/db.mjs';
import { listCampaigns, saveAppointment, seedCampaigns, setLeadCommercialStage } from '../src/outreach/campaign-store.mjs';
import { createRouter } from '../src/server/router.mjs';
import { registerCampaignRoutes } from '../src/server/routes/campaigns.mjs';
function setup({
state = 'READY',
now = new Date(2026, 6, 29, 12),
random = () => 0
} = {}) {
const db = openDb(':memory:');
seedCampaigns(db);
setSetting(db, 'outreach.mensagensPorDia', '7');
let checks = 0;
let sends = 0;
const whatsapp = {
getStatus: () => ({ state, connected: state === 'READY' }),
checkNumber: async (number) => {
checks += 1;
return { registered: true, numero: number, numero_resolvido: number };
},
sendText: async () => {
sends += 1;
throw new Error('rota de campanha nao pode enviar mensagens');
}
};
const router = createRouter();
registerCampaignRoutes(router, { db, whatsapp, random, now: () => now });
return { db, router, whatsapp, counts: () => ({ checks, sends }) };
}
async function invoke(router, method, pathname, {
body,
user = { role: 'admin' },
query = new URLSearchParams()
} = {}) {
const matched = router.match(method, pathname);
assert.ok(matched, `rota ausente: ${method} ${pathname}`);
const calls = [];
await matched.handler(null, null, {
params: matched.params,
body,
user,
query,
json: (status, data) => calls.push({ status, data })
});
assert.equal(calls.length, 1);
return calls[0];
}
function campaignIds(db) {
return db.prepare('SELECT id FROM outreach_campaigns ORDER BY id').all().map((row) => row.id);
}
function addPreview(db, campaignId, suffix) {
const campaign = db.prepare('SELECT produto FROM outreach_campaigns WHERE id = ?').get(campaignId);
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ? ORDER BY code LIMIT 1').get(campaignId);
const lead = db.prepare(
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score)
VALUES (?, ?, ?, 'restaurantes', 1, 90)`
).run(`lead-${campaignId}-${suffix}`, `Empresa ${suffix}`, `551199999${String(suffix).padStart(4, '0')}`);
return Number(db.prepare(
`INSERT INTO outreach_campaign_queue
(campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status)
VALUES (?, ?, ?, ?, ?, 'restaurantes', 'mensagem aprovada', 'teste', 'preview')`
).run(campaignId, Number(lead.lastInsertRowid), variant.id, campaign.produto, `551199999${String(suffix).padStart(4, '0')}`).lastInsertRowid);
}
function schedulePreview(db, queueId, hour = 9, minute = 30) {
const scheduledAt = new Date(2026, 6, 29, hour, minute).toISOString();
db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = ? WHERE id = ?")
.run(scheduledAt, queueId);
}
test('lista, detalha e filtra fila sem misturar uso manual e automatico', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
addPreview(db, campaignId, 1);
db.prepare(
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
VALUES ('G-Delivery', '5511999999001', 'manual', 'aceito', 'manual')`
).run();
for (const [index, status] of ['pendente', 'enviado', 'entregue', 'lido', 'desconhecido', 'erro'].entries()) {
db.prepare(
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
VALUES ('G-Delivery', ?, 'manual', ?, 'manual')`
).run(`55118888000${index}`, status);
}
const automaticSendId = Number(db.prepare(
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
VALUES ('G-Delivery', '5511999999002', 'campanha', 'aceito', 'automatico')`
).run().lastInsertRowid);
const queued = addPreview(db, campaignId, 2);
schedulePreview(db, queued);
db.prepare("UPDATE outreach_campaign_queue SET status = 'reserved' WHERE id = ?").run(queued);
db.prepare("UPDATE outreach_campaign_queue SET status = 'sent', outreach_send_id = ? WHERE id = ?")
.run(automaticSendId, queued);
const listed = await invoke(router, 'GET', '/api/campaigns', { user: { role: 'comercial' } });
assert.equal(listed.status, 200);
assert.equal(listed.data.campaigns.length, 3);
const campaign = listed.data.campaigns.find((item) => item.id === campaignId);
assert.deepEqual(campaign.usage, {
manual: { limit: 7, used: 6, remaining: 1 },
campaign: { limit: 10, used: 1, remaining: 9 }
});
const detail = await invoke(router, 'GET', `/api/campaigns/${campaignId}`, { user: { role: 'comercial' } });
assert.equal(detail.data.campaign.id, campaignId);
assert.deepEqual(detail.data.usage, campaign.usage);
const queue = await invoke(router, 'GET', `/api/campaigns/${campaignId}/queue`, {
user: { role: 'comercial' }, query: new URLSearchParams({ status: 'preview' })
});
assert.deepEqual(queue.data.queue.map((row) => row.status), ['preview']);
db.close();
});
test('todas as mutacoes rejeitam usuario nao-admin antes de alterar estado', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
const queueId = addPreview(db, campaignId, 3);
const requests = [
['POST', `/api/campaigns/${campaignId}/prepare`, {}],
['POST', `/api/campaigns/${campaignId}/approve`, { queueIds: [queueId], date: '2026-07-27' }],
['DELETE', `/api/campaigns/${campaignId}/queue/${queueId}`, undefined],
['POST', `/api/campaigns/${campaignId}/start`, {}],
['POST', `/api/campaigns/${campaignId}/pause`, { reason: 'revisao' }],
['PUT', `/api/campaigns/${campaignId}/week`, { week: 2 }]
];
for (const [method, path, body] of requests) {
const response = await invoke(router, method, path, { body, user: { role: 'comercial' } });
assert.equal(response.status, 403, `${method} ${path}`);
}
assert.equal(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id = ?').get(queueId).status, 'preview');
assert.equal(db.prepare('SELECT week_number FROM outreach_campaigns WHERE id = ?').get(campaignId).week_number, 1);
db.close();
});
test('preparacao usa verificacao gradual e nunca chama envio', async () => {
const { db, router, counts } = setup();
const [campaignId] = campaignIds(db);
db.prepare(
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score)
VALUES ('eligible', 'Hamburgueria Sol', '5511999990001', 'hamburguerias', 1, 100)`
).run();
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/prepare`, { body: {} });
assert.equal(response.status, 200);
assert.equal(response.data.summary.previewed, 1);
assert.deepEqual(counts(), { checks: 1, sends: 0 });
db.close();
});
test('aprovacao exige IDs explicitos da propria campanha e faz transicao atomica', async () => {
const { db, router } = setup();
const [campaignId, otherCampaignId] = campaignIds(db);
const selected = addPreview(db, campaignId, 10);
const removed = addPreview(db, campaignId, 11);
const foreign = addPreview(db, otherCampaignId, 12);
const originalPayload = db.prepare(
'SELECT variant_id, rendered_message FROM outreach_campaign_queue WHERE id = ?'
).get(selected);
const missing = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { date: '2026-07-27' }
});
assert.equal(missing.status, 400);
const mixed = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { queueIds: [selected, foreign], date: '2026-07-27' }
});
assert.equal(mixed.status, 409);
assert.deepEqual(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id IN (?, ?) ORDER BY id').all(selected, removed).map((row) => row.status), ['preview', 'preview']);
const approved = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { queueIds: [selected], date: '2026-07-27' }
});
assert.equal(approved.status, 200);
assert.equal(approved.data.scheduled, 1);
const rows = db.prepare(
'SELECT id, status, variant_id, rendered_message, scheduled_at, cancel_reason FROM outreach_campaign_queue WHERE campaign_id = ? ORDER BY id'
).all(campaignId);
assert.equal(rows[0].status, 'scheduled');
assert.match(rows[0].scheduled_at, /^2026-07-27T/);
assert.equal(rows[0].variant_id, originalPayload.variant_id);
assert.equal(rows[0].rendered_message, originalPayload.rendered_message);
const { variant_id: ignoredVariant, rendered_message: ignoredMessage, ...removedRow } = rows[1];
void ignoredVariant;
void ignoredMessage;
assert.deepEqual({ ...removedRow }, { id: removed, status: 'cancelled', scheduled_at: null, cancel_reason: 'removed_by_operator' });
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'ready');
db.close();
});
test('aprovacao reverte tudo quando a data ou janelas nao fornecem slots suficientes', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
const first = addPreview(db, campaignId, 20);
const second = addPreview(db, campaignId, 21);
db.prepare("UPDATE outreach_campaigns SET windows_json = '[{\"start\":\"09:30\",\"end\":\"09:30\"}]' WHERE id = ?").run(campaignId);
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { queueIds: [first, second], date: '2026-07-27' }
});
assert.equal(response.status, 409);
assert.deepEqual(db.prepare('SELECT status FROM outreach_campaign_queue WHERE campaign_id = ? ORDER BY id').all(campaignId).map((row) => row.status), ['preview', 'preview']);
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'paused');
db.close();
});
test('aprovacao rejeita excesso somando fila ja ativa no mesmo dia local', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
const existing = Array.from({ length: 10 }, (_, index) => addPreview(db, campaignId, 100 + index));
existing.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
const selected = addPreview(db, campaignId, 111);
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { queueIds: [selected], date: '2026-07-29' }
});
assert.equal(response.status, 409);
assert.equal(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id = ?').get(selected).status, 'preview');
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_campaign_queue WHERE campaign_id = ? AND status = 'scheduled'").get(campaignId).total, 10);
db.close();
});
test('remocao cancela somente item mutavel pertencente a campanha', async () => {
const { db, router } = setup();
const [campaignId, otherCampaignId] = campaignIds(db);
const removable = addPreview(db, campaignId, 30);
const foreign = addPreview(db, otherCampaignId, 31);
const sent = addPreview(db, campaignId, 32);
schedulePreview(db, sent);
db.prepare("UPDATE outreach_campaign_queue SET status = 'reserved' WHERE id = ?").run(sent);
db.prepare("UPDATE outreach_campaign_queue SET status = 'sent' WHERE id = ?").run(sent);
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${foreign}`)).status, 404);
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${sent}`)).status, 409);
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${removable}`)).status, 200);
assert.deepEqual({ ...db.prepare('SELECT status, cancel_reason FROM outreach_campaign_queue WHERE id = ?').get(removable) }, {
status: 'cancelled', cancel_reason: 'removed_by_operator'
});
db.close();
});
test('remocao de item agendado preserva timestamp imutavel de auditoria', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
const queueId = addPreview(db, campaignId, 33);
schedulePreview(db, queueId);
const before = db.prepare('SELECT scheduled_at FROM outreach_campaign_queue WHERE id = ?').get(queueId).scheduled_at;
const response = await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${queueId}`);
assert.equal(response.status, 200);
assert.deepEqual({ ...db.prepare('SELECT status, scheduled_at, cancel_reason FROM outreach_campaign_queue WHERE id = ?').get(queueId) }, {
status: 'cancelled', scheduled_at: before, cancel_reason: 'removed_by_operator'
});
db.close();
});
test('inicio exige fila agendada, WhatsApp READY e estado ready ou paused', async () => {
const noSchedule = setup();
const [campaignId] = campaignIds(noSchedule.db);
assert.equal((await invoke(noSchedule.router, 'POST', `/api/campaigns/${campaignId}/start`, { body: {} })).status, 409);
noSchedule.db.close();
const disconnected = setup({ state: 'DISCONNECTED' });
const [disconnectedId] = campaignIds(disconnected.db);
const queueId = addPreview(disconnected.db, disconnectedId, 40);
disconnected.db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = '2026-07-27T09:30:00.000' WHERE id = ?").run(queueId);
assert.equal((await invoke(disconnected.router, 'POST', `/api/campaigns/${disconnectedId}/start`, { body: {} })).status, 409);
assert.equal(disconnected.db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(disconnectedId).status, 'paused');
disconnected.db.close();
const ready = setup();
const [readyId] = campaignIds(ready.db);
const scheduled = addPreview(ready.db, readyId, 41);
ready.db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = '2026-07-27T09:30:00.000' WHERE id = ?").run(scheduled);
const started = await invoke(ready.router, 'POST', `/api/campaigns/${readyId}/start`, { body: {} });
assert.equal(started.status, 200);
assert.equal(started.data.campaign.status, 'running');
ready.db.close();
});
test('inicio rejeita dia local com tentativas ativas acima do limite aprovado', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
const queueIds = Array.from({ length: 11 }, (_, index) => addPreview(db, campaignId, 200 + index));
queueIds.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/start`, { body: {} });
assert.equal(response.status, 409);
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'paused');
db.close();
});
test('reducao de semana rejeita limite abaixo da fila ativa do dia sem alterar controles', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
db.prepare('UPDATE outreach_campaigns SET week_number = 4, approved_daily_limit = 18 WHERE id = ?').run(campaignId);
const queueIds = Array.from({ length: 11 }, (_, index) => addPreview(db, campaignId, 300 + index));
queueIds.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
const response = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 1 } });
assert.equal(response.status, 409);
assert.deepEqual({ ...db.prepare('SELECT week_number, approved_daily_limit FROM outreach_campaigns WHERE id = ?').get(campaignId) }, {
week_number: 4, approved_daily_limit: 18
});
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_campaign_queue WHERE campaign_id = ? AND status = 'scheduled'").get(campaignId).total, 11);
db.close();
});
test('pausa exige motivo e semana muda somente pelo mapa explicito', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
assert.equal((await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, { body: {} })).status, 400);
const paused = await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, { body: { reason: 'revisao diaria' } });
assert.equal(paused.status, 200);
assert.equal(paused.data.campaign.pause_reason, 'revisao diaria');
assert.equal((await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: {} })).status, 400);
assert.equal(db.prepare('SELECT week_number FROM outreach_campaigns WHERE id = ?').get(campaignId).week_number, 1);
for (const [week, limit] of [[1, 10], [2, 12], [3, 15], [4, 18]]) {
const changed = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week } });
assert.equal(changed.status, 200);
assert.equal(changed.data.campaign.week_number, week);
assert.equal(changed.data.campaign.approved_daily_limit, limit);
}
assert.equal((await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 5 } })).status, 400);
db.close();
});
test('pausa aceita somente ready, running ou paused e preserva estados terminais', async () => {
const { db, router } = setup();
const [, draftId, completedId] = campaignIds(db);
db.prepare("UPDATE outreach_campaigns SET status = 'draft' WHERE id = ?").run(draftId);
db.prepare("UPDATE outreach_campaigns SET status = 'completed' WHERE id = ?").run(completedId);
for (const campaignId of [draftId, completedId]) {
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, {
body: { reason: 'nao deve alterar' }
});
assert.equal(response.status, 409);
}
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(draftId).status, 'draft');
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(completedId).status, 'completed');
db.close();
});
test('falha operacional retorna 500 generico sem vazar mensagem SQLite', async () => {
const { db, router } = setup();
const [campaignId] = campaignIds(db);
db.exec(`
CREATE TRIGGER campaign_operational_failure
BEFORE UPDATE ON outreach_campaigns
BEGIN SELECT RAISE(FAIL, 'sqlite-secret-detail'); END;
`);
const response = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 2 } });
assert.deepEqual(response, { status: 500, data: { erro: 'falha interna ao atualizar campanha' } });
assert.doesNotMatch(JSON.stringify(response), /sqlite-secret-detail/i);
db.close();
});
test('aprovacao separa validacao conhecida de falha inesperada do agendador', async () => {
const invalid = setup();
const [invalidCampaignId] = campaignIds(invalid.db);
const invalidQueueId = addPreview(invalid.db, invalidCampaignId, 500);
const validation = await invoke(invalid.router, 'POST', `/api/campaigns/${invalidCampaignId}/approve`, {
body: { queueIds: [invalidQueueId], date: '2026-02-30' }
});
assert.equal(validation.status, 400);
assert.match(validation.data.erro, /data local invalida/i);
invalid.db.close();
const secret = 'SCHEDULE-SENTINEL-SECRET';
const unexpected = setup({ random: () => { throw new Error(secret); } });
const [campaignId] = campaignIds(unexpected.db);
const first = addPreview(unexpected.db, campaignId, 501);
const second = addPreview(unexpected.db, campaignId, 502);
const failure = await invoke(unexpected.router, 'POST', `/api/campaigns/${campaignId}/approve`, {
body: { queueIds: [first, second], date: '2026-07-29' }
});
assert.deepEqual(failure, { status: 500, data: { erro: 'falha interna ao atualizar campanha' } });
assert.doesNotMatch(JSON.stringify(failure), new RegExp(secret, 'i'));
assert.deepEqual(unexpected.db.prepare('SELECT status FROM outreach_campaign_queue ORDER BY id').all().map((row) => row.status), ['preview', 'preview']);
unexpected.db.close();
});
test('GET /api/campaigns/replies/pending retorna lista de leads que responderam', async () => {
const { db, router } = setup();
const campaigns = listCampaigns(db);
const leadId1 = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-pending-1', 'Lead Respondeu', '5511999991001')"
).run().lastInsertRowid);
const leadId2 = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-pending-2', 'Lead Avancado', '5511999991002')"
).run().lastInsertRowid);
setLeadCommercialStage(db, { leadId: leadId1, campaignId: campaigns[0].id, stage: 'respondeu', note: 'quer info' });
setLeadCommercialStage(db, { leadId: leadId2, campaignId: campaigns[0].id, stage: 'enviado' });
const response = await invoke(router, 'GET', '/api/campaigns/replies/pending');
assert.equal(response.status, 200);
assert.equal(response.data.pending.length, 1);
assert.equal(response.data.pending[0].lead_id, leadId1);
db.close();
});
test('PUT /api/campaigns/leads/:leadId/stage atualiza etapa comercial', async () => {
const { db, router } = setup();
const leadId = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-stage-1', 'Lead Stage', '5511999991010')"
).run().lastInsertRowid);
const response = await invoke(router, 'PUT', `/api/campaigns/leads/${leadId}/stage`, {
body: { stage: 'interessado_gti_vendas', note: 'demonstracao marcada' }
});
assert.equal(response.status, 200);
assert.equal(response.data.state.stage, 'interessado_gti_vendas');
db.close();
});
test('PUT /api/campaigns/leads/:leadId/stage rejeita etapa invalida', async () => {
const { db, router } = setup();
const leadId = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-stage-2', 'Lead Inv', '5511999991020')"
).run().lastInsertRowid);
const response = await invoke(router, 'PUT', `/api/campaigns/leads/${leadId}/stage`, {
body: { stage: 'etapa_inexistente' }
});
assert.equal(response.status, 400);
assert.match(JSON.stringify(response.data), /etapa comercial invalida/i);
db.close();
});
test('POST /api/campaigns/leads/:leadId/appointments cria agendamento', async () => {
const { db, router } = setup();
const campaigns = listCampaigns(db);
const leadId = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-app-1', 'Lead App', '5511999991030')"
).run().lastInsertRowid);
const response = await invoke(router, 'POST', `/api/campaigns/leads/${leadId}/appointments`, {
body: {
campaignId: campaigns[0].id,
kind: 'demonstracao',
participantName: 'Maria',
companyName: 'Empresa App',
preferredDay: '2026-08-10',
preferredTime: '15:00'
}
});
assert.equal(response.status, 201);
assert.equal(response.data.appointment.kind, 'demonstracao');
assert.equal(response.data.appointment.participant_name, 'Maria');
db.close();
});
test('POST /api/campaigns/leads/:leadId/appointments rejeita demonstracao sem campos obrigatorios', async () => {
const { db, router } = setup();
const leadId = Number(db.prepare(
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-app-2', 'Lead App Fail', '5511999991040')"
).run().lastInsertRowid);
const response = await invoke(router, 'POST', `/api/campaigns/leads/${leadId}/appointments`, {
body: { kind: 'demonstracao' }
});
assert.equal(response.status, 400);
db.close();
});
test('GET /api/campaigns/replies/pending funciona sem autenticacao de admin', async () => {
const { db, router } = setup();
const response = await invoke(router, 'GET', '/api/campaigns/replies/pending', { user: { role: 'comercial' } });
assert.equal(response.status, 200);
assert.ok(Array.isArray(response.data.pending));
db.close();
});