import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { Database } from 'bun:sqlite'; import { initializeDatabase, ensureUserExists } from '../../../src/db'; import { TaskService } from '../../../src/tasks/service'; let memDb: Database; beforeEach(() => { memDb = new Database(':memory:'); initializeDatabase(memDb); TaskService.dbInstance = memDb; }); afterEach(() => { try { memDb.close(); } catch {} }); describe('TaskService.listGroupUnassigned / countGroupUnassigned', () => { it('devuelve solo tareas sin dueño del grupo, en orden por fecha (NULL al final) y respeta el límite', () => { // Sembrar grupos para FK memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`) .run('g1@g.us', 'c1', 'G1'); memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`) .run('g2@g.us', 'c1', 'G2'); // En g1: 3 sin dueño con distintas fechas, 1 asignada (excluir), 1 sin dueño completada (excluir) const tEarly = TaskService.createTask({ description: 'Sin dueño pronto', due_date: '2025-08-01', group_id: 'g1@g.us', created_by: '1000000000', }); const tLater = TaskService.createTask({ description: 'Sin dueño tarde', due_date: '2025-10-01', group_id: 'g1@g.us', created_by: '1000000000', }); const tNull = TaskService.createTask({ description: 'Sin dueño sin fecha', due_date: null, group_id: 'g1@g.us', created_by: '1000000000', }); TaskService.createTask({ description: 'Asignada (no debe salir)', due_date: '2025-09-01', group_id: 'g1@g.us', created_by: '1000000000', }, [{ user_id: '2000000000', assigned_by: '1000000000' }]); // Completar una sin dueño para que no aparezca memDb.prepare(`UPDATE tasks SET completed = 1, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(tEarly); // En g2: sin dueño (no deben aparecer al listar g1) for (let i = 1; i <= 3; i++) { TaskService.createTask({ description: `G2 ${i}`, due_date: '2025-12-31', group_id: 'g2@g.us', created_by: '1000000000', }); } const list = TaskService.listGroupUnassigned('g1@g.us', 10); expect(Array.isArray(list)).toBe(true); // Deben quedar 2: 'Sin dueño tarde' (fecha) y 'Sin dueño sin fecha' (NULL al final) expect(list.length).toBe(2); expect(list[0].description).toBe('Sin dueño tarde'); expect(list[1].description).toBe('Sin dueño sin fecha'); expect(list[0].assignees).toEqual([]); expect(list[1].assignees).toEqual([]); expect(list[0].group_id).toBe('g1@g.us'); expect(list[1].group_id).toBe('g1@g.us'); // Límite // Crear más en g1 para forzar límite 1 TaskService.createTask({ description: 'Otra sin dueño', due_date: '2025-11-01', group_id: 'g1@g.us', created_by: '1000000000', }); const limited = TaskService.listGroupUnassigned('g1@g.us', 1); expect(limited.length).toBe(1); }); it('countGroupUnassigned cuenta solo sin dueño pendientes (excluye completadas y asignadas)', () => { // Limpiar por si acaso memDb.exec('DELETE FROM tasks'); memDb.exec('DELETE FROM task_assignments'); // Sembrar grupo memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`) .run('g1@g.us', 'c1', 'G1'); const a = TaskService.createTask({ description: 'Sin dueño 1', due_date: '2025-09-10', group_id: 'g1@g.us', created_by: '3333333333', }); const b = TaskService.createTask({ description: 'Sin dueño 2', due_date: null, group_id: 'g1@g.us', created_by: '3333333333', }); // Asignada (no cuenta como sin dueño) TaskService.createTask({ description: 'Asignada', due_date: '2025-09-12', group_id: 'g1@g.us', created_by: '3333333333', }, [{ user_id: '4444444444', assigned_by: '3333333333' }]); // Completar una sin dueño memDb.prepare(`UPDATE tasks SET completed = 1, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(a); const cnt = TaskService.countGroupUnassigned('g1@g.us'); expect(cnt).toBe(1); // sólo queda b }); }); describe('TaskService.createTask', () => { it('crea una tarea mínima con created_by y sin due_date ni group_id', () => { const creatorRaw = '555111222@s.whatsapp.net'; const createdBy = ensureUserExists(creatorRaw, memDb); expect(createdBy).toBeTruthy(); const id = TaskService.createTask( { description: 'Comprar agua', created_by: createdBy!, due_date: null, group_id: null, }, [] // sin asignaciones ); expect(typeof id).toBe('number'); const task = memDb .prepare( `SELECT id, description, due_date, group_id, created_by, completed FROM tasks WHERE id = ?` ) .get(id) as any; expect(task).toBeTruthy(); expect(task.description).toBe('Comprar agua'); expect(task.due_date).toBeNull(); expect(task.group_id).toBeNull(); expect(task.created_by).toBe(createdBy); expect(task.completed).toBe(0); // BOOLEAN en SQLite como 0/1 }); it('guarda due_date como cadena YYYY-MM-DD y group_id como JID completo', () => { const creatorRaw = '555333444@s.whatsapp.net'; const createdBy = ensureUserExists(creatorRaw, memDb)!; const due = '2025-09-10'; const groupId = '12345@g.us'; // Sembrar el grupo para satisfacer la FK de tasks.group_id -> groups.id memDb .prepare(`INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, TRUE)`) .run(groupId, 'test-community', 'Grupo de prueba'); const id = TaskService.createTask( { description: 'Pagar servicios', created_by: createdBy, due_date: due, group_id: groupId, }, [] ); const row = memDb .prepare( `SELECT description, due_date, group_id, created_by FROM tasks WHERE id = ?` ) .get(id) as any; expect(row.due_date).toBe(due); expect(row.group_id).toBe(groupId); expect(row.created_by).toBe(createdBy); }); it('inserta asignaciones y evita duplicados por usuario', () => { const creator = ensureUserExists('555000001@s.whatsapp.net', memDb)!; const assigneeA = ensureUserExists('555000002@s.whatsapp.net', memDb)!; const assigneeB = ensureUserExists('555000003@s.whatsapp.net', memDb)!; const id = TaskService.createTask( { description: 'Organizar reunión', created_by: creator, due_date: null, group_id: null, }, [ { user_id: assigneeA, assigned_by: creator }, { user_id: assigneeA, assigned_by: creator }, // duplicado { user_id: assigneeB, assigned_by: creator }, ] ); const count = memDb .prepare( `SELECT COUNT(*) AS c FROM task_assignments WHERE task_id = ?` ) .get(id) as any; expect(count.c).toBe(2); const users = memDb .prepare( `SELECT user_id FROM task_assignments WHERE task_id = ? ORDER BY user_id` ) .all(id) as any[]; expect(users.map(u => u.user_id)).toEqual([assigneeA, assigneeB].sort()); }); it('asegura usuarios inexistentes en asignaciones y no viola FK', () => { const creator = ensureUserExists('555010101@s.whatsapp.net', memDb)!; const id = TaskService.createTask( { description: 'Tarea con asignado nuevo', created_by: creator, due_date: null, group_id: null, }, [ // Usuario no existente: TaskService debe asegurar y crear { user_id: '555099999', assigned_by: creator }, ] ); const counts = memDb .prepare( `SELECT (SELECT COUNT(*) FROM tasks) AS tasks_count, (SELECT COUNT(*) FROM task_assignments WHERE task_id = ?) AS assigns_count` ) .get(id) as any; expect(counts.tasks_count).toBe(1); expect(counts.assigns_count).toBe(1); }); it('lanza error si created_by es inválido (no normalizable) y no inserta la tarea', () => { const invalidCreator = 'invalid-id!'; expect(() => TaskService.createTask( { description: 'No debería insertarse', created_by: invalidCreator, due_date: null, group_id: null, }, [] ) ).toThrow(); const count = memDb .prepare(`SELECT COUNT(*) AS c FROM tasks`) .get() as any; expect(count.c).toBe(0); }); });