98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
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<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`,
|
|
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 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();
|
|
}
|
|
});
|
|
});
|