test: agregar pruebas unitarias para RemindersService y CommandService
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>pull/1/head
							parent
							
								
									5c49f16c4e
								
							
						
					
					
						commit
						0f96c27928
					
				| @ -0,0 +1,93 @@ | ||||
| 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 { CommandService } from '../../../src/services/command'; | ||||
| 
 | ||||
| describe('CommandService - configurar recordatorios', () => { | ||||
|   let memdb: Database; | ||||
|   const SENDER = '34600123456'; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     process.env.NODE_ENV = 'test'; | ||||
|     process.env.TZ = 'Europe/Madrid'; | ||||
| 
 | ||||
|     memdb = new Database(':memory:'); | ||||
|     initializeDatabase(memdb); | ||||
|     await Migrator.migrateToLatest(memdb); | ||||
| 
 | ||||
|     // Inyectar DB
 | ||||
|     (CommandService as any).dbInstance = memdb; | ||||
| 
 | ||||
|     // Limpiar tablas
 | ||||
|     memdb.exec(`DELETE FROM response_queue;`); | ||||
|     memdb.exec(`DELETE FROM user_preferences;`); | ||||
|     memdb.exec(`DELETE FROM users;`); | ||||
|   }); | ||||
| 
 | ||||
|   async function runCmd(msg: string) { | ||||
|     return await CommandService.handle({ | ||||
|       sender: SENDER, | ||||
|       groupId: '123@g.us', | ||||
|       message: msg, | ||||
|       mentions: [] | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function getPref(): { freq: string; time: string | null } | null { | ||||
|     const row = memdb.prepare(`SELECT reminder_freq AS freq, reminder_time AS time FROM user_preferences WHERE user_id = ?`).get(SENDER) as any; | ||||
|     if (!row) return null; | ||||
|     return { freq: String(row.freq), time: row.time ? String(row.time) : null }; | ||||
|   } | ||||
| 
 | ||||
|   it('configurar daily guarda preferencia y responde confirmación', async () => { | ||||
|     const res = await runCmd('/t configurar daily'); | ||||
|     expect(res).toHaveLength(1); | ||||
|     expect(res[0].recipient).toBe(SENDER); | ||||
|     expect(res[0].message).toContain('✅ Recordatorios: diario'); | ||||
| 
 | ||||
|     const pref = getPref(); | ||||
|     expect(pref).not.toBeNull(); | ||||
|     expect(pref!.freq).toBe('daily'); | ||||
|     expect(pref!.time).toBe('08:30'); // default
 | ||||
|   }); | ||||
| 
 | ||||
|   it('configurar weekly guarda preferencia y responde confirmación', async () => { | ||||
|     const res = await runCmd('/t configurar weekly'); | ||||
|     expect(res).toHaveLength(1); | ||||
|     expect(res[0].message).toContain('semanal (lunes 08:30)'); | ||||
| 
 | ||||
|     const pref = getPref(); | ||||
|     expect(pref).not.toBeNull(); | ||||
|     expect(pref!.freq).toBe('weekly'); | ||||
|   }); | ||||
| 
 | ||||
|   it('configurar off guarda preferencia y responde confirmación', async () => { | ||||
|     const res = await runCmd('/t configurar off'); | ||||
|     expect(res).toHaveLength(1); | ||||
|     expect(res[0].message).toContain('apagado'); | ||||
| 
 | ||||
|     const pref = getPref(); | ||||
|     expect(pref).not.toBeNull(); | ||||
|     expect(pref!.freq).toBe('off'); | ||||
|   }); | ||||
| 
 | ||||
|   it('configurar con opción inválida devuelve uso correcto y no escribe en DB', async () => { | ||||
|     const res = await runCmd('/t configurar foo'); | ||||
|     expect(res).toHaveLength(1); | ||||
|     expect(res[0].message).toContain('Uso: /t configurar daily|weekly|off'); | ||||
| 
 | ||||
|     const pref = getPref(); | ||||
|     expect(pref).toBeNull(); | ||||
|   }); | ||||
| 
 | ||||
|   it('upsert idempotente: cambiar de daily a off actualiza la fila existente', async () => { | ||||
|     await runCmd('/t configurar daily'); | ||||
|     let pref = getPref(); | ||||
|     expect(pref!.freq).toBe('daily'); | ||||
| 
 | ||||
|     await runCmd('/t configurar off'); | ||||
|     pref = getPref(); | ||||
|     expect(pref!.freq).toBe('off'); | ||||
|   }); | ||||
| }); | ||||
| @ -0,0 +1,170 @@ | ||||
| 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 09:00 local (07:00Z) -> antes de hora: no envía
 | ||||
|     await RemindersService.runOnce(new Date('2025-09-08T07: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); | ||||
|   }); | ||||
| }); | ||||
					Loading…
					
					
				
		Reference in New Issue