diff --git a/tests/unit/server/groups-upsert.sync.test.ts b/tests/unit/server/groups-upsert.sync.test.ts new file mode 100644 index 0000000..df999a1 --- /dev/null +++ b/tests/unit/server/groups-upsert.sync.test.ts @@ -0,0 +1,87 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { WebhookServer } from '../../../src/server'; +import { GroupSyncService } from '../../../src/services/group-sync'; +import { makeMemDb, injectAllServices, resetServices } from '../../helpers/db'; +import type { Database as SqliteDatabase } from 'bun:sqlite'; + +describe('WebhookServer - groups.upsert encadena syncGroups y syncMembersForActiveGroups', () => { + const envBackup = { ...process.env }; + let db: SqliteDatabase; + + let originalSyncGroups: any; + let originalRefresh: any; + let originalSyncMembers: any; + + let calledSyncGroups = 0; + let calledRefresh = 0; + let calledSyncMembers = 0; + + beforeEach(() => { + process.env = { + ...envBackup, + NODE_ENV: 'production', + EVOLUTION_API_INSTANCE: 'inst-1', + EVOLUTION_API_URL: 'http://localhost:1234', + EVOLUTION_API_KEY: 'dummy', + CHATBOT_PHONE_NUMBER: '123456789', + WEBHOOK_URL: 'http://localhost:3000/webhook' + }; + + db = makeMemDb(); + // Inyectar DB en servicios + (WebhookServer as any).dbInstance = db; + injectAllServices(db); + + // Guardar originales y stubear + originalSyncGroups = GroupSyncService.syncGroups; + originalRefresh = GroupSyncService.refreshActiveGroupsCache; + originalSyncMembers = GroupSyncService.syncMembersForActiveGroups; + + calledSyncGroups = 0; + calledRefresh = 0; + calledSyncMembers = 0; + + GroupSyncService.syncGroups = async () => { + calledSyncGroups++; + return { added: 0, updated: 0 }; + }; + GroupSyncService.refreshActiveGroupsCache = () => { + calledRefresh++; + }; + GroupSyncService.syncMembersForActiveGroups = async () => { + calledSyncMembers++; + return { groups: 0, added: 0, updated: 0, deactivated: 0 }; + }; + }); + + afterEach(async () => { + // Restaurar stubs + GroupSyncService.syncGroups = originalSyncGroups; + GroupSyncService.refreshActiveGroupsCache = originalRefresh; + GroupSyncService.syncMembersForActiveGroups = originalSyncMembers; + + resetServices(); + try { db.close(); } catch {} + process.env = envBackup; + }); + + test('dispara syncGroups -> refreshActiveGroupsCache -> syncMembersForActiveGroups y responde 200', async () => { + const payload = { + event: 'groups.upsert', + instance: 'inst-1', + data: { any: 'thing' } + }; + + const req = new Request('http://localhost/webhook', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const res = await WebhookServer.handleRequest(req); + expect(res.status).toBe(200); + expect(calledSyncGroups).toBe(1); + expect(calledRefresh).toBe(1); + expect(calledSyncMembers).toBe(1); + }); +}); diff --git a/tests/unit/services/group-sync.coverage.test.ts b/tests/unit/services/group-sync.coverage.test.ts new file mode 100644 index 0000000..2629676 --- /dev/null +++ b/tests/unit/services/group-sync.coverage.test.ts @@ -0,0 +1,53 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import type { Database as SqliteDatabase } from 'bun:sqlite'; +import { makeMemDb, injectAllServices, resetServices } from '../../helpers/db'; +import { GroupSyncService } from '../../../src/services/group-sync'; +import { IdentityService } from '../../../src/services/identity'; +import { Metrics } from '../../../src/services/metrics'; + +describe('GroupSyncService - alias_coverage_ratio', () => { + const envBackup = { ...process.env }; + let db: SqliteDatabase; + + beforeEach(() => { + process.env = { ...envBackup, NODE_ENV: 'test', METRICS_ENABLED: 'true' }; + Metrics.reset(); + db = makeMemDb(); + injectAllServices(db); + (GroupSyncService as any).dbInstance = db; + (IdentityService as any).dbInstance = db; + + // Crear grupo activo requerido por FK + db.prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`) + .run('g1@g.us', 'comm-1', 'G1'); + }); + + afterEach(() => { + resetServices(); + try { db.close(); } catch {} + Metrics.reset(); + process.env = envBackup; + try { (IdentityService as any).inMemoryAliases?.clear?.(); } catch {} + }); + + test('calcula cobertura en función de dígitos y alias resolubles', () => { + // Sembrar un alias resoluble: aliasB -> 222 + IdentityService.upsertAlias('aliasB', '222', 'test'); + + // Reconciliar miembros: 111 (dígitos), aliasA (no resoluble), aliasB (resoluble a 222) + GroupSyncService.reconcileGroupMembers('g1@g.us', [ + { userId: '111', isAdmin: false }, + { userId: 'aliasA', isAdmin: false }, + { userId: 'aliasB', isAdmin: false }, + ], '2025-01-01 00:00:00.000'); + + const json = JSON.parse(Metrics.render('json')); + const key = 'group_id="g1@g.us"'; + const ratio = json?.labeledGauges?.alias_coverage_ratio?.[key]; + + expect(typeof ratio).toBe('number'); + // 2 resolubles (111 y aliasB) de 3 totales -> 0.666... + expect(ratio).toBeGreaterThan(0.66 - 1e-6); + expect(ratio).toBeLessThan(0.67 + 1e-6); + }); +});