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.

707 lines
22 KiB
TypeScript

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';
// 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;
// Ensure database is initialized (recreates tables if dropped)
initializeDatabase(testDb);
// Reset database state between tests
testDb.exec('DELETE FROM task_assignments');
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 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;
}
});
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 <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(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
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);
});
});
});