mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-08 13:37:29 +01:00
8ab7d354e6
* Adding further console logging but also fixed lines with JSON.stringify(error) * Added retry mechanism for GETs for armRequest workflow * Moved stringifyError into its own file as it was causing strict compile issues in ErrorHandlingUtils
137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import { fetchWithTimeout, tryFetchWithTimeout } from "./FetchWithTimeout";
|
|
|
|
describe("fetchWithTimeout", () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it("forwards init and resolves with the fetch response", async () => {
|
|
const fakeResponse = { ok: true } as Response;
|
|
const fetchMock = jest.fn().mockResolvedValue(fakeResponse);
|
|
global.fetch = fetchMock as unknown as typeof global.fetch;
|
|
|
|
const result = await fetchWithTimeout("https://example.com", { method: "GET" }, 1000);
|
|
|
|
expect(result).toBe(fakeResponse);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
expect(url).toBe("https://example.com");
|
|
expect((init as RequestInit).method).toBe("GET");
|
|
expect((init as RequestInit).signal).toBeDefined();
|
|
});
|
|
|
|
it("aborts the fetch when the internal timeout fires", async () => {
|
|
jest.useFakeTimers();
|
|
let receivedSignal: AbortSignal | undefined;
|
|
global.fetch = jest.fn().mockImplementation((_url: string, init: RequestInit) => {
|
|
receivedSignal = init.signal ?? undefined;
|
|
return new Promise<Response>((_resolve, reject) => {
|
|
init.signal?.addEventListener("abort", () => {
|
|
const err = new Error("aborted");
|
|
err.name = "AbortError";
|
|
reject(err);
|
|
});
|
|
});
|
|
}) as unknown as typeof global.fetch;
|
|
|
|
const pending = fetchWithTimeout("https://example.com", {}, 50);
|
|
jest.advanceTimersByTime(50);
|
|
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
|
|
expect(receivedSignal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("throws immediately when the external signal is already aborted", async () => {
|
|
const fetchMock = jest.fn();
|
|
global.fetch = fetchMock as unknown as typeof global.fetch;
|
|
|
|
const controller = new AbortController();
|
|
const reason = new Error("user-cancelled");
|
|
controller.abort(reason);
|
|
|
|
await expect(fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000)).rejects.toBe(reason);
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("aborts mid-fetch when the external signal aborts and propagates the reason", async () => {
|
|
const reason = new Error("user-cancelled");
|
|
let internalSignal: AbortSignal | undefined;
|
|
global.fetch = jest.fn().mockImplementation((_url: string, init: RequestInit) => {
|
|
internalSignal = init.signal ?? undefined;
|
|
return new Promise<Response>((_resolve, reject) => {
|
|
init.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")));
|
|
});
|
|
}) as unknown as typeof global.fetch;
|
|
|
|
const controller = new AbortController();
|
|
const pending = fetchWithTimeout("https://example.com", { signal: controller.signal }, 60000);
|
|
// Let the fetch wire up its abort listener before triggering.
|
|
await Promise.resolve();
|
|
controller.abort(reason);
|
|
|
|
await expect(pending).rejects.toBe(reason);
|
|
expect(internalSignal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("skips the timer when timeoutMs is Infinity", async () => {
|
|
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
|
|
global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response) as unknown as typeof global.fetch;
|
|
|
|
await fetchWithTimeout("https://example.com", {}, Infinity);
|
|
|
|
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
setTimeoutSpy.mockRestore();
|
|
});
|
|
|
|
it("cleans up the timer and external listener after a successful fetch", async () => {
|
|
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
|
|
global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response) as unknown as typeof global.fetch;
|
|
|
|
const controller = new AbortController();
|
|
const removeListenerSpy = jest.spyOn(controller.signal, "removeEventListener");
|
|
|
|
await fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000);
|
|
|
|
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
expect(removeListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function));
|
|
clearTimeoutSpy.mockRestore();
|
|
});
|
|
|
|
it("cleans up the timer and listener when the fetch rejects", async () => {
|
|
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
|
|
const networkError = new Error("network down");
|
|
global.fetch = jest.fn().mockRejectedValue(networkError) as unknown as typeof global.fetch;
|
|
|
|
const controller = new AbortController();
|
|
const removeListenerSpy = jest.spyOn(controller.signal, "removeEventListener");
|
|
|
|
await expect(fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000)).rejects.toBe(
|
|
networkError,
|
|
);
|
|
|
|
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
expect(removeListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function));
|
|
clearTimeoutSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("tryFetchWithTimeout", () => {
|
|
const originalFetch = global.fetch;
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
it("returns null on fetch failure", async () => {
|
|
global.fetch = jest.fn().mockRejectedValue(new Error("boom")) as unknown as typeof global.fetch;
|
|
await expect(tryFetchWithTimeout("https://example.com")).resolves.toBeNull();
|
|
});
|
|
|
|
it("returns the response on success", async () => {
|
|
const response = { ok: true } as Response;
|
|
global.fetch = jest.fn().mockResolvedValue(response) as unknown as typeof global.fetch;
|
|
await expect(tryFetchWithTimeout("https://example.com")).resolves.toBe(response);
|
|
});
|
|
});
|