import crypto from "node:crypto"; import { describe, expect, it, vi } from "vitest"; import { createLineWebhookMiddleware } from "./webhook.js"; const sign = (body: string, secret: string) => crypto.createHmac("SHA256", secret).update(body).digest("base64"); const createRes = () => { const res = { status: vi.fn(), json: vi.fn(), headersSent: false, } as any; res.status.mockReturnValue(res); res.json.mockReturnValue(res); return res; }; describe("createLineWebhookMiddleware", () => { it("parses JSON from raw string body", async () => { const onEvents = vi.fn(async () => {}); const secret = "secret"; const rawBody = JSON.stringify({ events: [{ type: "message" }] }); const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents }); const req = { headers: { "x-line-signature": sign(rawBody, secret) }, body: rawBody, } as any; const res = createRes(); await middleware(req, res, {} as any); expect(res.status).toHaveBeenCalledWith(200); expect(onEvents).toHaveBeenCalledWith(expect.objectContaining({ events: expect.any(Array) })); }); it("parses JSON from raw buffer body", async () => { const onEvents = vi.fn(async () => {}); const secret = "secret"; const rawBody = JSON.stringify({ events: [{ type: "follow" }] }); const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents }); const req = { headers: { "x-line-signature": sign(rawBody, secret) }, body: Buffer.from(rawBody, "utf-8"), } as any; const res = createRes(); await middleware(req, res, {} as any); expect(res.status).toHaveBeenCalledWith(200); expect(onEvents).toHaveBeenCalledWith(expect.objectContaining({ events: expect.any(Array) })); }); it("rejects invalid JSON payloads", async () => { const onEvents = vi.fn(async () => {}); const secret = "secret"; const rawBody = "not json"; const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents }); const req = { headers: { "x-line-signature": sign(rawBody, secret) }, body: rawBody, } as any; const res = createRes(); await middleware(req, res, {} as any); expect(res.status).toHaveBeenCalledWith(400); expect(onEvents).not.toHaveBeenCalled(); }); it("rejects webhooks with invalid signatures", async () => { const onEvents = vi.fn(async () => {}); const secret = "secret"; const rawBody = JSON.stringify({ events: [{ type: "message" }] }); const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents }); const req = { headers: { "x-line-signature": "invalid-signature" }, body: rawBody, } as any; const res = createRes(); await middleware(req, res, {} as any); expect(res.status).toHaveBeenCalledWith(401); expect(onEvents).not.toHaveBeenCalled(); }); it("rejects webhooks with signatures computed using wrong secret", async () => { const onEvents = vi.fn(async () => {}); const correctSecret = "correct-secret"; const wrongSecret = "wrong-secret"; const rawBody = JSON.stringify({ events: [{ type: "message" }] }); const middleware = createLineWebhookMiddleware({ channelSecret: correctSecret, onEvents }); const req = { headers: { "x-line-signature": sign(rawBody, wrongSecret) }, body: rawBody, } as any; const res = createRes(); await middleware(req, res, {} as any); expect(res.status).toHaveBeenCalledWith(401); expect(onEvents).not.toHaveBeenCalled(); }); });