Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
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(setPrincipalId: string | null = "u1") {
resetPaymentInstruments();
const app = new OpenAPIHono();
app.use("*", async (c, next) => {
c.set(
"user",
setPrincipalId === null
? null
: { id: setPrincipalId, emailVerified: true, roles: [] }
);
await next();
});
const provider = new FakePaymentProvider();
const services = new Map<string, unknown>();
services.set("app", app);
services.set(
"openAPIRegistry",
createOpenAPIRegistry({ info: { title: "t", version: "0.1" } })
);
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 };
}
beforeEach(() => {
resetPaymentInstruments();
});
describe("payment plugin — error paths", () => {
it("currency mismatch on refund returns 422", {
timeout: 60_000,
}, async () => {
const { app } = await boot();
const created = await app.request("/payments/intents", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
amount: { amount: "100", currency: "USD" },
}),
});
const { intent } = (await created.json()) as {
intent: { id: string };
};
await app.request(`/payments/intents/${intent.id}/capture`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
const refundRes = await app.request(
`/payments/intents/${intent.id}/refund`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
amount: { amount: "10", currency: "EUR" },
}),
}
);
expect(refundRes.status).toBe(422);
const refundBody = (await refundRes.json()) as {
error: { code: string };
};
expect(refundBody.error.code).toBe("PAYMENT_PLUGIN.CURRENCY_MISMATCH");
});
it("foreign intent capture returns 404", { timeout: 60_000 }, async () => {
const { app } = await boot("u1");
const created = await app.request("/payments/intents", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
amount: { amount: "100", currency: "USD" },
}),
});
const { intent } = (await created.json()) as {
intent: { id: string };
};
const otherApp = await boot("u2");
const captureRes = await otherApp.app.request(
`/payments/intents/${intent.id}/capture`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
}
);
expect(captureRes.status).toBe(404);
});
});

View File

@@ -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"
);
});
});

View File

@@ -0,0 +1,108 @@
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<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 };
}
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);
});
});

View File

@@ -0,0 +1,123 @@
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<string, unknown>();
services.set("app", app);
services.set(
"openAPIRegistry",
createOpenAPIRegistry({ info: { title: "t", version: "0.1" } })
);
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 };
}
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");
});
});