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