import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; import { DatabaseSync } from 'node:sqlite'; import { openDb, setSetting } from '../../src/db.mjs'; import { hashPassword } from '../../src/auth.mjs'; import { createUser } from '../../src/db.mjs'; import { createAppServer } from '../../src/server/index.mjs'; import { seedCampaigns } from '../../src/outreach/campaign-store.mjs'; const DEBUG = process.env.DEBUG_SERVER === '1'; const log = DEBUG ? (m) => process.stderr.write(`[test-server] ${m}\n`) : () => {}; function randomHex(length) { const bytes = new Uint8Array(Math.ceil(length / 2)); crypto.getRandomValues(bytes); return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, length); } export async function startTestServer() { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-')); const dbPath = path.join(tmpDir, 'test.db'); process.env.APP_DB_PATH = dbPath; process.env.APP_WHATSAPP_AUTH_DIR = path.join(tmpDir, 'whatsapp-auth'); const db = openDb(dbPath); const adminPw = 'admin-senha-teste'; createUser(db, { username: 'admin', passwordHash: hashPassword(adminPw), role: 'admin' }); seedCampaigns(db); setSetting(db, 'outreach.mensagensPorDia', '10'); const campaignIds = db.prepare('SELECT id, code FROM outreach_campaigns ORDER BY id').all(); const leadIds = []; const empresas = { '1': ['Hamburgueria Bella', 'Pizzaria Famosa', 'Acaiteria Tropical', 'Marmitaria Sabor', 'Lanchonete do Ze'], '2': ['Restaurante Chefe', 'Mercado Central', 'Mercearia Sao Jose', 'Minimercado Bom', 'Padaria Pao Quente'], '3': ['Loja de Moda', 'Material Construcao', 'Autopecas Veloz', 'Oficina Mecanica'] }; let leadIdCounter = 1; for (const c of campaignIds) { const names = empresas[c.code] || ['Empresa Generica']; for (const nome of names) { const whatsapp = `5511999${String(leadIdCounter).padStart(6, '0')}`; const r = db.prepare( `INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score, classificacao, telefone_valido) VALUES (?, ?, ?, ?, ?, ?, 'bom', 1)` ).run( `browser-test-lead-${leadIdCounter}`, nome, whatsapp, c.code === 'gdelivery-food' ? 'hamburguerias' : c.code === 'gti-food' ? 'restaurantes' : 'lojas_de_roupa', leadIdCounter <= 5 ? 1 : 2, 100 - leadIdCounter ); const lid = Number(r.lastInsertRowid); leadIds.push({ id: lid, empresa: nome, whatsapp, campaignId: c.id }); db.prepare( `INSERT INTO whatsapp_number_verifications (numero, status, numero_resolvido, checked_at) VALUES (?, 'confirmado', ?, datetime('now'))` ).run(whatsapp, whatsapp); leadIdCounter += 1; } } const firstLead = leadIds[0]; setSetting(db, 'outreach.ativo', 'true'); const firstCampaignId = campaignIds[0].id; const variantIds = db.prepare( 'SELECT id FROM outreach_campaign_variants WHERE campaign_id = ? ORDER BY code LIMIT 1' ).all(firstCampaignId); if (variantIds.length > 0 && firstLead) { const variantId = variantIds[0].id; const produto = db.prepare('SELECT produto FROM outreach_campaigns WHERE id = ?').get(firstCampaignId).produto; db.prepare( `INSERT INTO outreach_campaign_queue (campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'preview')` ).run(firstCampaignId, firstLead.id, variantId, produto, firstLead.whatsapp, 'hamburguerias', 'Ola! Teste de mensagem renderizada para campanha.', 'browser-test'); } const stageLeadId = leadIds.length > 2 ? leadIds[2].id : (firstLead ? firstLead.id : null); if (stageLeadId) { db.prepare( `INSERT INTO outreach_lead_states (lead_id, stage, first_reply_at) VALUES (?, 'respondeu', datetime('now'))` ).run(stageLeadId); db.prepare( `INSERT INTO outreach_stage_events (lead_id, campaign_id, stage, note) VALUES (?, ?, 'respondeu', 'cliente quer saber mais sobre G-Delivery')` ).run(stageLeadId, firstCampaignId); } const replyLeadId = leadIds.length > 1 ? leadIds[1].id : (firstLead ? firstLead.id : null); if (replyLeadId) { db.prepare( `INSERT OR IGNORE INTO outreach_inbound_replies (provider_message_id, lead_id, numero, received_at) VALUES (?, ?, ?, datetime('now'))` ).run(`test-inbound-${Date.now()}`, replyLeadId, leadIds[1]?.whatsapp || '5511999990002'); } let fakeSends = []; const whatsapp = { getStatus: () => ({ state: 'READY', connected: true }), getQr: () => null, connect: async () => {}, disconnect: async () => {}, checkNumber: async (number) => ({ registered: true, numero: number, numeroResolvido: number, status: 'confirmado' }), sendText: async () => { fakeSends.push({ at: new Date().toISOString() }); throw new Error('fake manager nao envia mensagens reais'); } }; const jobs = { subscribe: () => null, activeJobs: () => [] }; const server = createAppServer({ db, jobs, whatsapp, log }); return new Promise((resolve, reject) => { server.listen(3911, '127.0.0.1', () => { log(`server listening on 3911`); const apiRequest = async (method, path, options = {}) => { const url = `http://127.0.0.1:3911${path}`; const headers = { ...options.headers }; if (options.body && !headers['content-type']) { headers['content-type'] = 'application/json'; } const response = await fetch(url, { method, headers, body: options.body ? JSON.stringify(options.body) : undefined }); let data = null; try { data = await response.json(); } catch { data = null; } return { status: response.status, headers: response.headers, data, raw: response }; }; const teardown = async () => { await new Promise((r) => server.close(r)); db.close(); try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} log('test server teardown complete'); }; resolve({ server, db, tmpDir, whatsapp, fakeSends, apiRequest, teardown, adminPw }); }); server.on('error', reject); }); }