126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { createServer, type Server } from "node:http";
|
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
|
|
import { createPluginContext } from "@suaveplan/api-plugin";
|
|
import { EventBus } from "@suaveplan/events";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
createWebhookPlugin,
|
|
resetWebhookInstruments,
|
|
type WebhookPluginConfig,
|
|
type WebhookService,
|
|
} from "../../src/index.js";
|
|
|
|
interface Receiver {
|
|
readonly url: string;
|
|
readonly observed: { event: string; body: string }[];
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
async function startReceiver(): Promise<Receiver> {
|
|
const observed: { event: string; body: string }[] = [];
|
|
const server: Server = createServer((req, res) => {
|
|
let body = "";
|
|
req.on("data", (c) => {
|
|
body += c;
|
|
});
|
|
req.on("end", () => {
|
|
const evt = req.headers["x-webhook-event"];
|
|
observed.push({
|
|
event: Array.isArray(evt) ? (evt[0] ?? "") : (evt ?? ""),
|
|
body,
|
|
});
|
|
res.writeHead(200);
|
|
res.end("ok");
|
|
});
|
|
});
|
|
await new Promise<void>((resolve) =>
|
|
server.listen(0, "127.0.0.1", resolve)
|
|
);
|
|
const address = server.address();
|
|
if (address === null || typeof address === "string") {
|
|
throw new Error("failed to bind");
|
|
}
|
|
return {
|
|
url: `http://127.0.0.1:${address.port}/hooks`,
|
|
observed,
|
|
close: () =>
|
|
new Promise<void>((resolve, reject) => {
|
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
}),
|
|
};
|
|
}
|
|
|
|
const noopLogger = {
|
|
debug: () => undefined,
|
|
info: () => undefined,
|
|
warn: () => undefined,
|
|
error: () => undefined,
|
|
};
|
|
|
|
describe("integration: webhook plugin full boot", () => {
|
|
it("POST /webhooks/:id/test fires a synthetic webhook.ping", {
|
|
timeout: 30_000,
|
|
}, async () => {
|
|
const receiver = await startReceiver();
|
|
try {
|
|
resetWebhookInstruments();
|
|
const app = new OpenAPIHono();
|
|
app.onError((err, c) => {
|
|
const ctx = (err as { context?: { statusCode?: number } })
|
|
.context;
|
|
const status = (ctx?.statusCode ?? 500) as
|
|
| 401
|
|
| 403
|
|
| 404
|
|
| 500;
|
|
return c.json({ message: err?.message ?? "error" }, status);
|
|
});
|
|
const registry = createOpenAPIRegistry({
|
|
info: { title: "test", version: "0.1.0" },
|
|
});
|
|
const services = new Map<string, unknown>();
|
|
services.set("app", app);
|
|
services.set("openAPIRegistry", registry);
|
|
// Install a permissive auth middleware ABOVE the plugin's
|
|
// /webhooks/* requireAuth so the integration test does not
|
|
// need a real auth plugin.
|
|
app.use("*", async (c, next) => {
|
|
c.set("user", { id: "u", tenantId: null });
|
|
await next();
|
|
});
|
|
const descriptor = createWebhookPlugin();
|
|
const ctx = createPluginContext<WebhookPluginConfig>({
|
|
logger: noopLogger,
|
|
eventBus: new EventBus(),
|
|
signal: new AbortController().signal,
|
|
config: descriptor.defaults as WebhookPluginConfig,
|
|
services,
|
|
});
|
|
await descriptor.onInit?.(ctx);
|
|
|
|
// Register an endpoint via the service that the plugin
|
|
// registered in ServiceRegistry.
|
|
const service = ctx.getService("webhookService") as WebhookService;
|
|
const endpoint = await service.register({
|
|
tenantId: null,
|
|
url: receiver.url,
|
|
secret: "shared-secret",
|
|
events: ["webhook.ping"],
|
|
});
|
|
|
|
// Fire via the test route.
|
|
const res = await app.request(`/webhooks/${endpoint.id}/test`, {
|
|
method: "POST",
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { status: string };
|
|
expect(body.status).toBe("delivered");
|
|
expect(receiver.observed).toHaveLength(1);
|
|
expect(receiver.observed[0]?.event).toBe("webhook.ping");
|
|
} finally {
|
|
await receiver.close();
|
|
}
|
|
});
|
|
});
|