import { describe, it, beforeEach, expect } from 'bun:test'; import { Database } from 'bun:sqlite'; import { initializeDatabase } from '../../../src/db'; import { Migrator } from '../../../src/db/migrator'; import { TaskService } from '../../../src/tasks/service'; import { RemindersService } from '../../../src/services/reminders'; import { ResponseQueue } from '../../../src/services/response-queue'; function toIso(dt: Date): string { return dt.toISOString().replace('T', ' ').replace('Z', ''); } describe('RemindersService', () => { let memdb: Database; const USER = '34600123456'; beforeEach(() => { process.env.NODE_ENV = 'test'; process.env.TZ = 'Europe/Madrid'; memdb = new Database(':memory:'); initializeDatabase(memdb); // Migraciones para user_preferences return Migrator.migrateToLatest(memdb).then(() => { // Inyectar DB en servicios (TaskService as any).dbInstance = memdb; (RemindersService as any).dbInstance = memdb; (ResponseQueue as any).dbInstance = memdb; // Limpiar tablas entre tests por seguridad memdb.exec(`DELETE FROM response_queue;`); memdb.exec(`DELETE FROM user_preferences;`); memdb.exec(`DELETE FROM users;`); memdb.exec(`DELETE FROM tasks;`); memdb.exec(`DELETE FROM task_assignments;`); // Asegurar usuario memdb.exec(` INSERT INTO users (id, first_seen, last_seen) VALUES ('${USER}', '${toIso(new Date())}', '${toIso(new Date())}') ON CONFLICT(id) DO NOTHING; `); }); }); function insertPref(freq: 'daily' | 'weekly' | 'off', time: string = '08:30', last: string | null = null) { memdb.prepare(` INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) VALUES (?, ?, ?, ?, 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 = excluded.last_reminded_on, updated_at = excluded.updated_at `).run(USER, freq, time, last); } function countQueued(): number { const row = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; return Number(row?.c || 0); } function getLastReminded(): string | null { const row = memdb.prepare(`SELECT last_reminded_on AS d FROM user_preferences WHERE user_id = ?`).get(USER) as any; return row?.d ?? null; } it('envía recordatorio diario cuando hora alcanzada y no enviado hoy', async () => { insertPref('daily', '08:30', null); // Lunes 2025-09-08 09:30 Europe/Madrid ≈ 07:30Z const now = new Date('2025-09-08T07:30:00.000Z'); // Crear 1 tarea asignada al usuario TaskService.createTask( { description: 'Tarea A', due_date: '2025-09-10', group_id: null, created_by: USER }, [{ user_id: USER, assigned_by: USER }] ); await RemindersService.runOnce(now); expect(countQueued()).toBe(1); expect(getLastReminded()).toBe('2025-09-08'); // YYYY-MM-DD en TZ }); it('no duplica recordatorio diario en el mismo día', async () => { insertPref('daily', '08:30', null); const now = new Date('2025-09-08T07:40:00.000Z'); // ≥ 08:30 local TaskService.createTask( { description: 'Tarea B', due_date: '2025-09-11', group_id: null, created_by: USER }, [{ user_id: USER, assigned_by: USER }] ); await RemindersService.runOnce(now); expect(countQueued()).toBe(1); // Segunda ejecución el mismo día await RemindersService.runOnce(new Date('2025-09-08T08:00:00.000Z')); expect(countQueued()).toBe(1); // sin incremento }); it('no envía diario antes de la hora configurada', async () => { insertPref('daily', '08:30', null); const now = new Date('2025-09-08T05:00:00.000Z'); // 07:00 local < 08:30 TaskService.createTask( { description: 'Tarea C', due_date: '2025-09-12', group_id: null, created_by: USER }, [{ user_id: USER, assigned_by: USER }] ); await RemindersService.runOnce(now); expect(countQueued()).toBe(0); expect(getLastReminded()).toBeNull(); }); it('no envía si el usuario no tiene tareas; no marca last_reminded_on', async () => { insertPref('daily', '08:30', null); const now = new Date('2025-09-08T07:40:00.000Z'); // ≥ 08:30 local await RemindersService.runOnce(now); expect(countQueued()).toBe(0); expect(getLastReminded()).toBeNull(); }); it('weekly: solo envía en lunes y a la hora, no en martes', async () => { insertPref('weekly', '08:30', null); // Crear 1 tarea TaskService.createTask( { description: 'Tarea W', due_date: '2025-09-15', group_id: null, created_by: USER }, [{ user_id: USER, assigned_by: USER }] ); // Lunes (Mon) 2025-09-08 07:00 local (05:00Z) -> antes de hora: no envía await RemindersService.runOnce(new Date('2025-09-08T05:00:00.000Z')); expect(countQueued()).toBe(0); expect(getLastReminded()).toBeNull(); // Lunes 09:30 local (07:30Z) -> envía await RemindersService.runOnce(new Date('2025-09-08T07:30:00.000Z')); expect(countQueued()).toBe(1); expect(getLastReminded()).toBe('2025-09-08'); // Martes 2025-09-09 ≥ hora -> no debe enviar (weekly solo lunes) await RemindersService.runOnce(new Date('2025-09-09T07:40:00.000Z')); expect(countQueued()).toBe(1); // sin incremento // last_reminded_on permanece en lunes expect(getLastReminded()).toBe('2025-09-08'); }); it('incluye “… y X más” cuando hay más de 10 tareas (tope de listado)', async () => { insertPref('daily', '08:30', null); const now = new Date('2025-09-08T07:40:00.000Z'); // Crear 12 tareas asignadas al usuario for (let i = 0; i < 12; i++) { TaskService.createTask( { description: `Tarea ${i + 1}`, due_date: '2025-09-20', group_id: null, created_by: USER }, [{ user_id: USER, assigned_by: USER }] ); } await RemindersService.runOnce(now); expect(countQueued()).toBe(1); const row = memdb.prepare(`SELECT message FROM response_queue LIMIT 1`).get() as any; const msg: string = String(row?.message || ''); expect(msg.includes('… y 2 más')).toBe(true); }); });