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