test: añade pruebas para GroupSyncService y webhook
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>pull/1/head
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']);
|
||||
});
|
||||
});
|
||||
@ -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…
Reference in New Issue