From 4f5fbefa99159dc90321fc16a517d28b8ca9c152 Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 6 Jul 2026 02:14:21 -0700 Subject: [PATCH] fix: return wechat pay webhook validation failures --- .../wechat/handleWeChatPayWebhook.ts | 21 +++- .../wechat-pay-webhook-handler.test.ts | 96 +++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/server/src/internal/domestic-payments/wechat/handleWeChatPayWebhook.ts b/server/src/internal/domestic-payments/wechat/handleWeChatPayWebhook.ts index 1947d1ace..0d08f088d 100644 --- a/server/src/internal/domestic-payments/wechat/handleWeChatPayWebhook.ts +++ b/server/src/internal/domestic-payments/wechat/handleWeChatPayWebhook.ts @@ -1,4 +1,5 @@ import type { Context } from "hono"; +import { StatusCodes } from "http-status-codes"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { DomesticPaymentIntentService, @@ -25,10 +26,22 @@ export function createWeChatPayWebhookHandler({ return async (c: Context) => { const ctx = c.get("ctx"); const body = await c.req.text(); - const notification = await adapter.parseNotification({ - body, - headers: c.req.raw.headers, - }); + let notification: Awaited>; + + try { + notification = await adapter.parseNotification({ + body, + headers: c.req.raw.headers, + }); + } catch (error) { + return c.json( + { + code: "FAIL", + message: error instanceof Error ? error.message : String(error), + }, + StatusCodes.BAD_REQUEST, + ); + } await intentService.processParsedNotification({ ctx, diff --git a/server/tests/unit/domestic-payments/wechat-pay-webhook-handler.test.ts b/server/tests/unit/domestic-payments/wechat-pay-webhook-handler.test.ts index 8af9e605b..bfae664d4 100644 --- a/server/tests/unit/domestic-payments/wechat-pay-webhook-handler.test.ts +++ b/server/tests/unit/domestic-payments/wechat-pay-webhook-handler.test.ts @@ -92,4 +92,100 @@ describe("createWeChatPayWebhookHandler", () => { notification, }); }); + + test("returns a WeChat FAIL response with HTTP 400 when notification parsing fails", async () => { + const body = JSON.stringify({ id: "notify_bad" }); + const adapter: PaymentProcessorAdapter = { + provider: DomesticPaymentProvider.WeChatPay, + createPayment: async () => { + throw new Error("createPayment is not used in this test"); + }, + queryPayment: async () => { + throw new Error("queryPayment is not used in this test"); + }, + closePayment: async () => { + throw new Error("closePayment is not used in this test"); + }, + parseNotification: mock(async () => { + throw new Error("Invalid WeChat notification signature"); + }), + }; + const processParsedNotification = mock(async () => { + throw new Error("processParsedNotification must not be called"); + }); + const app = new Hono(); + app.use("*", async (c, next) => { + c.set("ctx", ctx as HonoEnv["Variables"]["ctx"]); + await next(); + }); + app.post( + "/webhooks/wechatpay", + createWeChatPayWebhookHandler({ + adapter, + intentService: { processParsedNotification }, + }), + ); + + const response = await app.request("/webhooks/wechatpay", { + method: "POST", + body, + headers: { + "Wechatpay-Signature": "bad_signature", + }, + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + code: "FAIL", + message: "Invalid WeChat notification signature", + }); + expect(processParsedNotification).toHaveBeenCalledTimes(0); + }); + + test("returns a server error after a parsed notification hits a service failure", async () => { + const notification = { + providerEventId: "notify_123", + idempotencyKey: "wechatpay:notification:notify_123", + eventType: DomesticPaymentEventType.WebhookPaid, + providerOrderId: "dp_order_123", + providerTransactionId: "wx_txn_123", + status: DomesticPaymentIntentStatus.Paid, + paidAt: 1_774_001_860_000, + rawPayload: { id: "notify_123" }, + parsedPayload: { out_trade_no: "dp_order_123" }, + }; + const adapter: PaymentProcessorAdapter = { + provider: DomesticPaymentProvider.WeChatPay, + createPayment: async () => { + throw new Error("createPayment is not used in this test"); + }, + queryPayment: async () => { + throw new Error("queryPayment is not used in this test"); + }, + closePayment: async () => { + throw new Error("closePayment is not used in this test"); + }, + parseNotification: mock(async () => notification), + }; + const processParsedNotification = mock(async () => { + throw new Error("database unavailable"); + }); + const handler = createWeChatPayWebhookHandler({ + adapter, + intentService: { processParsedNotification }, + }); + const app = new Hono(); + app.use("*", async (c, next) => { + c.set("ctx", ctx as HonoEnv["Variables"]["ctx"]); + await next(); + }); + app.post("/webhooks/wechatpay", handler); + + const response = await app.request("/webhooks/wechatpay", { + method: "POST", + body: JSON.stringify({ id: "notify_123" }), + }); + + expect(response.status).toBe(500); + }); });