import { createServer, type IncomingMessage, type Server, type ServerResponse, } from "node:http"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { createWebhookService, MemoryDeadLetterSink, MemoryEndpointStore, resetWebhookInstruments, WEBHOOK_CODES, } from "../../src/index.js"; async function startReceiver( handler: (req: IncomingMessage, res: ServerResponse) => void ) { const server: Server = createServer((req, res) => { req.on("data", () => undefined); req.on("end", () => handler(req, res)); }); await new Promise((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`, close: () => new Promise((resolve, reject) => { server.close((err) => (err ? reject(err) : resolve())); }), }; } function noopLogger() { return { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; } describe("integration: webhook dead letter", () => { it("5x 500 lands in dead letter", { timeout: 30_000 }, async () => { let attempts = 0; const receiver = await startReceiver((_, res) => { attempts++; res.writeHead(500); res.end("nope"); }); try { resetWebhookInstruments(); const sink = new MemoryDeadLetterSink(); const store = new MemoryEndpointStore({ clock: () => 0, idFactory: () => "ep-1", }); const service = createWebhookService({ endpointStore: store, deadLetterSink: sink, maxAttempts: 5, initialRetryDelayMs: 1, maxRetryDelayMs: 30, idempotencyGuard: null, logger: noopLogger(), clock: () => 0, fetch: globalThis.fetch.bind(globalThis), }); await service.register({ tenantId: null, url: receiver.url, secret: "shared-secret", events: ["evt"], }); await expect( service.send("ep-1", "evt", { x: 1 }) ).rejects.toMatchObject({ webhookCode: WEBHOOK_CODES.DELIVERY_EXHAUSTED, }); expect(attempts).toBe(5); const dl = await sink.list({ endpointId: "ep-1" }); expect(dl).toHaveLength(1); const entry = dl[0]; if (!entry) throw new Error("missing dl entry"); expect(entry.event).toBe("evt"); expect(entry.endpointId).toBe("ep-1"); expect(entry.payload.event).toBe("evt"); } finally { await receiver.close(); } }); });