Initial Commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user