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, } from "../../src/index.js"; import type { PaymentPluginConfig } from "../../src/types/types.js"; import { FakePaymentProvider } from "../setup/fake-provider.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", { id: "u1", emailVerified: true, roles: [] }); await next(); }); const provider = new FakePaymentProvider(); const services = new Map(); services.set("app", app); const registry = createOpenAPIRegistry({ info: { title: "test", version: "0.1" }, }); services.set("openAPIRegistry", registry); 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 — idempotency", () => { it("second create with same key returns same intent (no duplicate)", { timeout: 60_000, }, async () => { const { app, provider } = await boot(); const headers = { "content-type": "application/json", "Idempotency-Key": "order-1", }; const a = await app.request("/payments/intents", { method: "POST", headers, body: JSON.stringify({ amount: { amount: "100", currency: "USD" }, }), }); const b = await app.request("/payments/intents", { method: "POST", headers, body: JSON.stringify({ amount: { amount: "100", currency: "USD" }, }), }); const aBody = (await a.json()) as { intent: { id: string } }; const bBody = (await b.json()) as { intent: { id: string } }; expect(bBody.intent.id).toBe(aBody.intent.id); // Provider should have been called once. const list = await provider.getIntent(aBody.intent.id); expect(list.id).toBe(aBody.intent.id); }); it("different body under same key returns 409", { timeout: 60_000, }, async () => { const { app } = await boot(); const headers = { "content-type": "application/json", "Idempotency-Key": "order-2", }; await app.request("/payments/intents", { method: "POST", headers, body: JSON.stringify({ amount: { amount: "100", currency: "USD" }, }), }); const second = await app.request("/payments/intents", { method: "POST", headers, body: JSON.stringify({ amount: { amount: "200", currency: "USD" }, }), }); expect(second.status).toBe(409); }); });