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:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { startTestServer } from './server.mjs';
|
||||
|
||||
let ctx;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
ctx = await startTestServer();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (ctx) await ctx.teardown();
|
||||
});
|
||||
|
||||
async function loginViaApi() {
|
||||
const login = await ctx.apiRequest('POST', '/api/auth/login', {
|
||||
body: { username: 'admin', password: ctx.adminPw }
|
||||
});
|
||||
expect(login.status).toBe(200);
|
||||
const setCookie = login.headers.get('set-cookie');
|
||||
return setCookie ? setCookie.split(';')[0] : '';
|
||||
}
|
||||
|
||||
async function setCookieOnPage(page, cookie) {
|
||||
const match = cookie ? cookie.match(/maquina_pessoal_session=([^;]+)/) : null;
|
||||
if (!match) return;
|
||||
await page.context().addCookies([{
|
||||
name: 'maquina_pessoal_session',
|
||||
value: match[1],
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'Strict'
|
||||
}]);
|
||||
}
|
||||
|
||||
test.describe('Campaign Pilot Browser Tests', () => {
|
||||
test.describe('Authentication', () => {
|
||||
test('login with seeded admin credentials', async ({ page }) => {
|
||||
await page.goto('/login.html');
|
||||
await expect(page.locator('#form-login')).toBeVisible();
|
||||
|
||||
await page.fill('#login-username', 'admin');
|
||||
await page.fill('#login-password', ctx.adminPw);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL('**/', { timeout: 10000 });
|
||||
await expect(page.locator('#usuario-logado')).toContainText('admin');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Campaign Cards', () => {
|
||||
let cookie;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cookie = await loginViaApi();
|
||||
});
|
||||
|
||||
test('campaign tab shows cards with correct status/limits', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const cards = page.locator('.campaign-card');
|
||||
const count = await cards.count();
|
||||
expect(count).toBe(3);
|
||||
|
||||
const cardText = await cards.first().textContent();
|
||||
expect(cardText).toContain('G-Delivery');
|
||||
expect(cardText).toContain('Semana 1');
|
||||
expect(cardText).toContain('Limite: 10');
|
||||
});
|
||||
|
||||
test('separate manual/automatic limits display', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const cardText = await page.locator('.campaign-card').first().textContent();
|
||||
expect(cardText).toMatch(/Manual|manual/);
|
||||
expect(cardText).toMatch(/Automático|auto/i);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Campaign Queue Operations (via API)', () => {
|
||||
let cookie;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cookie = await loginViaApi();
|
||||
});
|
||||
|
||||
test('prepare creates queue items', async () => {
|
||||
const { data } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaignId = data.campaigns[0].id;
|
||||
|
||||
const response = await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, {
|
||||
headers: { cookie }, body: {}
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data.summary.previewed).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('preview rows show server-rendered text', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaignId = campaigns.campaigns[0].id;
|
||||
|
||||
await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, {
|
||||
headers: { cookie }, body: {}
|
||||
});
|
||||
|
||||
const { data: queue } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
expect(queue.queue.length).toBeGreaterThanOrEqual(1);
|
||||
expect(queue.queue[0].rendered_message).toBeTruthy();
|
||||
expect(queue.queue[0].status).toBe('preview');
|
||||
});
|
||||
|
||||
test('per-row removal works', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaignId = campaigns.campaigns[0].id;
|
||||
|
||||
await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, {
|
||||
headers: { cookie }, body: {}
|
||||
});
|
||||
|
||||
const { data: before } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue?status=preview`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
|
||||
if (before.queue.length === 0) return;
|
||||
|
||||
const firstId = before.queue[0].id;
|
||||
const removeResponse = await ctx.apiRequest('DELETE', `/api/campaigns/${campaignId}/queue/${firstId}`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
expect(removeResponse.status).toBe(200);
|
||||
|
||||
const { data: after } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue?status=preview`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
expect(after.queue.find((q) => q.id === firstId)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('approval flow works', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaign = campaigns.campaigns.find((c) => c.status === 'paused');
|
||||
if (!campaign) return;
|
||||
|
||||
let { data: queue } = await ctx.apiRequest('GET', `/api/campaigns/${campaign.id}/queue?status=preview`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
|
||||
if (queue.queue.length === 0) {
|
||||
await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/prepare`, {
|
||||
headers: { cookie }, body: {}
|
||||
});
|
||||
const { data: refetched } = await ctx.apiRequest('GET', `/api/campaigns/${campaign.id}/queue?status=preview`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
if (refetched.queue.length === 0) return;
|
||||
queue.queue = refetched.queue;
|
||||
}
|
||||
|
||||
const ids = queue.queue.slice(0, 1).map((q) => q.id);
|
||||
const approveResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/approve`, {
|
||||
headers: { cookie },
|
||||
body: { queueIds: ids, date: '2026-07-29' }
|
||||
});
|
||||
expect(approveResponse.status).toBe(200);
|
||||
expect(approveResponse.data.scheduled).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const { data: updated } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const updatedCampaign = updated.campaigns.find((c) => c.id === campaign.id);
|
||||
expect(['ready', 'running']).toContain(updatedCampaign.status);
|
||||
});
|
||||
|
||||
test('pause shows reason', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaign = campaigns.campaigns.find((c) => c.status === 'ready' || c.status === 'paused');
|
||||
if (!campaign) return;
|
||||
|
||||
const pauseResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/pause`, {
|
||||
headers: { cookie },
|
||||
body: { reason: 'revisao manual do operador' }
|
||||
});
|
||||
expect(pauseResponse.status).toBe(200);
|
||||
expect(pauseResponse.data.campaign.pause_reason).toBe('revisao manual do operador');
|
||||
expect(pauseResponse.data.campaign.status).toBe('paused');
|
||||
});
|
||||
|
||||
test('start succeeds on paused campaign with scheduled items', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
const campaign = campaigns.campaigns.find((c) => c.status === 'paused' || c.status === 'ready');
|
||||
if (!campaign) return;
|
||||
|
||||
const startResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/start`, {
|
||||
headers: { cookie }, body: {}
|
||||
});
|
||||
expect(startResponse.status).toBe(200);
|
||||
expect(startResponse.data.campaign.status).toBe('running');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Stage Classification', () => {
|
||||
let cookie;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cookie = await loginViaApi();
|
||||
});
|
||||
|
||||
test('stage classification works via API', async () => {
|
||||
const leadId = Number(ctx.db.prepare(
|
||||
"SELECT id FROM leads WHERE empresa LIKE '%Restaurante%' LIMIT 1"
|
||||
).get()?.id);
|
||||
|
||||
if (!leadId) return;
|
||||
|
||||
const stageResponse = await ctx.apiRequest('PUT', `/api/campaigns/leads/${leadId}/stage`, {
|
||||
headers: { cookie },
|
||||
body: { stage: 'interessado_gti_vendas', note: 'demonstracao agendada' }
|
||||
});
|
||||
expect(stageResponse.status).toBe(200);
|
||||
expect(stageResponse.data.state.stage).toBe('interessado_gti_vendas');
|
||||
});
|
||||
|
||||
test('appointment/demo scheduling form works via API', async () => {
|
||||
const leadId = Number(ctx.db.prepare(
|
||||
"SELECT id FROM leads WHERE empresa LIKE '%Pizzaria%' LIMIT 1"
|
||||
).get()?.id);
|
||||
|
||||
if (!leadId) return;
|
||||
|
||||
const appointmentResponse = await ctx.apiRequest('POST', `/api/campaigns/leads/${leadId}/appointments`, {
|
||||
headers: { cookie },
|
||||
body: {
|
||||
kind: 'demonstracao',
|
||||
participantName: 'Maria Silva',
|
||||
companyName: 'Pizzaria Teste',
|
||||
preferredDay: '2026-08-05',
|
||||
preferredTime: '15:30'
|
||||
}
|
||||
});
|
||||
expect(appointmentResponse.status).toBe(201);
|
||||
expect(appointmentResponse.data.appointment.kind).toBe('demonstracao');
|
||||
});
|
||||
|
||||
test('pending replies section accessible via API', async () => {
|
||||
const { data } = await ctx.apiRequest('GET', '/api/campaigns/replies/pending', {
|
||||
headers: { cookie }
|
||||
});
|
||||
expect(Array.isArray(data.pending)).toBe(true);
|
||||
});
|
||||
|
||||
test('metrics endpoint returns data', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
if (campaigns.campaigns.length === 0) return;
|
||||
|
||||
const campaignId = campaigns.campaigns[0].id;
|
||||
const { data } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/metrics`, {
|
||||
headers: { cookie }
|
||||
});
|
||||
expect(data.metrics).toBeDefined();
|
||||
});
|
||||
|
||||
test('metrics supports segmento and variant filters', async () => {
|
||||
const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } });
|
||||
if (campaigns.campaigns.length === 0) return;
|
||||
|
||||
const campaignId = campaigns.campaigns[0].id;
|
||||
const { data: filtered } = await ctx.apiRequest(
|
||||
'GET',
|
||||
`/api/campaigns/${campaignId}/metrics?segmento=hamburguerias&variant=A`,
|
||||
{ headers: { cookie } }
|
||||
);
|
||||
expect(filtered.metrics).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI Rendering', () => {
|
||||
let cookie;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cookie = await loginViaApi();
|
||||
});
|
||||
|
||||
test('pending replies section visible on campaigns page', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const repliesSection = page.locator('#pending-replies');
|
||||
await expect(repliesSection).toBeVisible();
|
||||
const content = await repliesSection.textContent();
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('stage classification section renders', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const stageSection = page.locator('#stage-classification');
|
||||
await expect(stageSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('appointment scheduling form renders', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const appointmentSection = page.locator('#appointment-form');
|
||||
await expect(appointmentSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('metrics section renders with filters', async ({ page }) => {
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const metricsFilters = page.locator('#metrics-filters');
|
||||
await expect(metricsFilters).toBeVisible();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const filterSelects = metricsFilters.locator('select');
|
||||
const selectCount = await filterSelects.count();
|
||||
expect(selectCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Safety', () => {
|
||||
test('fake provider send count remains zero', async () => {
|
||||
expect(ctx.fakeSends.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mobile Viewport', () => {
|
||||
let cookie;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
cookie = await loginViaApi();
|
||||
});
|
||||
|
||||
test('mobile viewport renders campaign cards', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const cards = page.locator('.campaign-card');
|
||||
const count = await cards.count();
|
||||
expect(count).toBe(3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(cards.nth(i)).toBeVisible();
|
||||
}
|
||||
|
||||
const queueSection = page.locator('#campaign-queue');
|
||||
await expect(queueSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('mobile viewport shows full-width controls', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await setCookieOnPage(page, cookie);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click('button[data-tab="campanhas"]');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const section = page.locator('#tab-campanhas');
|
||||
const box = await section.boundingBox();
|
||||
expect(box.width).toBeGreaterThanOrEqual(350);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user