feat: implement base webhook server with validation and tests

main
borja (aider) 3 months ago
parent 714c756dbf
commit b7ed60a471

@ -0,0 +1,73 @@
import { Bun } from 'bun';
import { CommandService } from './services/command';
const PORT = 3007;
type WebhookPayload = {
event: string;
instance: string;
data: any;
// Other fields from Evolution API
};
export class WebhookServer {
static async handleRequest(request: Request): Promise<Response> {
// 1. Method validation
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
// 2. Content-Type validation
const contentType = request.headers.get('content-type');
if (!contentType?.includes('application/json')) {
return new Response('Invalid content type', { status: 400 });
}
try {
// 3. Parse and validate payload
const payload = await request.json() as WebhookPayload;
if (!payload.event || !payload.instance) {
return new Response('Invalid payload', { status: 400 });
}
// 4. Verify instance matches
if (payload.instance !== process.env.INSTANCE_NAME) {
return new Response('Invalid instance', { status: 403 });
}
// 5. Route events
switch (payload.event) {
case 'messages.upsert':
await this.handleMessageUpsert(payload.data);
break;
// Other events will be added later
}
return new Response('OK', { status: 200 });
} catch (error) {
return new Response('Invalid request', { status: 400 });
}
}
private static async handleMessageUpsert(data: any) {
// Basic message validation
if (!data?.key?.remoteJid || !data.message) return;
// Forward to command service if applicable
const messageText = data.message.conversation;
if (messageText?.startsWith('/tarea')) {
await CommandService.handle({
sender: data.key.participant,
groupId: data.key.remoteJid,
message: messageText,
mentions: data.contextInfo?.mentionedJid || []
});
}
}
}
Bun.serve({
port: PORT,
fetch: WebhookServer.handleRequest,
});

@ -0,0 +1,78 @@
import { describe, test, expect, mock } from 'bun:test';
import { WebhookServer } from '../../src/server';
describe('WebhookServer', () => {
const envBackup = process.env;
beforeEach(() => {
process.env = { ...envBackup, INSTANCE_NAME: 'test-instance' };
});
afterEach(() => {
process.env = envBackup;
});
const createTestRequest = (payload: any) =>
new Request('http://localhost:3007', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
test('should reject non-POST requests', async () => {
const request = new Request('http://localhost:3007', { method: 'GET' });
const response = await WebhookServer.handleRequest(request);
expect(response.status).toBe(405);
});
test('should require JSON content type', async () => {
const request = new Request('http://localhost:3007', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' }
});
const response = await WebhookServer.handleRequest(request);
expect(response.status).toBe(400);
});
test('should validate payload structure', async () => {
const invalidPayloads = [
{},
{ event: null },
{ event: 'messages.upsert', instance: null }
];
for (const payload of invalidPayloads) {
const request = createTestRequest(payload);
const response = await WebhookServer.handleRequest(request);
expect(response.status).toBe(400);
}
});
test('should verify instance name', async () => {
const payload = {
event: 'messages.upsert',
instance: 'wrong-instance',
data: {}
};
const request = createTestRequest(payload);
const response = await WebhookServer.handleRequest(request);
expect(response.status).toBe(403);
});
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);
});
});
Loading…
Cancel
Save