You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			231 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			231 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
| import { describe, it, beforeEach, 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 { 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';
 | |
|     process.env.REMINDERS_GRACE_MINUTES = '60';
 | |
| 
 | |
|     memdb = new Database(':memory:');
 | |
|     initializeDatabase(memdb);
 | |
| 
 | |
|     // 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' | 'weekdays' | '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:30:00.000Z'); // dentro de ventana (≤ 60 min)
 | |
| 
 | |
|     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 (fuera de ventana, pero ya enviado hoy → no duplica)
 | |
|     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:30:00.000Z'); // dentro de ventana
 | |
| 
 | |
|     // 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);
 | |
|   });
 | |
| 
 | |
|   it('weekdays: envía en martes a la hora configurada', async () => {
 | |
|     insertPref('weekdays', '08:00', null);
 | |
|     // Martes 2025-09-09 08:05 Europe/Madrid ≈ 06:05Z
 | |
|     const now = new Date('2025-09-09T06:05:00.000Z');
 | |
| 
 | |
|     // Crear 1 tarea asignada al usuario
 | |
|     TaskService.createTask(
 | |
|       { description: 'Tarea LV', 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-09');
 | |
|   });
 | |
| 
 | |
|   it('weekdays: no envía en sábado', async () => {
 | |
|     insertPref('weekdays', '08:00', null);
 | |
|     // Sábado 2025-09-13 08:05 Europe/Madrid ≈ 06:05Z
 | |
|     const now = new Date('2025-09-13T06:05:00.000Z');
 | |
| 
 | |
|     TaskService.createTask(
 | |
|       { description: 'Tarea LV2', due_date: '2025-09-14', 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 fuera de la ventana de gracia tras reinicio tardío', async () => {
 | |
|     insertPref('daily', '08:30', null);
 | |
|     // 08:30 local + 90 min ≈ 10:00 local => 08:00Z en CEST
 | |
|     const now = new Date('2025-09-08T08:00:00.000Z');
 | |
| 
 | |
|     // Crear una tarea asignada después de la hora configurada
 | |
|     TaskService.createTask(
 | |
|       { description: 'Tarea fuera de ventana', due_date: '2025-09-20', group_id: null, created_by: USER },
 | |
|       [{ user_id: USER, assigned_by: USER }]
 | |
|     );
 | |
| 
 | |
|     await RemindersService.runOnce(now);
 | |
|     expect(countQueued()).toBe(0);
 | |
|     expect(getLastReminded()).toBeNull();
 | |
|   });
 | |
| 
 | |
|   it('envía dentro de la ventana de gracia si hay tareas', async () => {
 | |
|     insertPref('daily', '08:30', null);
 | |
|     // 08:30 local + 45 min ≈ 09:15 local => 07:15Z en CEST
 | |
|     const now = new Date('2025-09-08T07:15:00.000Z');
 | |
| 
 | |
|     TaskService.createTask(
 | |
|       { description: 'Tarea dentro de ventana', 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);
 | |
|     expect(getLastReminded()).toBe('2025-09-08');
 | |
|   });
 | |
| });
 |