import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; import { WebhookServer } from '../../src/server'; import { ResponseQueue } from '../../src/services/response-queue'; // Mock the ResponseQueue let mockAdd: any; beforeEach(() => { mockAdd = mock(() => Promise.resolve()); ResponseQueue.add = mockAdd; }); describe('WebhookServer', () => { const envBackup = process.env; beforeEach(() => { process.env = { ...envBackup, INSTANCE_NAME: 'test-instance', NODE_ENV: 'test' }; }); afterEach(() => { process.env = envBackup; }); 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 payload of invalidPayloads) { const request = createTestRequest(payload); 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(mockAdd).toHaveBeenCalled(); }); 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(mockAdd).not.toHaveBeenCalled(); }); 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(mockAdd).toHaveBeenCalled(); }); 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(mockAdd).toHaveBeenCalled(); }); 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(mockAdd).not.toHaveBeenCalled(); }); 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(mockAdd).not.toHaveBeenCalled(); }); 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(mockAdd).not.toHaveBeenCalled(); }); test('should handle requests on configured port', async () => { const originalPort = process.env.PORT; process.env.PORT = '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; } }); describe('/tarea command logging', () => { let consoleSpy: any; beforeEach(() => { consoleSpy = mock(() => {}); console.log = consoleSpy; }); afterEach(() => { consoleSpy.mockRestore(); }); test('should log basic /tarea command', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group123@g.us', participant: 'user123@s.whatsapp.net' }, message: { conversation: '/tarea test' } } }; await WebhookServer.handleRequest(createTestRequest(payload)); expect(consoleSpy).toHaveBeenCalledWith( '🔍 Detected /tarea command:', expect.objectContaining({ rawMessage: '/tarea test' }) ); expect(consoleSpy).toHaveBeenCalledWith( '✅ Successfully parsed command:', expect.objectContaining({ action: 'test', description: '', dueDate: 'none', mentionCount: 0 }) ); }); test('should log command with due date', async () => { const payload = { event: 'messages.upsert', instance: 'test-instance', data: { key: { remoteJid: 'group123@g.us', participant: 'user123@s.whatsapp.net' }, message: { conversation: '/tarea nueva Finish project @user2 2025-04-30', contextInfo: { mentionedJid: ['user2@s.whatsapp.net'] } } } }; await WebhookServer.handleRequest(createTestRequest(payload)); // Verify the two command-related log calls (first log is skipped in test mode) expect(consoleSpy).toHaveBeenCalledTimes(2); // First call should be the command detection expect(consoleSpy.mock.calls[0][0]).toBe('🔍 Detected /tarea command:'); expect(consoleSpy.mock.calls[0][1]).toEqual({ timestamp: expect.any(String), from: 'user123@s.whatsapp.net', group: 'group123@g.us', rawMessage: '/tarea nueva Finish project @user2 2025-04-30' }); // Second call should be the successful parsing expect(consoleSpy.mock.calls[1][0]).toBe('✅ Successfully parsed command:'); expect(consoleSpy.mock.calls[1][1]).toEqual({ action: 'nueva', description: 'Finish project @user2', dueDate: '2025-04-30', mentionCount: 1 }); }); }); 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(mockAdd).toHaveBeenCalled(); }); });