import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test'; import { Database } from 'bun:sqlite'; 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[] = []; let originalAdd: any; class SimulatedResponseQueue { static async add(responses: any[]) { simulatedQueue.push(...responses); } static getQueue() { return simulatedQueue; } static clear() { simulatedQueue = []; } } // Test database instance let testDb: Database; beforeAll(() => { // Create in-memory test database testDb = new Database(':memory:'); // Initialize schema initializeDatabase(testDb); // Guardar implementación original de ResponseQueue.add para restaurar después originalAdd = (ResponseQueue as any).add; }); afterAll(() => { (ResponseQueue as any).add = originalAdd; // Close the test database testDb.close(); }); beforeEach(() => { // Clear simulated queue SimulatedResponseQueue.clear(); // Replace ResponseQueue with simulated version (ResponseQueue as any).add = SimulatedResponseQueue.add; // Inject testDb for WebhookServer to use WebhookServer.dbInstance = testDb; // 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); // Reset database state between tests (borrar raíz primero; ON DELETE CASCADE limpia assignments) testDb.exec('DELETE FROM response_queue'); try { testDb.exec('DELETE FROM task_origins'); } catch {} testDb.exec('DELETE FROM tasks'); testDb.exec('DELETE FROM users'); testDb.exec('DELETE FROM groups'); // Insert test data for active group testDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('group-id@g.us', 'test-community', 'Test Group', 1) `); // Populate active groups cache with test data GroupSyncService['cacheActiveGroups'](); }); describe('WebhookServer', () => { const envBackup = process.env; beforeEach(() => { process.env = { ...envBackup, INSTANCE_NAME: 'test-instance', NODE_ENV: 'test' }; }); afterEach(() => { process.env = envBackup; (ResponseQueue as any).add = originalAdd; }); const createTestRequest = (payload: any) => new Request('http://localhost:3007', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); test('should reject non-POST requests', async () => { const request = new Request('http://localhost:3007', { method: 'GET' }); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(405); }); test('should require JSON content type', async () => { const request = new Request('http://localhost:3007', { method: 'POST', headers: { 'Content-Type': 'text/plain' } }); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(400); }); test('should validate payload structure', async () => { const invalidPayloads = [ {}, { event: null }, { event: 'messages.upsert', instance: null } ]; for (const invalidPayload of invalidPayloads) { const request = createTestRequest(invalidPayload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(400); } }); test('should verify instance name', async () => { process.env.TEST_VERIFY_INSTANCE = 'true'; const payload = { event: 'messages.upsert', instance: 'wrong-instance', data: {} }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(403); delete process.env.TEST_VERIFY_INSTANCE; }); test('should handle valid messages.upsert', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should ignore empty message content', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: '' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should handle very long messages', async () => { const longMessage = '/tarea nueva ' + 'A'.repeat(5000); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: longMessage } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle messages with special characters and emojis', async () => { const specialMessage = '/tarea nueva Test 😊 你好 @#$%^&*()'; const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: specialMessage } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should ignore non-/tarea commands', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: '/othercommand test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should ignore message with mentions but no command', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: 'Hello everyone!', contextInfo: { mentionedJid: ['1234567890@s.whatsapp.net'] } } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should ignore media attachment messages', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { imageMessage: { caption: 'This is an image' } } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should process command from extendedTextMessage', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { extendedTextMessage: { text: '/t n Test ext' } } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should process command from image caption when caption starts with a command', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { imageMessage: { caption: '/t n From caption' } } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle requests on configured port', async () => { const originalPort = process.env.PORT; process.env.PORT = '3007'; // Satisfacer validación de entorno en start() const prevEnv = { EVOLUTION_API_URL: process.env.EVOLUTION_API_URL, EVOLUTION_API_KEY: process.env.EVOLUTION_API_KEY, EVOLUTION_API_INSTANCE: process.env.EVOLUTION_API_INSTANCE, CHATBOT_PHONE_NUMBER: process.env.CHATBOT_PHONE_NUMBER, WEBHOOK_URL: process.env.WEBHOOK_URL }; process.env.EVOLUTION_API_URL = 'http://localhost:3000'; process.env.EVOLUTION_API_KEY = 'test-key'; process.env.EVOLUTION_API_INSTANCE = 'test-instance'; process.env.CHATBOT_PHONE_NUMBER = '9999999999'; process.env.WEBHOOK_URL = 'http://localhost:3007'; try { const server = await WebhookServer.start(); const response = await fetch('http://localhost:3007/health'); expect(response.status).toBe(200); server.stop(); } finally { process.env.PORT = originalPort; process.env.EVOLUTION_API_URL = prevEnv.EVOLUTION_API_URL; process.env.EVOLUTION_API_KEY = prevEnv.EVOLUTION_API_KEY; process.env.EVOLUTION_API_INSTANCE = prevEnv.EVOLUTION_API_INSTANCE; process.env.CHATBOT_PHONE_NUMBER = prevEnv.CHATBOT_PHONE_NUMBER; process.env.WEBHOOK_URL = prevEnv.WEBHOOK_URL; } }); function getFutureDate(days: number): string { const date = new Date(); date.setDate(date.getDate() + days); return date.toISOString().split('T')[0]; } describe('/tarea command logging', () => { test('should log basic /tarea command', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'user123@s.whatsapp.net' }, message: { conversation: '/tarea test' } } }; await WebhookServer.handleRequest(createTestRequest(payload)); // Check that a response was queued (indicating command processing) expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should log command with due date', async () => { const futureDate = getFutureDate(3); // Get date 3 days in future const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'user123@s.whatsapp.net' }, message: { conversation: `/tarea nueva Finish project @user2 ${futureDate}`, contextInfo: { mentionedJid: ['user2@s.whatsapp.net'] } } } }; await WebhookServer.handleRequest(createTestRequest(payload)); // Verify command processing by checking queue expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); }); test('should handle XSS/SQL injection attempts', async () => { const maliciousMessage = `/tarea nueva '; DROP TABLE tasks; --`; const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: maliciousMessage } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle multiple dates in command (use last one as due date)', async () => { const futureDate1 = getFutureDate(3); const futureDate2 = getFutureDate(5); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: `/tarea nueva Test task ${futureDate1} some text ${futureDate2}` } } }; await WebhookServer.handleRequest(createTestRequest(payload)); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should ignore past dates as due dates', async () => { const pastDate = '2020-01-01'; const futureDate = getFutureDate(2); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: `/tarea nueva Test task ${pastDate} more text ${futureDate}` } } }; await WebhookServer.handleRequest(createTestRequest(payload)); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle multiple past dates correctly', async () => { const pastDate1 = '2020-01-01'; const pastDate2 = '2021-01-01'; const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: `/tarea nueva Test task ${pastDate1} and ${pastDate2}` } } }; await WebhookServer.handleRequest(createTestRequest(payload)); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle mixed valid and invalid date formats', async () => { const futureDate = getFutureDate(2); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'sender-id@s.whatsapp.net' }, message: { conversation: `/tarea nueva Test task 2023-13-01 (invalid) ${futureDate} 25/12/2023 (invalid)` } } }; await WebhookServer.handleRequest(createTestRequest(payload)); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should normalize sender ID before processing', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890:12@s.whatsapp.net' // ID with participant }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should ignore messages with invalid sender ID', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'invalid-id!' // Invalid ID }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should ensure user exists and use normalized ID', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); // Verify user was created in real database const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1234567890'); expect(user).toBeDefined(); expect(user.id).toBe('1234567890'); }); test('should ignore messages if user creation fails', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: null // Invalid participant }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); // Verify no user was created const userCount = testDb.query("SELECT COUNT(*) as count FROM users").get(); expect(userCount.count).toBe(0); }); // Integration tests with real database describe('User validation in handleMessageUpsert', () => { test('should proceed with valid user', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); // Verify user was created in real database const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1234567890'); expect(user).toBeDefined(); expect(user.id).toBe('1234567890'); }); test('should ignore message if user validation fails', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: 'invalid!user@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); // Verify no user was created const userCount = testDb.query("SELECT COUNT(*) as count FROM users").get(); expect(userCount.count).toBe(0); }); test('should handle database errors during user validation', async () => { // Force a database error by corrupting the database state testDb.exec('DROP TABLE users'); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); // Reinitialize database for subsequent tests (force full migration) testDb.exec('DROP TABLE IF EXISTS schema_migrations'); initializeDatabase(testDb); }); test('should integrate user validation completely in handleMessageUpsert with valid user', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); // Verify user was created/updated in database const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1234567890'); expect(user).toBeDefined(); expect(user.id).toBe('1234567890'); expect(user.first_seen).toBeDefined(); expect(user.last_seen).toBeDefined(); }); test('should use normalized ID in command service', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890:12@s.whatsapp.net' // Raw ID with participant }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); await WebhookServer.handleRequest(request); // Verify that a response was queued, indicating command processing expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should handle end-to-end flow with valid user and command processing', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test task' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); // Verify user was created/updated const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1234567890'); expect(user).toBeDefined(); // Verify that a response was queued expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); }); describe('Group validation in handleMessageUpsert', () => { test('should ignore messages from inactive groups', async () => { // Insert inactive group testDb.exec(` INSERT OR REPLACE INTO groups (id, community_id, name, active) VALUES ('inactive-group@g.us', 'test-community', 'Inactive Group', 0) `); const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'inactive-group@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBe(0); }); test('should proceed with messages from active groups', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/tarea nueva Test' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should accept /t alias and process command', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/t n Tarea alias hoy' } } }; const request = createTestRequest(payload); const response = await WebhookServer.handleRequest(request); expect(response.status).toBe(200); expect(SimulatedResponseQueue.getQueue().length).toBeGreaterThan(0); }); test('should never send responses to the group (DM only policy)', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group-id@g.us', participant: '1234567890@s.whatsapp.net' }, message: { conversation: '/t n Probar silencio grupo mañana' } } }; const request = createTestRequest(payload); await WebhookServer.handleRequest(request); const out = SimulatedResponseQueue.getQueue(); expect(out.length).toBeGreaterThan(0); for (const r of out) { expect(r.recipient.endsWith('@g.us')).toBe(false); } }); }); 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 responsable'); 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 responsable'); 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 responsable'); }); }); });