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;
+ });
});