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

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');
});
});