You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 lines
9.7 KiB
TypeScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 all log calls in order
expect(consoleSpy.mock.calls).toEqual([
[
' Incoming webhook request:',
expect.objectContaining({
method: 'POST',
path: '/',
time: expect.any(String)
})
],
[
'🔍 Detected /tarea command:',
expect.objectContaining({
timestamp: expect.any(String),
from: 'user123@s.whatsapp.net',
group: 'group123@g.us',
rawMessage: '/tarea nueva Finish project @user2 2025-04-30'
})
],
[
'✅ Successfully parsed command:',
expect.objectContaining({
action: 'nueva',
description: 'Finish project @user2',
dueDate: '2025-04-30',
mentionCount: 1
})
]
]);
});
});
test('should handle XSS/SQL injection attempts', async () => {
const maliciousMessage = `/tarea nueva <script>alert('xss')</script>'; 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();
});
});