Initial Commit
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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,
|
||||
attempt: number
|
||||
) => void
|
||||
) {
|
||||
let attempt = 0;
|
||||
const server: Server = createServer((req, res) => {
|
||||
req.on("data", () => undefined);
|
||||
req.on("end", () => {
|
||||
attempt += 1;
|
||||
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");
|
||||
}
|
||||
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 retry", () => {
|
||||
it("retries 5xx then succeeds", { timeout: 30_000 }, async () => {
|
||||
const receiver = await startReceiver((_, res, attempt) => {
|
||||
if (attempt < 3) {
|
||||
res.writeHead(500);
|
||||
res.end("transient");
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
res.end("ok");
|
||||
}
|
||||
});
|
||||
try {
|
||||
resetWebhookInstruments();
|
||||
const store = new MemoryEndpointStore({
|
||||
clock: () => 0,
|
||||
idFactory: () => "ep-1",
|
||||
});
|
||||
const service = createWebhookService({
|
||||
endpointStore: store,
|
||||
deadLetterSink: new MemoryDeadLetterSink(),
|
||||
maxAttempts: 5,
|
||||
initialRetryDelayMs: 1,
|
||||
maxRetryDelayMs: 50,
|
||||
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", {
|
||||
x: 1,
|
||||
});
|
||||
expect(result.status).toBe("delivered");
|
||||
expect(result.attempts.length).toBe(3);
|
||||
expect(result.finalStatusCode).toBe(200);
|
||||
} finally {
|
||||
await receiver.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("4xx terminates without retry", { timeout: 30_000 }, async () => {
|
||||
let attempts = 0;
|
||||
const receiver = await startReceiver((_, res) => {
|
||||
attempts++;
|
||||
res.writeHead(422);
|
||||
res.end("client error");
|
||||
});
|
||||
try {
|
||||
resetWebhookInstruments();
|
||||
const store = new MemoryEndpointStore({
|
||||
clock: () => 0,
|
||||
idFactory: () => "ep-1",
|
||||
});
|
||||
const service = createWebhookService({
|
||||
endpointStore: store,
|
||||
deadLetterSink: new MemoryDeadLetterSink(),
|
||||
maxAttempts: 5,
|
||||
initialRetryDelayMs: 1,
|
||||
maxRetryDelayMs: 50,
|
||||
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", {})).rejects.toMatchObject(
|
||||
{
|
||||
webhookCode: WEBHOOK_CODES.DELIVERY_EXHAUSTED,
|
||||
}
|
||||
);
|
||||
expect(attempts).toBe(1);
|
||||
} finally {
|
||||
await receiver.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user