test: añade pruebas para GroupSyncService y webhook

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
borja 2 months ago
parent a092a25234
commit b014015768

@ -0,0 +1,55 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { WebhookServer } from '../../src/server';
import { GroupSyncService } from '../../src/services/group-sync';
const envBackup = { ...process.env };
describe('WebhookServer - evento GROUPS_UPSERT', () => {
let originalSyncGroups: any;
let originalRefresh: any;
let originalSyncMembers: any;
beforeEach(() => {
process.env = { ...envBackup, NODE_ENV: 'test' }; // omite verificación de instancia
originalSyncGroups = GroupSyncService.syncGroups;
originalRefresh = (GroupSyncService as any).refreshActiveGroupsCache;
originalSyncMembers = GroupSyncService.syncMembersForActiveGroups;
});
afterEach(() => {
GroupSyncService.syncGroups = originalSyncGroups;
(GroupSyncService as any).refreshActiveGroupsCache = originalRefresh;
GroupSyncService.syncMembersForActiveGroups = originalSyncMembers;
process.env = envBackup;
});
test('dispara syncGroups -> refresh -> syncMembers', async () => {
const calls: string[] = [];
GroupSyncService.syncGroups = async () => {
calls.push('syncGroups');
return { added: 0, updated: 0 };
};
(GroupSyncService as any).refreshActiveGroupsCache = () => {
calls.push('refresh');
};
GroupSyncService.syncMembersForActiveGroups = async () => {
calls.push('syncMembers');
return { groups: 0, added: 0, updated: 0, deactivated: 0 };
};
const payload = {
event: 'GROUPS_UPSERT',
instance: 'any-instance',
data: {}
};
const req = new Request('http://localhost:3007', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const res = await WebhookServer.handleRequest(req);
expect(res.status).toBe(200);
expect(calls).toEqual(['syncGroups', 'refresh', 'syncMembers']);
});
});

@ -320,6 +320,19 @@ describe('WebhookServer', () => {
test('should handle requests on configured port', async () => {
const originalPort = process.env.PORT;
process.env.PORT = '3007';
// Satisfacer validación de entorno en start()
const prevEnv = {
EVOLUTION_API_URL: process.env.EVOLUTION_API_URL,
EVOLUTION_API_KEY: process.env.EVOLUTION_API_KEY,
EVOLUTION_API_INSTANCE: process.env.EVOLUTION_API_INSTANCE,
CHATBOT_PHONE_NUMBER: process.env.CHATBOT_PHONE_NUMBER,
WEBHOOK_URL: process.env.WEBHOOK_URL
};
process.env.EVOLUTION_API_URL = 'http://localhost:3000';
process.env.EVOLUTION_API_KEY = 'test-key';
process.env.EVOLUTION_API_INSTANCE = 'test-instance';
process.env.CHATBOT_PHONE_NUMBER = '9999999999';
process.env.WEBHOOK_URL = 'http://localhost:3007';
try {
const server = await WebhookServer.start();
@ -328,6 +341,11 @@ describe('WebhookServer', () => {
server.stop();
} finally {
process.env.PORT = originalPort;
process.env.EVOLUTION_API_URL = prevEnv.EVOLUTION_API_URL;
process.env.EVOLUTION_API_KEY = prevEnv.EVOLUTION_API_KEY;
process.env.EVOLUTION_API_INSTANCE = prevEnv.EVOLUTION_API_INSTANCE;
process.env.CHATBOT_PHONE_NUMBER = prevEnv.CHATBOT_PHONE_NUMBER;
process.env.WEBHOOK_URL = prevEnv.WEBHOOK_URL;
}
});

@ -0,0 +1,98 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { GroupSyncService } from '../../../src/services/group-sync';
const ORIGINAL_FETCH = globalThis.fetch;
describe('GroupSyncService - fetchGroupMembersFromAPI (parsing y fallbacks)', () => {
const envBackup = { ...process.env };
beforeEach(() => {
process.env = {
...envBackup,
NODE_ENV: 'development', // evitar early-return de test
EVOLUTION_API_URL: 'http://evolution.test',
EVOLUTION_API_INSTANCE: 'instance-1',
EVOLUTION_API_KEY: 'apikey'
};
});
afterEach(() => {
process.env = envBackup;
globalThis.fetch = ORIGINAL_FETCH;
});
test('parsea /group/participants con múltiples formas de payload y roles', async () => {
globalThis.fetch = async (url: RequestInfo | URL) => {
if (String(url).includes('/group/participants/')) {
const body = {
participants: [
{ id: '553198296801@s.whatsapp.net', admin: 'superadmin' },
{ id: '1234567890@s.whatsapp.net', admin: 'admin' },
{ id: '1111111111@s.whatsapp.net', admin: null },
'2222222222@s.whatsapp.net',
{ jid: '3333333333@s.whatsapp.net', role: 'member' },
{ user: { id: '4444444444@s.whatsapp.net' }, isAdmin: true }
]
};
return new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
return new Response('not found', { status: 404 });
};
const out = await (GroupSyncService as any).fetchGroupMembersFromAPI('123@g.us');
// Debe mapear a IDs normalizados (solo dígitos) y detectar admin correctamente
const ids = out.map((x: any) => x.userId).sort();
expect(ids).toEqual(['1111111111','1234567890','2222222222','3333333333','4444444444','553198296801'].sort());
const adminMap = new Map(out.map((x: any) => [x.userId, x.isAdmin]));
expect(adminMap.get('553198296801')).toBe(true);
expect(adminMap.get('1234567890')).toBe(true);
expect(adminMap.get('1111111111')).toBe(false);
expect(adminMap.get('2222222222')).toBe(false);
expect(adminMap.get('3333333333')).toBe(false);
expect(adminMap.get('4444444444')).toBe(true);
});
test('fallback a fetchAllGroups cuando /group/participants no trae participants', async () => {
globalThis.fetch = async (url: RequestInfo | URL) => {
const u = String(url);
if (u.includes('/group/participants/')) {
// OK pero sin campo participants
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (u.includes('/group/fetchAllGroups/')) {
const wrapped = {
response: [
{ id: '123@g.us', participants: [{ id: '9999999999@s.whatsapp.net', admin: 'admin' }] }
]
};
return new Response(JSON.stringify(wrapped), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
return new Response('not found', { status: 404 });
};
const out = await (GroupSyncService as any).fetchGroupMembersFromAPI('123@g.us');
expect(out).toEqual([{ userId: '9999999999', isAdmin: true }]);
});
test('fallback devuelve snapshot vacío si el grupo no está presente', async () => {
globalThis.fetch = async (url: RequestInfo | URL) => {
const u = String(url);
if (u.includes('/group/participants/')) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (u.includes('/group/fetchAllGroups/')) {
const wrapped = {
response: [
{ id: 'other@g.us', participants: [{ id: '9999999999@s.whatsapp.net', admin: 'admin' }] }
]
};
return new Response(JSON.stringify(wrapped), { status: 200 });
}
return new Response('not found', { status: 404 });
};
const out = await (GroupSyncService as any).fetchGroupMembersFromAPI('123@g.us');
expect(out).toEqual([]);
});
});

@ -0,0 +1,48 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { GroupSyncService } from '../../../src/services/group-sync';
const envBackup = { ...process.env };
let originalSyncMembers: any;
function sleep(ms: number) {
return new Promise(res => setTimeout(res, ms));
}
describe('GroupSyncService - scheduler de miembros', () => {
beforeEach(() => {
originalSyncMembers = GroupSyncService.syncMembersForActiveGroups;
});
afterEach(() => {
GroupSyncService.stopMembersScheduler();
GroupSyncService.syncMembersForActiveGroups = originalSyncMembers;
process.env = envBackup;
});
test('no arranca en entorno de test', async () => {
process.env = { ...envBackup, NODE_ENV: 'test' };
let called = 0;
GroupSyncService.syncMembersForActiveGroups = async () => {
called++;
return { groups: 0, added: 0, updated: 0, deactivated: 0 };
};
GroupSyncService.startMembersScheduler();
await sleep(100);
expect(called).toBe(0);
});
test('arranca en producción y ejecuta según intervalo', async () => {
process.env = { ...envBackup, NODE_ENV: 'production', GROUP_MEMBERS_SYNC_INTERVAL_MS: '30' };
let called = 0;
GroupSyncService.syncMembersForActiveGroups = async () => {
called++;
return { groups: 0, added: 0, updated: 0, deactivated: 0 };
};
GroupSyncService.startMembersScheduler();
await sleep(120);
GroupSyncService.stopMembersScheduler();
expect(called).toBeGreaterThanOrEqual(1);
});
});

@ -0,0 +1,83 @@
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
import { initializeDatabase } from '../../../src/db';
import { GroupSyncService } from '../../../src/services/group-sync';
let memdb: Database;
const envBackup = { ...process.env };
let originalFetchMembers: any;
describe('GroupSyncService - syncMembersForActiveGroups (agregado por grupos)', () => {
beforeAll(() => {
memdb = new Database(':memory:');
memdb.exec('PRAGMA foreign_keys = ON;');
initializeDatabase(memdb);
GroupSyncService.dbInstance = memdb as any;
});
afterAll(() => {
memdb.close();
});
beforeEach(() => {
process.env = { ...envBackup, NODE_ENV: 'development' }; // evitar early return
// Reset tablas
memdb.exec('DELETE FROM group_members');
memdb.exec('DELETE FROM users');
memdb.exec('DELETE FROM groups');
// Grupo activo
memdb.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`)
.run('g1@g.us', 'community-1', 'Grupo 1');
// Refrescar caché de grupos activos
GroupSyncService.refreshActiveGroupsCache();
// Stub método de red
originalFetchMembers = (GroupSyncService as any).fetchGroupMembersFromAPI;
});
afterEach(() => {
(GroupSyncService as any).fetchGroupMembersFromAPI = originalFetchMembers;
process.env = envBackup;
});
test('primera pasada añade miembros y marca activos', async () => {
(GroupSyncService as any).fetchGroupMembersFromAPI = async (_gid: string) => ([
{ userId: '111', isAdmin: true },
{ userId: '222', isAdmin: false },
]);
const res = await GroupSyncService.syncMembersForActiveGroups();
expect(res).toEqual({ groups: 1, added: 2, updated: 0, deactivated: 0 });
const rows = memdb.prepare(`SELECT user_id, is_admin, is_active FROM group_members ORDER BY user_id`).all() as any[];
expect(rows).toHaveLength(2);
expect(rows[0]).toEqual({ user_id: '111', is_admin: 1, is_active: 1 });
expect(rows[1]).toEqual({ user_id: '222', is_admin: 0, is_active: 1 });
});
test('pasadas posteriores actualizan roles y desactivan ausentes', async () => {
// Semilla inicial
(GroupSyncService as any).fetchGroupMembersFromAPI = async (_gid: string) => ([
{ userId: '111', isAdmin: true },
{ userId: '222', isAdmin: false },
]);
await GroupSyncService.syncMembersForActiveGroups();
// Cambio: 111 pierde admin, 222 desaparece
(GroupSyncService as any).fetchGroupMembersFromAPI = async (_gid: string) => ([
{ userId: '111', isAdmin: false },
]);
const res2 = await GroupSyncService.syncMembersForActiveGroups();
expect(res2).toEqual({ groups: 1, added: 0, updated: 1, deactivated: 1 });
const rows = memdb.prepare(`SELECT user_id, is_admin, is_active FROM group_members ORDER BY user_id`).all() as any[];
const m111 = rows.find(r => r.user_id === '111');
const m222 = rows.find(r => r.user_id === '222');
expect(m111.is_admin).toBe(0);
expect(m111.is_active).toBe(1);
expect(m222.is_active).toBe(0);
});
});
Loading…
Cancel
Save