import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import Database from 'bun:sqlite'; import { initializeDatabase, ensureUserExists } from '../../../src/db'; import { GroupSyncService } from '../../../src/services/group-sync'; import { ResponseQueue } from '../../../src/services/response-queue'; const envBackup = { ...process.env }; let originalSyncMembers: any; let originalSyncGroups: any; function sleep(ms: number) { return new Promise(res => setTimeout(res, ms)); } describe('GroupSyncService - scheduler de miembros', () => { beforeEach(() => { originalSyncMembers = GroupSyncService.syncMembersForActiveGroups; originalSyncGroups = GroupSyncService.syncGroups; }); afterEach(() => { GroupSyncService.stopMembersScheduler(); GroupSyncService.stopGroupsScheduler(); GroupSyncService.syncMembersForActiveGroups = originalSyncMembers; GroupSyncService.syncGroups = originalSyncGroups; 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); }); test('groups scheduler no arranca en entorno de test', async () => { process.env = { ...envBackup, NODE_ENV: 'test' }; let called = 0; GroupSyncService.syncGroups = async () => { called++; return { added: 0, updated: 0 }; }; GroupSyncService.startGroupsScheduler(); await sleep(100); expect(called).toBe(0); expect(GroupSyncService.getSecondsUntilNextGroupSync()).toBeNull(); }); test('groups scheduler arranca en producción y programa next tick', async () => { process.env = { ...envBackup, NODE_ENV: 'production', GROUP_SYNC_INTERVAL_MS: '30' }; let called = 0; GroupSyncService.syncGroups = async () => { called++; return { added: 0, updated: 0 }; }; GroupSyncService.startGroupsScheduler(); const secs1 = GroupSyncService.getSecondsUntilNextGroupSync(); expect(secs1).not.toBeNull(); expect(Number(secs1)).toBeGreaterThan(0); await sleep(120); GroupSyncService.stopGroupsScheduler(); expect(called).toBeGreaterThanOrEqual(1); }); test('al desactivarse un grupo en sync: revoca tokens, desactiva membresía y notifica admins', async () => { process.env = { ...envBackup, NODE_ENV: 'development', ADMIN_USERS: '34600123456' }; const memdb = new Database(':memory:'); initializeDatabase(memdb); (GroupSyncService as any).dbInstance = memdb; (ResponseQueue as any).dbInstance = memdb; // Sembrar grupo activo con miembro y token de calendario memdb.exec(`INSERT INTO groups (id, community_id, name, active, archived, last_verified) VALUES ('g1@g.us','comm-1','G1',1,0,strftime('%Y-%m-%d %H:%M:%f','now'))`); const uid = ensureUserExists('34600123456', memdb)!; memdb.exec(`INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES ('g1@g.us','${uid}',0,1,strftime('%Y-%m-%d %H:%M:%f','now'),strftime('%Y-%m-%d %H:%M:%f','now'))`); memdb.exec(`INSERT INTO calendar_tokens (type, user_id, group_id, token_hash, created_at) VALUES ('group','${uid}','g1@g.us','h1',strftime('%Y-%m-%d %H:%M:%f','now'))`); // Stub: API devuelve 0 grupos → el existente pasa a inactivo const originalFetch = (GroupSyncService as any).fetchGroupsFromAPI; (GroupSyncService as any).fetchGroupsFromAPI = async () => []; try { await GroupSyncService.syncGroups(true); } finally { // Restaurar stub (GroupSyncService as any).fetchGroupsFromAPI = originalFetch; } // Tokens revocados const tok = memdb.query(`SELECT revoked_at FROM calendar_tokens WHERE group_id='g1@g.us'`).get() as any; expect(tok && tok.revoked_at).toBeTruthy(); // Membresías desactivadas const mem = memdb.query(`SELECT is_active FROM group_members WHERE group_id='g1@g.us'`).get() as any; expect(Number(mem?.is_active || 0)).toBe(0); // Notificación encolada a admins const msg = memdb.query(`SELECT message FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any; expect(msg && String(msg.message)).toContain('/admin archivar-grupo g1@g.us'); }); });