pi-telegram-webhook/tests/webhook-server.test.ts
Harivansh Rathi ce9abc2a8e initial: webhook telegram adapter for pi with streaming replies
- webhook server with secret validation, rate limiting, body guards
- streaming replies via sendMessage + editMessageText throttled loop
- RPC session management for persistent conversations
- 15/15 tests passing
2026-04-03 05:30:05 +00:00

195 lines
5.7 KiB
TypeScript

/**
* 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();
}
});
});