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,143 @@
import {
createServer,
type IncomingMessage,
type Server,
type ServerResponse,
} from "node:http";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { verifySignature } from "@suaveplan/webhook-client";
import {
createWebhookService,
MemoryDeadLetterSink,
MemoryEndpointStore,
resetWebhookInstruments,
} from "../../src/index.js";
interface CapturedRequest {
method: string;
url: string;
headers: Record<string, string>;
body: string;
}
interface TestReceiver {
readonly url: string;
readonly requests: CapturedRequest[];
close(): Promise<void>;
}
type RequestHandler = (
req: IncomingMessage,
res: ServerResponse,
attempt: number
) => void;
async function startReceiver(handler: RequestHandler): Promise<TestReceiver> {
const requests: CapturedRequest[] = [];
let attempt = 0;
const server: Server = createServer((req, res) => {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
attempt += 1;
const captured: CapturedRequest = {
method: req.method ?? "GET",
url: req.url ?? "/",
headers: Object.fromEntries(
Object.entries(req.headers).map(([k, v]) => [
k,
Array.isArray(v) ? v.join(",") : (v ?? ""),
])
),
body,
};
requests.push(captured);
handler(req, res, attempt);
});
});
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 test receiver");
}
const url = `http://127.0.0.1:${address.port}/hooks`;
return {
url,
requests,
close: () =>
new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
}),
};
}
function noopLogger() {
return {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
}
describe("integration: webhook delivery", () => {
it("sends a signed payload with canonical headers", {
timeout: 30_000,
}, async () => {
const receiver = await startReceiver((_, res) => {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
});
try {
resetWebhookInstruments();
const store = new MemoryEndpointStore({
clock: () => 0,
idFactory: () => "ep-1",
});
const service = createWebhookService({
endpointStore: store,
deadLetterSink: new MemoryDeadLetterSink(),
maxAttempts: 1,
initialRetryDelayMs: 1,
maxRetryDelayMs: 100,
idempotencyGuard: null,
logger: noopLogger(),
clock: () => 0,
fetch: globalThis.fetch.bind(globalThis),
});
await service.register({
tenantId: null,
url: receiver.url,
secret: "shared-secret",
events: ["order.created"],
});
const result = await service.send("ep-1", "order.created", {
orderId: 42,
});
expect(result.status).toBe("delivered");
expect(receiver.requests).toHaveLength(1);
const req = receiver.requests[0];
if (!req) throw new Error("missing request");
expect(req.method).toBe("POST");
expect(req.headers["x-webhook-event"]).toBe("order.created");
expect(req.headers["x-webhook-id"]).toBeTruthy();
expect(req.headers["x-webhook-timestamp"]).toBeTruthy();
const sig = req.headers["x-hub-signature-256"];
expect(sig).toMatch(/^sha256=/);
const verified = await verifySignature(
req.body,
sig as string,
"shared-secret",
"sha256"
);
expect(verified).toBe(true);
expect(JSON.parse(req.body).data).toEqual({ orderId: 42 });
} finally {
await receiver.close();
}
});
});