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"; interface BootResult { app: OpenAPIHono; provider: FakePaymentProvider; setPrincipal: (id: string | null) => void; } async function boot(): Promise { resetPaymentInstruments(); const app = new OpenAPIHono(); let currentPrincipalId: string | null = "u1"; app.use("*", async (c, next) => { if (currentPrincipalId === null) { c.set("user", null); } else { c.set("user", { id: currentPrincipalId, 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, setPrincipal: (id) => { currentPrincipalId = id; }, }; } async function jsonReq( app: OpenAPIHono, path: string, init: { method: string; body?: unknown; headers?: Record } ) { const headers: Record = { "content-type": "application/json", ...(init.headers ?? {}), }; const reqInit: RequestInit = { method: init.method, headers }; if (init.body !== undefined) { reqInit.body = JSON.stringify(init.body); } const res = await app.request(path, reqInit); const text = await res.text(); let parsed: unknown = null; try { parsed = text.length > 0 ? JSON.parse(text) : null; } catch { parsed = text; } return { res, body: parsed as Record | null }; } beforeEach(() => { resetPaymentInstruments(); }); describe("payment plugin — full lifecycle", () => { it("create → confirm → capture → refund → get", { timeout: 60_000, }, async () => { const { app, setPrincipal } = await boot(); setPrincipal("u1"); const created = await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "1000", currency: "USD" }, captureMethod: "manual", }, }); expect(created.res.status).toBe(201); const intentId = (created.body?.["intent"] as { id: string }).id; const confirmed = await jsonReq( app, `/payments/intents/${intentId}/confirm`, { method: "POST", body: {} } ); expect(confirmed.res.status).toBe(200); const captured = await jsonReq( app, `/payments/intents/${intentId}/capture`, { method: "POST", body: {} } ); expect(captured.res.status).toBe(200); expect((captured.body?.["intent"] as { status: string }).status).toBe( "captured" ); const refunded = await jsonReq( app, `/payments/intents/${intentId}/refund`, { method: "POST", body: { amount: { amount: "400", currency: "USD" }, reason: "requested_by_customer", }, } ); expect(refunded.res.status).toBe(200); const final = await jsonReq(app, `/payments/intents/${intentId}`, { method: "GET", }); expect(final.res.status).toBe(200); const finalIntent = final.body?.["intent"] as { status: string; refundedAmount: { amount: string }; }; expect(finalIntent.status).toBe("partially_refunded"); }); it("foreign intent returns 404 anti-enumeration", { timeout: 60_000, }, async () => { const { app, setPrincipal } = await boot(); setPrincipal("u1"); const created = await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "100", currency: "USD" } }, }); const intentId = (created.body?.["intent"] as { id: string }).id; setPrincipal("u2"); const foreign = await jsonReq(app, `/payments/intents/${intentId}`, { method: "GET", }); expect(foreign.res.status).toBe(404); const missing = await jsonReq( app, "/payments/intents/pi_does_not_exist", { method: "GET" } ); expect(missing.res.status).toBe(404); }); it("list returns only owner-owned intents", { timeout: 60_000, }, async () => { const { app, setPrincipal } = await boot(); setPrincipal("u1"); await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "100", currency: "USD" } }, }); await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "200", currency: "USD" } }, }); setPrincipal("u2"); await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "300", currency: "USD" } }, }); setPrincipal("u1"); const list = await jsonReq(app, "/payments/intents", { method: "GET", }); expect(list.res.status).toBe(200); expect((list.body?.["intents"] as readonly unknown[]).length).toBe(2); }); it("cancel route works", { timeout: 60_000 }, async () => { const { app, setPrincipal } = await boot(); setPrincipal("u1"); const created = await jsonReq(app, "/payments/intents", { method: "POST", body: { amount: { amount: "100", currency: "USD" } }, }); const intentId = (created.body?.["intent"] as { id: string }).id; const canceled = await jsonReq( app, `/payments/intents/${intentId}/cancel`, { method: "POST", body: {} } ); expect(canceled.res.status).toBe(200); expect((canceled.body?.["intent"] as { status: string }).status).toBe( "canceled" ); }); });