Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions test/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -919,11 +919,91 @@ describe("webhook adapter", () => {
}
});

it("refuses a non-loopback bind when no token is set", () => {
expect(() => assert_bind_allowed("0.0.0.0", undefined)).toThrow(/refuses non-loopback/);
expect(() => assert_bind_allowed("0.0.0.0", "")).toThrow(/refuses non-loopback/);
expect(() => assert_bind_allowed("127.0.0.1", undefined)).not.toThrow();
expect(() => assert_bind_allowed("0.0.0.0", "sekrit")).not.toThrow();
it("accepts only exact loopback names and never logs the token", () => {
const warn = vi.spyOn(logger, "warn").mockImplementation(() => undefined);
try {
for (const host of ["127.0.0.1", " localhost ", "::1", "LocalHost"]) {
expect(() => assert_bind_allowed(host, undefined)).not.toThrow();
}
for (const host of ["0.0.0.0", "[::1]", "127.1", "::ffff:127.0.0.1"]) {
expect(() => assert_bind_allowed(host, undefined)).toThrow(/refuses non-loopback/);
expect(() => assert_bind_allowed(host, "")).toThrow(/refuses non-loopback/);
}
assert_bind_allowed("10.1.2.3", "sekrit");
expect(warn).toHaveBeenCalledWith(expect.stringContaining("binding 10.1.2.3"));
expect(warn.mock.calls.some((call) => String(call[0]).includes("sekrit"))).toBe(false);
} finally {
warn.mockRestore();
}
});

it("rejects non-object JSON and stringifies numeric chat ids", async () => {
const work_dir = temp_work_dir();
let port: number | undefined;
const seen: string[] = [];
const adapter = make_adapter(work_dir, "echo", (bound) => {
port = bound;
}, async (platform, chat_id, user_id, text) => {
seen.push(`${platform}:${chat_id}:${user_id}:${text}`);
return "ok";
});
try {
await adapter.start();
if (port === undefined) {
throw new Error("webhook did not report a listening port");
}
const base = `http://127.0.0.1:${port}/message`;
for (const body of ["[]", "null", "\"hi\"", "42"]) {
const response = await fetch(base, { method: "POST", body });
expect(response.status).toBe(400);
}
const numeric = await fetch(base, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ text: "hi", chat_id: 42, user_id: 7 }),
});
expect(numeric.status).toBe(200);
expect(seen).toEqual(["webhook:42:7:hi"]);
} finally {
await adapter.stop();
rmSync(work_dir, { recursive: true, force: true });
}
});

it("rejects a chunked body over the cap when content-length is absent", async () => {
const work_dir = temp_work_dir();
let port: number | undefined;
const seen: string[] = [];
const adapter = make_adapter(work_dir, "echo", (bound) => {
port = bound;
}, async () => {
seen.push("called");
return "no";
});
try {
await adapter.start();
if (port === undefined) {
throw new Error("webhook did not report a listening port");
}
const { request } = await import("node:http");
const status = await new Promise<number>((resolve, reject) => {
const req = request(
{ hostname: "127.0.0.1", port, path: "/message", method: "POST" },
(res) => {
res.resume();
res.on("end", () => resolve(res.statusCode ?? 0));
},
);
req.on("error", reject);
req.write("x".repeat(MAX_WEBHOOK_BODY_BYTES + 1));
req.end();
});
expect(status).toBe(413);
expect(seen).toEqual([]);
} finally {
await adapter.stop();
rmSync(work_dir, { recursive: true, force: true });
}
});
});

Expand Down
Loading
Loading