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