feat: implement base webhook server with validation and tests
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…
Reference in New Issue