124 lines
3.8 KiB
TypeScript
124 lines
3.8 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,
|
|
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");
|
|
});
|
|
});
|