diff --git a/tests/unit/server.test.ts b/tests/unit/server.test.ts index cc99bf9..7b95a02 100644 --- a/tests/unit/server.test.ts +++ b/tests/unit/server.test.ts @@ -11,949 +11,949 @@ let simulatedQueue: any[] = []; let originalAdd: any; class SimulatedResponseQueue { - static async add(responses: any[]) { - simulatedQueue.push(...responses); - } + static async add(responses: any[]) { + simulatedQueue.push(...responses); + } - static getQueue() { - return simulatedQueue; - } + static getQueue() { + return simulatedQueue; + } - static clear() { - 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; + // 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(); + (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(` + // 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'](); + // 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(` + 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'); - }); - }); + 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('🙅'); + 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('🙅'); + 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'); + }); + }); }); diff --git a/tests/unit/services/command.test.ts b/tests/unit/services/command.test.ts index 4487884..82caa9b 100644 --- a/tests/unit/services/command.test.ts +++ b/tests/unit/services/command.test.ts @@ -7,336 +7,336 @@ import { GroupSyncService } from '../../../src/services/group-sync'; let memDb: Database; const testContextBase = { - sender: '1234567890', - groupId: 'test-group@g.us', - mentions: [] as string[], + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [] as string[], }; beforeEach(() => { - memDb = new Database(':memory:'); - initializeDatabase(memDb); - (CommandService as any).dbInstance = memDb; - (TaskService as any).dbInstance = memDb; - (GroupSyncService as any).dbInstance = memDb; - GroupSyncService.activeGroupsCache.clear(); + memDb = new Database(':memory:'); + initializeDatabase(memDb); + (CommandService as any).dbInstance = memDb; + (TaskService as any).dbInstance = memDb; + (GroupSyncService as any).dbInstance = memDb; + GroupSyncService.activeGroupsCache.clear(); }); test('listar grupo por defecto con /t ver en grupo e incluir “… y X más”', async () => { - // Insert group and cache it as active - memDb.exec(` + // Insert group and cache it as active + 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'); - - // Crear 12 tareas sin asignados en el grupo - for (let i = 1; i <= 12; i++) { - TaskService.createTask({ - description: `Task ${i}`, - due_date: '2025-12-31', - group_id: 'test-group@g.us', - created_by: '1234567890', - }); - } - - const responses = await CommandService.handle({ - sender: '1234567890', - groupId: 'test-group@g.us', - mentions: [], - message: '/t ver' - }); - - expect(responses.length).toBe(1); - expect(responses[0].recipient).toBe('1234567890'); - expect(responses[0].message).toContain('Test Group'); - // Debe indicar que hay 2 más (límite 10) - expect(responses[0].message).toContain('… y 2 más'); - // Debe mostrar “sin responsable” - expect(responses[0].message).toContain('sin responsable'); + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); + + // Crear 12 tareas sin asignados en el grupo + for (let i = 1; i <= 12; i++) { + TaskService.createTask({ + description: `Task ${i}`, + due_date: '2025-12-31', + group_id: 'test-group@g.us', + created_by: '1234567890', + }); + } + + const responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: '/t ver' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + expect(responses[0].message).toContain('Test Group'); + // Debe indicar que hay 2 más (límite 10) + expect(responses[0].message).toContain('… y 2 más'); + // Debe mostrar “sin responsable” + expect(responses[0].message).toContain('🙅'); }); test('listar “mis” por defecto en DM con /t ver', async () => { - // Insert groups and cache them - memDb.exec(` + // Insert groups and cache them + memDb.exec(` INSERT OR REPLACE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1), ('group-2@g.us', 'test-community', 'Group 2', 1) `); - GroupSyncService.activeGroupsCache.clear(); - GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); - GroupSyncService.activeGroupsCache.set('group-2@g.us', 'Group 2'); - - // Crear 2 tareas asignadas al usuario en distintos grupos - const t1 = TaskService.createTask({ - description: 'G1 Task', - due_date: '2025-11-20', - group_id: 'test-group@g.us', - created_by: '1111111111', - }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); - - const t2 = TaskService.createTask({ - description: 'G2 Task', - due_date: '2025-11-25', - group_id: 'group-2@g.us', - created_by: '2222222222', - }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); - - const responses = await CommandService.handle({ - sender: '1234567890', - // Contexto de DM: usar un JID que NO sea de grupo - groupId: '1234567890@s.whatsapp.net', - mentions: [], - message: '/t ver' - }); - - expect(responses.length).toBe(1); - expect(responses[0].recipient).toBe('1234567890'); - const msg = responses[0].message; - expect(msg).toContain('Test Group'); - expect(msg).toContain('Group 2'); - expect(msg).toMatch(/- `\d{4}` G1 Task/); - expect(msg).toMatch(/- `\d{4}` G2 Task/); + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); + GroupSyncService.activeGroupsCache.set('group-2@g.us', 'Group 2'); + + // Crear 2 tareas asignadas al usuario en distintos grupos + const t1 = TaskService.createTask({ + description: 'G1 Task', + due_date: '2025-11-20', + group_id: 'test-group@g.us', + created_by: '1111111111', + }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); + + const t2 = TaskService.createTask({ + description: 'G2 Task', + due_date: '2025-11-25', + group_id: 'group-2@g.us', + created_by: '2222222222', + }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); + + const responses = await CommandService.handle({ + sender: '1234567890', + // Contexto de DM: usar un JID que NO sea de grupo + groupId: '1234567890@s.whatsapp.net', + mentions: [], + message: '/t ver' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + const msg = responses[0].message; + expect(msg).toContain('Test Group'); + expect(msg).toContain('Group 2'); + expect(msg).toMatch(/- `\d{4}` G1 Task/); + expect(msg).toMatch(/- `\d{4}` G2 Task/); }); test('completar tarea: camino feliz, ya completada y no encontrada', async () => { - // Insertar grupo y cache - memDb.exec(` + // Insertar grupo y cache + 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'); - - const taskId = TaskService.createTask({ - description: 'Completar yo', - due_date: '2025-10-10', - group_id: 'test-group@g.us', - created_by: '1111111111', - }); - - const dc = Number((memDb.prepare(`SELECT display_code FROM tasks WHERE id = ?`).get(taskId) as any)?.display_code || 0); - - // 1) Camino feliz - let responses = await CommandService.handle({ - sender: '1234567890', - groupId: 'test-group@g.us', - mentions: [], - message: `/t x ${dc}` - }); - expect(responses.length).toBe(1); - expect(responses[0].recipient).toBe('1234567890'); - expect(responses[0].message).toMatch(/^✅ `\d{4}` _completada_/); - - // 2) Ya completada (ahora no debe resolverse por display_code → no encontrada) - responses = await CommandService.handle({ - sender: '1234567890', - groupId: 'test-group@g.us', - mentions: [], - message: `/t x ${dc}` - }); - expect(responses.length).toBe(1); - expect(responses[0].message).toContain('no encontrada'); - - // 3) No encontrada - responses = await CommandService.handle({ - sender: '1234567890', - groupId: 'test-group@g.us', - mentions: [], - message: `/t x 999999` - }); - expect(responses.length).toBe(1); - expect(responses[0].message).toContain('no encontrada'); + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); + + const taskId = TaskService.createTask({ + description: 'Completar yo', + due_date: '2025-10-10', + group_id: 'test-group@g.us', + created_by: '1111111111', + }); + + const dc = Number((memDb.prepare(`SELECT display_code FROM tasks WHERE id = ?`).get(taskId) as any)?.display_code || 0); + + // 1) Camino feliz + let responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: `/t x ${dc}` + }); + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + expect(responses[0].message).toMatch(/^✅ `\d{4}` _completada_/); + + // 2) Ya completada (ahora no debe resolverse por display_code → no encontrada) + responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: `/t x ${dc}` + }); + expect(responses.length).toBe(1); + expect(responses[0].message).toContain('no encontrada'); + + // 3) No encontrada + responses = await CommandService.handle({ + sender: '1234567890', + groupId: 'test-group@g.us', + mentions: [], + message: `/t x 999999` + }); + expect(responses.length).toBe(1); + 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(` + // 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 responsable'); - expect(msg).toContain('… y 2 más'); - expect(msg).not.toContain('Asignada 1'); - expect(msg).not.toContain('Asignada 2'); + 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('🙅'); + 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'); + 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(` + // 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 responsable'); - expect(msg).toContain('… y 2 más'); // paginación en la sección “sin dueño” + 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('🙅'); + 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 responsable'); + // 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 responsable'); }); afterEach(() => { - try { memDb.close(); } catch {} + try { memDb.close(); } catch { } }); describe('CommandService', () => { - test('should ignore non-tarea commands', async () => { - const responses = await CommandService.handle({ - ...testContextBase, - message: '/othercommand' - }); - expect(responses).toEqual([]); - }); - - test('acepta alias /t y responde con formato compacto', async () => { - const responses = await CommandService.handle({ - ...testContextBase, - message: '/t n Test task' - }); - - expect(responses.length).toBe(1); - expect(responses[0].recipient).toBe('1234567890'); - // Debe empezar con "📝 `0001` " - expect(responses[0].message).toMatch(/^📝 `\d{4}` /); - // Debe mostrar la descripción en texto plano (sin cursiva) - expect(responses[0].message).toContain(' Test task'); - expect(responses[0].message).not.toContain('_Test task_'); - // No debe usar el texto antiguo "Tarea creada" - expect(responses[0].message).not.toMatch(/Tarea \d+ creada/); - }); - - test('should return error response on failure', async () => { - // Forzar error temporalmente - const original = TaskService.createTask; - (TaskService as any).createTask = () => { throw new Error('forced'); }; - - const responses = await CommandService.handle({ - ...testContextBase, - message: '/tarea nueva Test task' - }); - - expect(responses.length).toBe(1); - expect(responses[0].recipient).toBe('1234567890'); - expect(responses[0].message).toBe('Error processing command'); - - // Restaurar - (TaskService as any).createTask = original; - }); + test('should ignore non-tarea commands', async () => { + const responses = await CommandService.handle({ + ...testContextBase, + message: '/othercommand' + }); + expect(responses).toEqual([]); + }); + + test('acepta alias /t y responde con formato compacto', async () => { + const responses = await CommandService.handle({ + ...testContextBase, + message: '/t n Test task' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + // Debe empezar con "📝 `0001` " + expect(responses[0].message).toMatch(/^📝 `\d{4}` /); + // Debe mostrar la descripción en texto plano (sin cursiva) + expect(responses[0].message).toContain(' Test task'); + expect(responses[0].message).not.toContain('_Test task_'); + // No debe usar el texto antiguo "Tarea creada" + expect(responses[0].message).not.toMatch(/Tarea \d+ creada/); + }); + + test('should return error response on failure', async () => { + // Forzar error temporalmente + const original = TaskService.createTask; + (TaskService as any).createTask = () => { throw new Error('forced'); }; + + const responses = await CommandService.handle({ + ...testContextBase, + message: '/tarea nueva Test task' + }); + + expect(responses.length).toBe(1); + expect(responses[0].recipient).toBe('1234567890'); + expect(responses[0].message).toBe('Error processing command'); + + // Restaurar + (TaskService as any).createTask = original; + }); });