import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { NetworkErrorTypes } from "../../../../errors"; import { RequestError } from "../../../../utils/request"; import TaskCanceller from "../../../../utils/task_canceller"; import { scheduleRequestWithCdns, scheduleRequestPromise } from "../schedule_request"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ const { mockLog, mockCancellableSleep, mockGetFuzzedDelay, mockGetTimestamp } = vi.hoisted(() => { return { mockLog: { warn: vi.fn() }, mockCancellableSleep: vi.fn(), mockGetFuzzedDelay: vi.fn((delay: number) => delay), mockGetTimestamp: vi.fn(() => 0), }; }); vi.mock("../../../log", () => ({ default: mockLog })); vi.mock("../../../utils/cancellable_sleep", () => ({ default: mockCancellableSleep })); vi.mock("../../../utils/get_fuzzed_delay", () => ({ default: mockGetFuzzedDelay })); vi.mock("../../../utils/monotonic_timestamp", () => ({ default: mockGetTimestamp })); const defaultOptions = { baseDelay: 100, maxDelay: 2000, maxRetry: 3, onRetry: vi.fn(), }; function makeCdn(id: string): any { return { baseUrl: id }; } beforeEach(() => { mockGetTimestamp.mockReturnValue(0); mockGetFuzzedDelay.mockImplementation((delay: number) => delay); // By default cancellableSleep resolves immediately mockCancellableSleep.mockResolvedValue(undefined); }); afterEach(() => { vi.resetAllMocks(); }); describe.skip("scheduleRequestWithCdns", () => { it("resolves with the result of a successful request", async () => { const cdn = makeCdn("https://cdn1.example.com"); const performRequest = vi.fn().mockResolvedValue("ok"); const result = await scheduleRequestWithCdns( [cdn], null, performRequest, defaultOptions, new TaskCanceller("test").signal, ); expect(result).toBe("ok"); expect(performRequest).toHaveBeenCalledTimes(1); expect(performRequest).toHaveBeenCalledWith(cdn, expect.anything()); }); it("passes null cdn when cdns param is null", async () => { const performRequest = vi.fn().mockResolvedValue("data"); await scheduleRequestWithCdns( null, null, performRequest, defaultOptions, new TaskCanceller("test").signal, ); expect(performRequest).toHaveBeenCalledWith(null, expect.anything()); }); it("throws immediately if cancellationSignal is already cancelled", async () => { const cancellationError = new Error("cancelled"); const signal: any = { cancellationError, isCancelled: () => true, }; await expect( scheduleRequestWithCdns([makeCdn("a")], null, vi.fn(), defaultOptions, signal), ).rejects.toThrow("cancelled"); }); it("throws 'No CDN to request' when given an empty CDN array", async () => { await expect( scheduleRequestWithCdns( [], null, vi.fn(), defaultOptions, new TaskCanceller("test").signal, ), ).rejects.toThrow("No CDN to request"); expect(mockLog.warn).toHaveBeenCalled(); }); it("retries on a 500 HTTP error and resolves on second attempt", async () => { const cdn = makeCdn("https://cdn1.example.com"); const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi .fn() .mockRejectedValueOnce(error) .mockResolvedValueOnce("recovered"); const onRetry = vi.fn(); const result = await scheduleRequestWithCdns( [cdn], null, performRequest, { ...defaultOptions, onRetry }, new TaskCanceller("test").signal, ); expect(result).toBe("recovered"); expect(onRetry).toHaveBeenCalledTimes(1); expect(onRetry).toHaveBeenCalledWith(error); }); it("retries on TIMEOUT error", async () => { const cdn = makeCdn("https://cdn1.example.com"); const error = new RequestError("url", 0, NetworkErrorTypes.TIMEOUT); const performRequest = vi .fn() .mockRejectedValueOnce(error) .mockResolvedValueOnce("ok"); const result = await scheduleRequestWithCdns( [cdn], null, performRequest, defaultOptions, new TaskCanceller("test").signal, ); expect(result).toBe("ok"); }); it("does NOT retry on a 403 HTTP error and rejects immediately", async () => { const cdn = makeCdn("https://cdn1.example.com"); const error = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi.fn().mockRejectedValue(error); const onRetry = vi.fn(); await expect( scheduleRequestWithCdns( [cdn], null, performRequest, { ...defaultOptions, maxRetry: 5, onRetry }, new TaskCanceller("test").signal, ), ).rejects.toThrow(); // 403 should not be retried — performRequest called once only expect(performRequest).toHaveBeenCalledTimes(1); expect(onRetry).not.toHaveBeenCalled(); }); it("blacklists a CDN after maxRetry exhaustion and rejects", async () => { const cdn = makeCdn("https://cdn1.example.com"); const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi.fn().mockRejectedValue(error); const onRetry = vi.fn(); const maxRetry = 2; await expect( scheduleRequestWithCdns( [cdn], null, performRequest, { ...defaultOptions, maxRetry, onRetry }, new TaskCanceller("test").signal, ), ).rejects.toThrow(); // initial attempt + maxRetry retries = maxRetry+1 calls expect(performRequest).toHaveBeenCalledTimes(maxRetry + 1); }); it("falls over to next CDN when first CDN fails with a non-retryable error", async () => { const cdn1 = makeCdn("https://cdn1.example.com"); const cdn2 = makeCdn("https://cdn2.example.com"); // 403 is non-retryable → should move to cdn2 const error403 = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi .fn() .mockRejectedValueOnce(error403) .mockResolvedValueOnce("cdn2-response"); const result = await scheduleRequestWithCdns( [cdn1, cdn2], null, performRequest, defaultOptions, new TaskCanceller("test").signal, ); expect(result).toBe("cdn2-response"); expect(performRequest).toHaveBeenNthCalledWith(1, cdn1, expect.anything()); expect(performRequest).toHaveBeenNthCalledWith(2, cdn2, expect.anything()); }); it("rejects with the last error when all CDNs are exhausted", async () => { const cdn1 = makeCdn("cdn1"); const cdn2 = makeCdn("cdn2"); const error1 = new RequestError("url1", 403, NetworkErrorTypes.ERROR_HTTP_CODE); const error2 = new RequestError("url2", 403, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi .fn() .mockRejectedValueOnce(error1) .mockRejectedValueOnce(error2); await expect( scheduleRequestWithCdns( [cdn1, cdn2], null, performRequest, defaultOptions, new TaskCanceller("test").signal, ), ).rejects.toThrow(); expect(performRequest).toHaveBeenCalledTimes(2); }); it("downgrades CDN in prioritizer after failure", async () => { const cdn = makeCdn("cdn1"); const error = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE); const cdnPrioritizer = { getCdnPreferenceForResource: vi.fn().mockReturnValue([cdn]), downgradeCdn: vi.fn(), addEventListener: vi.fn(), }; const performRequest = vi.fn().mockRejectedValue(error); await expect( scheduleRequestWithCdns( [cdn], cdnPrioritizer as never, performRequest, defaultOptions, new TaskCanceller("test").signal, ), ).rejects.toThrow(); expect(cdnPrioritizer.downgradeCdn).toHaveBeenCalledWith(cdn); }); it("retries on INTEGRITY_ERROR", async () => { const cdn = makeCdn("cdn1"); const integrityError = { code: "INTEGRITY_ERROR" }; const performRequest = vi .fn() .mockRejectedValueOnce(integrityError) .mockResolvedValueOnce("ok"); const result = await scheduleRequestWithCdns( [cdn], null, performRequest, { ...defaultOptions, maxRetry: 2 }, new TaskCanceller("test").signal, ); expect(result).toBe("ok"); }); it("computes exponential backoff delay capped at maxDelay", async () => { const cdn = makeCdn("cdn1"); const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE); // Fail twice then succeed const performRequest = vi .fn() .mockRejectedValueOnce(error) .mockRejectedValueOnce(error) .mockResolvedValueOnce("done"); const baseDelay = 100; const maxDelay = 150; await scheduleRequestWithCdns( [cdn], null, performRequest, { baseDelay, maxDelay, maxRetry: 5, onRetry: vi.fn() }, new TaskCanceller("test").signal, ); // After 1st failure: delay = min(100 * 2^0, 150) = 100 // After 2nd failure: delay = min(100 * 2^1, 150) = 150 expect(mockGetFuzzedDelay).toHaveBeenNthCalledWith(1, 100); expect(mockGetFuzzedDelay).toHaveBeenNthCalledWith(2, 150); }); }); describe("scheduleRequestPromise", () => { it("resolves with the performRequest result", async () => { const performRequest = vi.fn().mockResolvedValue("result"); const result = await scheduleRequestPromise( performRequest, defaultOptions, new TaskCanceller("test").signal, ); expect(result).toBe("result"); }); it("retries and resolves after transient failure", async () => { const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi .fn() .mockRejectedValueOnce(error) .mockResolvedValueOnce("ok"); const result = await scheduleRequestPromise( performRequest, { ...defaultOptions, maxRetry: 2 }, new TaskCanceller("test").signal, ); expect(result).toBe("ok"); }); it("rejects after all retries are exhausted", async () => { const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE); const performRequest = vi.fn().mockRejectedValue(error); await expect( scheduleRequestPromise( performRequest, { ...defaultOptions, maxRetry: 1 }, new TaskCanceller("test").signal, ), ).rejects.toThrow(); // 1 initial + 1 retry = 2 total expect(performRequest).toHaveBeenCalledTimes(2); }); });