From 8d1d992248bdfe32315ef2901d4210132f2589ee Mon Sep 17 00:00:00 2001 From: brobert Date: Sat, 20 Sep 2025 22:13:38 +0200 Subject: [PATCH] =?UTF-8?q?test:=20a=C3=B1ade=20tests=20de=20alias=20@lid?= =?UTF-8?q?=20y=20resoluci=C3=B3n=20en=20group-sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- .../unit/services/identity-alias.e2e.test.ts | 85 +++++++++++++++++++ .../response-queue.alias-mentions.test.ts | 70 +++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 tests/unit/services/identity-alias.e2e.test.ts create mode 100644 tests/unit/services/response-queue.alias-mentions.test.ts diff --git a/tests/unit/services/identity-alias.e2e.test.ts b/tests/unit/services/identity-alias.e2e.test.ts new file mode 100644 index 0000000..60d3432 --- /dev/null +++ b/tests/unit/services/identity-alias.e2e.test.ts @@ -0,0 +1,85 @@ +import { describe, it, beforeEach, afterEach, expect } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { IdentityService } from '../../../src/services/identity'; +import { WebhookServer } from '../../../src/server'; +import { GroupSyncService } from '../../../src/services/group-sync'; + +const ORIGINAL_FETCH = globalThis.fetch; +const envBackup = { ...process.env }; + +describe('Alias @lid ↔ número: aprendizaje y uso en sync de miembros', () => { + let memdb: Database; + + beforeEach(() => { + process.env = { ...envBackup }; + memdb = new Database(':memory:'); + memdb.exec('PRAGMA foreign_keys = ON;'); + initializeDatabase(memdb); + + // Inyectar DB en servicios implicados + (IdentityService as any).dbInstance = memdb; + (WebhookServer as any).dbInstance = memdb; + (GroupSyncService as any).dbInstance = memdb; + + // Limpiar caché de grupos para evitar interferencias + GroupSyncService.activeGroupsCache.clear(); + }); + + afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + process.env = envBackup; + memdb.close(); + }); + + it('aprende alias desde participantAlt y luego resuelve miembros con solo @lid', async () => { + // 1) Simular mensaje de grupo que trae participant (alias @lid) y participantAlt (número real) + process.env.NODE_ENV = 'test'; // evita efectos de red/timers + const payload = { + key: { + remoteJid: '12345-678@g.us', + participant: 'userXYZ@lid', + participantAlt: '666777888@s.whatsapp.net', + fromMe: false + }, + message: { conversation: 'hola' } + }; + + await WebhookServer.handleMessageUpsert(payload); + + // Verificar que el alias fue aprendido + const resolved = IdentityService.resolveAliasOrNull('userXYZ@lid'); + expect(resolved).toBe('666777888'); + + // Añadir un segundo alias manualmente para cubrir varios miembros + IdentityService.upsertAlias('abc@lid', '999000111@s.whatsapp.net', 'test-seed'); + expect(IdentityService.resolveAliasOrNull('abc@lid')).toBe('999000111'); + + // 2) Ahora simular la Evolution API devolviendo solo IDs con @lid en /group/participants + process.env.NODE_ENV = 'development'; // evitar early-return en fetchGroupMembersFromAPI + process.env.EVOLUTION_API_URL = 'http://evolution.test'; + process.env.EVOLUTION_API_INSTANCE = 'instance-1'; + process.env.EVOLUTION_API_KEY = 'apikey'; + + globalThis.fetch = async (url: RequestInfo | URL) => { + if (String(url).includes('/group/participants/')) { + const body = { + participants: [ + { id: 'userXYZ@lid', admin: 'admin' }, // debe resolverse a 666777888 (admin) + { id: 'abc@lid', admin: null } // debe resolverse a 999000111 (no admin) + ] + }; + 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('12345-678@g.us'); + const map = new Map(out.map((x: any) => [x.userId, x.isAdmin])); + + expect(map.has('666777888')).toBe(true); + expect(map.has('999000111')).toBe(true); + expect(map.get('666777888')).toBe(true); // admin resuelto + expect(map.get('999000111')).toBe(false); // no admin + }); +}); diff --git a/tests/unit/services/response-queue.alias-mentions.test.ts b/tests/unit/services/response-queue.alias-mentions.test.ts new file mode 100644 index 0000000..75710f2 --- /dev/null +++ b/tests/unit/services/response-queue.alias-mentions.test.ts @@ -0,0 +1,70 @@ +import { describe, it, beforeEach, afterEach, expect } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { ResponseQueue } from '../../../src/services/response-queue'; +import { IdentityService } from '../../../src/services/identity'; + +const ORIGINAL_FETCH = globalThis.fetch; +const envBackup = { ...process.env }; + +describe('ResponseQueue - resolución de menciones con alias (@lid)', () => { + let memdb: Database; + let captured: { url?: string; payload?: any } = {}; + + beforeEach(() => { + process.env = { + ...envBackup, + EVOLUTION_API_URL: 'http://evolution.test', + EVOLUTION_API_INSTANCE: 'instance-1', + EVOLUTION_API_KEY: 'apikey' + }; + + memdb = new Database(':memory:'); + memdb.exec('PRAGMA foreign_keys = ON;'); + initializeDatabase(memdb); + + (IdentityService as any).dbInstance = memdb; + (ResponseQueue as any).dbInstance = memdb; + + // Sembrar alias: userXYZ@lid -> 666777888 + IdentityService.upsertAlias('userXYZ@lid', '666777888@s.whatsapp.net', 'test'); + + // Stub fetch para capturar el payload enviado + globalThis.fetch = async (url: RequestInfo | URL, init?: RequestInit) => { + captured.url = String(url); + try { + captured.payload = init?.body ? JSON.parse(String(init.body)) : null; + } catch { + captured.payload = null; + } + return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + }; + }); + + afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + process.env = envBackup; + memdb.close(); + }); + + it('construye mentioned con números reales, deduplicados', async () => { + const item = { + id: 1, + recipient: '123456789', // número normalizado + message: 'hola @user', + metadata: JSON.stringify({ mentioned: ['userXYZ@lid', '666777888@s.whatsapp.net'] }), + attempts: 0 + }; + + const res = await ResponseQueue.sendOne(item as any); + expect(res.ok).toBe(true); + + expect(captured.url?.includes('/message/sendText/instance-1')).toBe(true); + expect(captured.payload).toBeDefined(); + expect(captured.payload.number).toBe('123456789'); + expect(Array.isArray(captured.payload.mentioned)).toBe(true); + + // Debe contener solo una entrada, resuelta a número real @s.whatsapp.net y sin duplicados + expect(captured.payload.mentioned.sort()).toEqual(['666777888@s.whatsapp.net']); + }); +});