import { OpenAPIHono } from "@hono/zod-openapi"; import { createOpenAPIRegistry } from "@suaveplan/api-openapi"; import { createPluginContext } from "@suaveplan/api-plugin"; import { EventBus } from "@suaveplan/events"; import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner"; import { createPaymentPlugin, resetPaymentInstruments, signPaymentWebhook, } from "../../src/index.js"; import type { PaymentPluginConfig } from "../../src/types/types.js"; import { FakePaymentProvider } from "../setup/fake-provider.js"; import { flushTestMetrics } from "../setup/polyfills.js"; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; const SECRET = "shared-webhook-secret-32-chars-or-more"; async function boot() { resetPaymentInstruments(); const app = new OpenAPIHono(); app.use("*", async (c, next) => { c.set("user", null); await next(); }); const provider = new FakePaymentProvider(); const services = new Map(); services.set("app", app); services.set( "openAPIRegistry", createOpenAPIRegistry({ info: { title: "t", version: "0.1" } }) ); const plugin = createPaymentPlugin({ provider, webhookSecret: SECRET }); await plugin.onInit?.( createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as PaymentPluginConfig, services, }) ); return { app, provider }; } beforeEach(() => { resetPaymentInstruments(); }); describe("payment plugin — webhook", () => { it("valid HMAC accepted", { timeout: 60_000 }, async () => { const { app } = await boot(); const body = JSON.stringify({ id: "evt_1", type: "intent.captured", timestamp: 1, data: {}, }); const ts = Date.now(); const sig = signPaymentWebhook({ body, timestamp: ts, secret: SECRET, algorithm: "SHA-256", }); const res = await app.request("/payments/webhook", { method: "POST", headers: { "content-type": "application/json", "X-Signature": sig, "X-Timestamp": String(ts), }, body, }); expect(res.status).toBe(200); const parsed = (await res.json()) as { accepted: boolean }; expect(parsed.accepted).toBe(true); const collected = await flushTestMetrics(); const names = collected.flatMap((c) => c.scopeMetrics.flatMap((sm) => sm.metrics.map((m) => m.descriptor.name) ) ); expect(names).toContain("payments.webhook_received_total"); }); it("invalid HMAC rejected before deserialisation", { timeout: 60_000, }, async () => { const { app } = await boot(); const body = JSON.stringify({ id: "evt_1", type: "intent.captured", timestamp: 1, data: {}, }); const ts = Date.now(); const res = await app.request("/payments/webhook", { method: "POST", headers: { "content-type": "application/json", "X-Signature": "deadbeef", "X-Timestamp": String(ts), }, body, }); expect(res.status).toBe(400); const collected = await flushTestMetrics(); const names = collected.flatMap((c) => c.scopeMetrics.flatMap((sm) => sm.metrics.map((m) => m.descriptor.name) ) ); expect(names).toContain("payments.webhook_signature_failures_total"); }); });