diff --git a/tests/unit/server.test.ts b/tests/unit/server.test.ts index 8d91bf7..c87a450 100644 --- a/tests/unit/server.test.ts +++ b/tests/unit/server.test.ts @@ -4,6 +4,7 @@ import { WebhookServer } from '../../src/server'; import { ResponseQueue } from '../../src/services/response-queue'; import { GroupSyncService } from '../../src/services/group-sync'; import { initializeDatabase, ensureUserExists } from '../../src/db'; +import { TaskService } from '../../src/tasks/service'; // Simulated ResponseQueue for testing (in-memory array) let simulatedQueue: any[] = []; @@ -53,6 +54,9 @@ beforeEach(() => { // Inject testDb for GroupSyncService to use GroupSyncService.dbInstance = testDb; + + // Inject testDb for TaskService to use + (TaskService as any).dbInstance = testDb; // Ensure database is initialized (recreates tables if dropped) initializeDatabase(testDb); @@ -783,4 +787,153 @@ describe('WebhookServer', () => { } }); }); + + describe('Advanced listings via WebhookServer', () => { + test('should process "/t ver sin" in group as DM-only with pagination line', async () => { + // 12 sin dueño en el grupo activo + for (let i = 1; i <= 12; i++) { + TaskService.createTask({ + description: `Sin dueño ${i}`, + due_date: '2025-12-31', + group_id: 'group-id@g.us', + created_by: '9999999999', + }); + } + // 2 asignadas (no deben aparecer en "sin") + TaskService.createTask({ + description: 'Asignada 1', + due_date: '2025-10-10', + group_id: 'group-id@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + TaskService.createTask({ + description: 'Asignada 2', + due_date: '2025-10-11', + group_id: 'group-id@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: '1234567890@s.whatsapp.net' + }, + message: { conversation: '/t ver sin' } + } + }; + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.getQueue(); + expect(out.length).toBeGreaterThan(0); + for (const r of out) { + expect(r.recipient.endsWith('@g.us')).toBe(false); + } + const msg = out.map(x => x.message).join('\n'); + expect(msg).toContain('sin dueño'); + expect(msg).toContain('… y 2 más'); + }); + + test('should process "/t ver sin" in DM returning instruction', async () => { + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: '1234567890@s.whatsapp.net', // DM + participant: '1234567890@s.whatsapp.net' + }, + message: { conversation: '/t ver sin' } + } + }; + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.getQueue(); + expect(out.length).toBeGreaterThan(0); + const msg = out.map(x => x.message).join('\n'); + expect(msg).toContain('Este comando se usa en grupos'); + }); + + test('should process "/t ver todos" in group showing "Tus tareas" + "Sin dueño (grupo actual)" with pagination in unassigned section', async () => { + // Tus tareas (2 asignadas) + TaskService.createTask({ + description: 'Mi Tarea 1', + due_date: '2025-10-10', + group_id: 'group-id@g.us', + created_by: '2222222222', + }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); + + TaskService.createTask({ + description: 'Mi Tarea 2', + due_date: '2025-10-11', + group_id: 'group-id@g.us', + created_by: '2222222222', + }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); + + // 12 sin dueño para provocar paginación + for (let i = 1; i <= 12; i++) { + TaskService.createTask({ + description: `Sin dueño ${i}`, + due_date: '2025-12-31', + group_id: 'group-id@g.us', + created_by: '9999999999', + }); + } + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: '1234567890@s.whatsapp.net' + }, + message: { conversation: '/t ver todos' } + } + }; + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.getQueue(); + expect(out.length).toBeGreaterThan(0); + const msg = out.map(x => x.message).join('\n'); + expect(msg).toContain('Tus tareas'); + expect(msg).toContain('sin dueño'); + expect(msg).toContain('… y 2 más'); + }); + + test('should process "/t ver todos" in DM showing "Tus tareas" + instructive note', async () => { + TaskService.createTask({ + description: 'Mi Tarea A', + due_date: '2025-11-20', + group_id: 'group-2@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: '1234567890@s.whatsapp.net', // DM + participant: '1234567890@s.whatsapp.net' + }, + message: { conversation: '/t ver todos' } + } + }; + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.getQueue(); + expect(out.length).toBeGreaterThan(0); + const msg = out.map(x => x.message).join('\n'); + expect(msg).toContain('Tus tareas'); + expect(msg).toContain('ℹ️ Para ver tareas sin dueño'); + }); + }); }); diff --git a/tests/unit/services/command.test.ts b/tests/unit/services/command.test.ts index 6b74a45..e8714ac 100644 --- a/tests/unit/services/command.test.ts +++ b/tests/unit/services/command.test.ts @@ -145,6 +145,149 @@ test('completar tarea: camino feliz, ya completada y no encontrada', async () => expect(responses[0].message).toContain('no encontrada'); }); +test('ver sin en grupo activo: solo sin dueño y paginación', async () => { + // Insertar grupo y cachearlo como activo + memDb.exec(` + INSERT OR IGNORE INTO groups (id, community_id, name, active) + VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) + `); + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); + + // 12 tareas sin dueño (para provocar “… y 2 más” con límite 10) + for (let i = 1; i <= 12; i++) { + TaskService.createTask({ + description: `Unassigned ${i}`, + due_date: '2025-12-31', + group_id: 'test-group@g.us', + created_by: '9999999999', + }); + } + + // 2 tareas asignadas (no deben aparecer en "ver sin") + TaskService.createTask({ + description: 'Asignada 1', + due_date: '2025-11-01', + group_id: 'test-group@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + TaskService.createTask({ + description: 'Asignada 2', + due_date: '2025-11-02', + group_id: 'test-group@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + const responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: '/t ver sin' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + const msg = responses[0].message; + expect(msg).toContain('Test Group'); + expect(msg).toContain('sin dueño'); + expect(msg).toContain('… y 2 más'); + expect(msg).not.toContain('Asignada 1'); + expect(msg).not.toContain('Asignada 2'); +}); + +test('ver sin por DM devuelve instrucción', async () => { + const responses = await CommandService.handle({ + sender: '1234567890', + // DM: no es un JID de grupo + groupId: '1234567890@s.whatsapp.net', + mentions: [], + message: '/t ver sin' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + expect(responses[0].message).toContain('Este comando se usa en grupos'); +}); + +test('ver todos en grupo: “Tus tareas” + “Sin dueño (grupo actual)” con paginación en la sección sin dueño', async () => { + // Insertar grupo y cachearlo como activo + memDb.exec(` + INSERT OR IGNORE INTO groups (id, community_id, name, active) + VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) + `); + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); + + // Tus tareas (2 asignadas al usuario) + TaskService.createTask({ + description: 'Mi Tarea 1', + due_date: '2025-10-10', + group_id: 'test-group@g.us', + created_by: '2222222222', + }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); + + TaskService.createTask({ + description: 'Mi Tarea 2', + due_date: '2025-10-11', + group_id: 'test-group@g.us', + created_by: '2222222222', + }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); + + // 12 sin dueño en el grupo (provoca “… y 2 más” en esa sección) + for (let i = 1; i <= 12; i++) { + TaskService.createTask({ + description: `Sin dueño ${i}`, + due_date: '2025-12-31', + group_id: 'test-group@g.us', + created_by: '9999999999', + }); + } + + const responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: '/t ver todos' + }); + + expect(responses.length).toBe(1); + const msg = responses[0].message; + expect(msg).toContain('Tus tareas'); + expect(msg).toContain('Test Group'); + expect(msg).toContain('sin dueño'); + expect(msg).toContain('… y 2 más'); // paginación en la sección “sin dueño” +}); + +test('ver todos por DM: “Tus tareas” + nota instructiva para ver sin dueño desde el grupo', async () => { + // 2 tareas asignadas al usuario en cualquier grupo (no importa para este test) + TaskService.createTask({ + description: 'Mi Tarea A', + due_date: '2025-11-20', + group_id: 'group-1@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + TaskService.createTask({ + description: 'Mi Tarea B', + due_date: '2025-11-21', + group_id: 'group-2@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + const responses = await CommandService.handle({ + sender: '1234567890', + groupId: '1234567890@s.whatsapp.net', // DM + mentions: [], + message: '/t ver todos' + }); + + expect(responses.length).toBe(1); + const msg = responses[0].message; + expect(msg).toContain('Tus tareas'); + expect(msg).toContain('ℹ️ Para ver tareas sin dueño'); +}); + afterEach(() => { try { memDb.close(); } catch {} }); diff --git a/tests/unit/tasks/service.test.ts b/tests/unit/tasks/service.test.ts index c4b334c..d8f17c5 100644 --- a/tests/unit/tasks/service.test.ts +++ b/tests/unit/tasks/service.test.ts @@ -17,6 +17,113 @@ afterEach(() => { } 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', 1); + memDb.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, ?, ?, 1)`) + .run('g2@g.us', 'c1', 'G2', 1); + + // 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', 1); + + 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';