113 lines
4.4 KiB
TypeScript
113 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
|
|
import { AdminWebhookInboxError, requestWebhookReplay, type AdminWebhookInboxDeps } from "./webhook-inbox.server";
|
|
|
|
function makeAttempt(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: "attempt_old",
|
|
endpoint: "https://example.test/webhook",
|
|
success: false,
|
|
status: 500,
|
|
eventId: "event_1",
|
|
event: {
|
|
id: "event_1",
|
|
name: "billing.mollie.webhook.failed",
|
|
data: JSON.stringify({ paymentId: "tr_123", status: "failed" }),
|
|
resource: "billing",
|
|
tenantId: "tenant_1",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeDeps(attempt = makeAttempt(), queue: { add: ReturnType<typeof vi.fn>; close: ReturnType<typeof vi.fn> } | null = { add: vi.fn(async () => ({ id: "job_1" })), close: vi.fn(async () => {}) }) {
|
|
const recordAction = vi.fn(async () => {});
|
|
const createdAttempts: unknown[] = [];
|
|
const db = {
|
|
eventWebhookAttempt: {
|
|
findUnique: vi.fn(async () => attempt),
|
|
create: vi.fn(async (args) => {
|
|
createdAttempts.push(args.data);
|
|
return { id: "attempt_replay" };
|
|
}),
|
|
update: vi.fn(async () => ({})),
|
|
},
|
|
};
|
|
const deps = {
|
|
db,
|
|
now: vi.fn(() => new Date("2026-07-09T10:00:00.000Z")),
|
|
createQueue: vi.fn(async () => queue),
|
|
enqueue: vi.fn(async (targetQueue, descriptor, options) => {
|
|
await targetQueue.add(descriptor.jobName, descriptor.payload, {});
|
|
return { id: options?.idempotencyKey ?? "job_1" };
|
|
}),
|
|
recordAction,
|
|
} as unknown as AdminWebhookInboxDeps;
|
|
return { deps, db, queue, recordAction, createdAttempts };
|
|
}
|
|
|
|
describe("requestWebhookReplay", () => {
|
|
it("creates a fresh attempt, enqueues webhook delivery, and audits the replay request", async () => {
|
|
const { deps, db, queue, recordAction, createdAttempts } = makeDeps();
|
|
db.eventWebhookAttempt.create.mockImplementationOnce(async (args) => {
|
|
expect(getRlsExecutionContext()).toMatchObject({ tenantId: "tenant_1" });
|
|
createdAttempts.push(args.data);
|
|
return { id: "attempt_replay" };
|
|
});
|
|
|
|
await expect(
|
|
requestWebhookReplay({
|
|
attemptId: "attempt_old",
|
|
reason: "Provider Timeout erneut ausliefern",
|
|
actorUserId: "admin_1",
|
|
actorEmail: "admin@hivegrid.de",
|
|
}, deps)
|
|
).resolves.toEqual({ replayAttemptId: "attempt_replay" });
|
|
|
|
expect(db.eventWebhookAttempt.findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "attempt_old" } }));
|
|
expect(createdAttempts[0]).toMatchObject({
|
|
eventId: "event_1",
|
|
endpoint: "https://example.test/webhook",
|
|
message: "admin.webhooks.messages.replayRequested",
|
|
});
|
|
expect(deps.enqueue).toHaveBeenCalledWith(
|
|
queue,
|
|
expect.objectContaining({
|
|
jobName: "deliver-webhook",
|
|
payload: expect.objectContaining({
|
|
url: "https://example.test/webhook",
|
|
tenantId: "tenant_1",
|
|
body: { paymentId: "tr_123", status: "failed" },
|
|
metadata: expect.objectContaining({ legacyAttemptId: "attempt_replay", adminReplayOfAttemptId: "attempt_old" }),
|
|
}),
|
|
}),
|
|
expect.objectContaining({ idempotencyKey: "attempt_replay", tenantId: "tenant_1" })
|
|
);
|
|
expect(queue?.close).toHaveBeenCalledOnce();
|
|
expect(recordAction).toHaveBeenCalledWith(expect.objectContaining({ action: "webhook.replayRequested", targetId: "attempt_old" }));
|
|
});
|
|
|
|
it("rejects delivered attempts so operators cannot duplicate successful webhooks", async () => {
|
|
const { deps } = makeDeps(makeAttempt({ success: true, status: 204 }));
|
|
|
|
await expect(
|
|
requestWebhookReplay({ attemptId: "attempt_old", reason: "Bitte erneut senden", actorUserId: "admin_1" }, deps)
|
|
).rejects.toMatchObject({ code: "REPLAY_NOT_ALLOWED" });
|
|
});
|
|
|
|
it("marks the fresh attempt failed when the queue is unavailable", async () => {
|
|
const { deps, db } = makeDeps(makeAttempt(), null);
|
|
|
|
await expect(
|
|
requestWebhookReplay({ attemptId: "attempt_old", reason: "Provider Timeout erneut ausliefern", actorUserId: "admin_1" }, deps)
|
|
).rejects.toBeInstanceOf(AdminWebhookInboxError);
|
|
|
|
expect(db.eventWebhookAttempt.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: "attempt_replay" },
|
|
data: expect.objectContaining({ success: false, status: 503, message: "admin.webhooks.messages.queueUnavailable" }),
|
|
})
|
|
);
|
|
});
|
|
});
|