/** * Tests for webhook server security features. */ import { describe, it, expect } from "vitest"; import { startWebhookServer } from "../src/webhook-server.js"; import type { TelegramUpdate } from "../src/types.js"; describe("webhook-server", () => { it("should reject requests without secret", async () => { const received: TelegramUpdate[] = []; const { server, stop } = startWebhookServer({ port: 0, // Random port host: "127.0.0.1", path: "/webhook", secret: "test-secret", trustedProxies: [], onUpdate: (update) => received.push(update), }); // Wait for server to be listening await new Promise((resolve) => setTimeout(resolve, 10)); const address = server.address(); const port = address && typeof address === "object" ? address.port : 0; try { const res = await fetch(`http://127.0.0.1:${port}/webhook`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ update_id: 1, message: { message_id: 1 } }), }); expect(res.status).toBe(401); expect(received.length).toBe(0); } finally { stop(); } }); it("should accept requests with valid secret", async () => { const received: TelegramUpdate[] = []; const { server, stop } = startWebhookServer({ port: 0, host: "127.0.0.1", path: "/webhook", secret: "test-secret", trustedProxies: [], onUpdate: (update) => received.push(update), }); await new Promise((resolve) => setTimeout(resolve, 10)); const address = server.address(); const port = address && typeof address === "object" ? address.port : 0; try { const update: TelegramUpdate = { update_id: 1, message: { message_id: 1, chat: { id: 123, type: "private" }, date: Date.now(), text: "hello", }, }; const res = await fetch(`http://127.0.0.1:${port}/webhook`, { method: "POST", headers: { "Content-Type": "application/json", "X-Telegram-Bot-Api-Secret-Token": "test-secret", }, body: JSON.stringify(update), }); expect(res.status).toBe(200); expect(received.length).toBe(1); expect(received[0]?.update_id).toBe(1); } finally { stop(); } }); it("should reject payloads exceeding body limit", async () => { const received: TelegramUpdate[] = []; const { server, stop } = startWebhookServer({ port: 0, host: "127.0.0.1", path: "/webhook", secret: "test-secret", trustedProxies: [], onUpdate: (update) => received.push(update), }); await new Promise((resolve) => setTimeout(resolve, 10)); const address = server.address(); const port = address && typeof address === "object" ? address.port : 0; try { const largePayload = JSON.stringify({ update_id: 1, message: { text: "x".repeat(2_000_000) }, // > 1MB }); // When payload exceeds limit, server may respond with 413 or destroy connection let rejected = false; try { const res = await fetch(`http://127.0.0.1:${port}/webhook`, { method: "POST", headers: { "Content-Type": "application/json", "X-Telegram-Bot-Api-Secret-Token": "test-secret", }, body: largePayload, }); rejected = res.status === 413; } catch (err: any) { // Connection destroyed (ECONNRESET) is also acceptable rejected = err.code === "ECONNRESET" || err.message?.includes("ECONNRESET") || err.cause?.code === "ECONNRESET"; } expect(rejected).toBe(true); expect(received.length).toBe(0); } finally { stop(); } }); it("should enforce rate limits per IP", async () => { const received: TelegramUpdate[] = []; const { server, stop } = startWebhookServer({ port: 0, host: "127.0.0.1", path: "/webhook", secret: "test-secret", trustedProxies: [], onUpdate: (update) => received.push(update), }); await new Promise((resolve) => setTimeout(resolve, 10)); const address = server.address(); const port = address && typeof address === "object" ? address.port : 0; try { // Make 65 requests (rate limit is 60/min) const requests = []; for (let i = 0; i < 65; i++) { requests.push( fetch(`http://127.0.0.1:${port}/webhook`, { method: "POST", headers: { "Content-Type": "application/json", "X-Telegram-Bot-Api-Secret-Token": "test-secret", }, body: JSON.stringify({ update_id: i }), }) ); } const results = await Promise.all(requests); const statuses = results.map((r) => r.status); // First 60 should succeed, rest should be rate limited const ok = statuses.filter((s) => s === 200).length; const rateLimited = statuses.filter((s) => s === 429).length; expect(ok).toBe(60); expect(rateLimited).toBe(5); } finally { stop(); } }); it("should respond to /healthz", async () => { const { server, stop } = startWebhookServer({ port: 0, host: "127.0.0.1", path: "/webhook", secret: "test-secret", trustedProxies: [], onUpdate: () => {}, }); await new Promise((resolve) => setTimeout(resolve, 10)); const address = server.address(); const port = address && typeof address === "object" ? address.port : 0; try { const res = await fetch(`http://127.0.0.1:${port}/healthz`); expect(res.status).toBe(200); expect(await res.text()).toBe("ok"); } finally { stop(); } }); });