import { describe, it, beforeEach, afterEach, expect } from 'bun:test'; import Database from 'bun:sqlite'; import { initializeDatabase } from '../../../src/db'; import { TaskService } from '../../../src/tasks/service'; import { RemindersService } from '../../../src/services/reminders'; import { AllowedGroups } from '../../../src/services/allowed-groups'; import { ResponseQueue } from '../../../src/services/response-queue'; function seedGroup(db: Database, groupId: string) { const cols = db.query(`PRAGMA table_info(groups)`).all() as any[]; const values: Record = {}; const nowIso = new Date().toISOString().replace('T', ' ').replace('Z', ''); for (const c of cols) { const name = String(c.name); const type = String(c.type || '').toUpperCase(); const notnull = Number(c.notnull || 0) === 1; const hasDefault = c.dflt_value != null; if (name === 'id') { values[name] = groupId; continue; } if (name === 'name' || name === 'title' || name === 'subject') { values[name] = 'Test Group'; continue; } if (name === 'created_by') { values[name] = 'tester'; continue; } if (name.endsWith('_at')) { values[name] = nowIso; continue; } if (name === 'is_active' || name === 'active') { values[name] = 1; continue; } if (notnull && !hasDefault) { if (type.includes('INT')) values[name] = 1; else if (type.includes('REAL')) values[name] = 0; else values[name] = 'N/A'; } } if (!('id' in values)) values['id'] = groupId; const colsList = Object.keys(values); const placeholders = colsList.map(() => '?').join(', '); const sql = `INSERT OR REPLACE INTO groups (${colsList.join(', ')}) VALUES (${placeholders})`; db.prepare(sql).run(...colsList.map(k => values[k])); } describe('RemindersService - gating por grupos en modo enforce', () => { const envBackup = process.env; let memdb: Database; let originalAdd: any; let sent: any[] = []; beforeEach(() => { process.env = { ...envBackup, NODE_ENV: 'test', GROUP_GATING_MODE: 'enforce', TZ: 'Europe/Madrid' }; memdb = new Database(':memory:'); initializeDatabase(memdb); (TaskService as any).dbInstance = memdb; (RemindersService as any).dbInstance = memdb; (AllowedGroups as any).dbInstance = memdb; // Stub de ResponseQueue originalAdd = (ResponseQueue as any).add; (ResponseQueue as any).add = async (msgs: any[]) => { sent.push(...msgs); }; sent = []; // Asegurar usuario receptor para satisfacer la FK de user_preferences const iso = new Date().toISOString().replace('T', ' ').replace('Z', ''); memdb.exec(` INSERT INTO users (id, first_seen, last_seen) VALUES ('34600123456', '${iso}', '${iso}') ON CONFLICT(id) DO NOTHING `); // Preferencias del usuario receptor memdb.exec(` INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) VALUES ('34600123456', 'daily', '00:00', NULL, strftime('%Y-%m-%d %H:%M:%f','now')) ON CONFLICT(user_id) DO UPDATE SET reminder_freq = excluded.reminder_freq, reminder_time = excluded.reminder_time, last_reminded_on = NULL, updated_at = excluded.updated_at `); // Sembrar grupos y estados seedGroup(memdb, 'ok@g.us'); seedGroup(memdb, 'na@g.us'); AllowedGroups.setStatus('ok@g.us', 'allowed', 'OK'); AllowedGroups.setStatus('na@g.us', 'allowed', 'NA'); // inicialmente allowed para que las tareas se creen con group_id // Crear dos tareas, una en cada grupo, asignadas al usuario TaskService.createTask( { description: 'Tarea OK', created_by: '34600123456', group_id: 'ok@g.us', due_date: null }, [{ user_id: '34600123456', assigned_by: '34600123456' }] ); TaskService.createTask( { description: 'Tarea NA', created_by: '34600123456', group_id: 'na@g.us', due_date: null }, [{ user_id: '34600123456', assigned_by: '34600123456' }] ); // Cambiar a bloqueado uno de los grupos antes de correr los recordatorios AllowedGroups.setStatus('na@g.us', 'blocked', 'NA'); }); afterEach(() => { (ResponseQueue as any).add = originalAdd; memdb.close(); process.env = envBackup; }); it('omite tareas de grupos no allowed en los recordatorios', async () => { const now = new Date('2025-09-08T07:40:00.000Z'); // ≥ 08:30 Europe/Madrid en un lunes await RemindersService.runOnce(now); expect(sent.length).toBe(1); const msg = String(sent[0].message); // Debe incluir solo la tarea del grupo allowed y omitir la del bloqueado expect(msg).toContain('Tarea OK'); expect(msg).not.toContain('Tarea NA'); }); });