Files
suaveplan/packages/api-stack/api/api-plugin-payment/tests/integration/full-lifecycle.test.ts
2026-05-27 23:29:53 +01:00

233 lines
7.1 KiB
TypeScript

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<BootResult> {
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<string, unknown>();
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<PaymentPluginConfig>({
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<string, string> }
) {
const headers: Record<string, string> = {
"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<string, unknown> | 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"
);
});
});