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.

960 lines
31 KiB
TypeScript

This file contains invisible Unicode characters!

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

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

import { describe, test, expect, beforeEach, afterEach, 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';
import { TaskService } from '../../src/tasks/service';
// 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;
// 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']();
});
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 <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 (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');
});
});
});