import { describe, expect, it, vi } from "@suaveplan/testing/runner"; import { STORAGE_HTTP_CODES, StorageHttpError, } from "../error-codes/error-codes.js"; import { defaultFetch, isAbortError } from "./fetch-adapter.js"; describe("defaultFetch", () => { it("forwards a string URL to globalThis.fetch", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockResolvedValueOnce(new Response(new Uint8Array([1]))); try { const fetchFn = defaultFetch(); const response = await fetchFn("https://example.com/x"); expect(response.ok).toBe(true); expect(spy).toHaveBeenCalledTimes(1); } finally { spy.mockRestore(); } }); it("maps network rejection to STORAGE_HTTP.NETWORK_ERROR with URL preserved", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce(new Error("ECONNREFUSED")); try { const fetchFn = defaultFetch(); await fetchFn("https://example.com/x"); expect.fail("expected throw"); } catch (err) { expect(err).toBeInstanceOf(StorageHttpError); expect((err as StorageHttpError).code).toBe( STORAGE_HTTP_CODES.NETWORK_ERROR ); expect((err as StorageHttpError).url).toBe("https://example.com/x"); } finally { spy.mockRestore(); } }); it("re-raises AbortError unchanged", { timeout: 5000 }, async () => { const abort = Object.assign(new Error("aborted"), { name: "AbortError", }); const spy = vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(abort); try { const fetchFn = defaultFetch(); await fetchFn("https://example.com/x"); expect.fail("expected throw"); } catch (err) { expect(err).toBe(abort); } finally { spy.mockRestore(); } }); it("resolves URL from a URL object", { timeout: 5000 }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce(new Error("boom")); try { const fetchFn = defaultFetch(); try { await fetchFn(new URL("https://example.com/y")); } catch (err) { expect((err as StorageHttpError).url).toBe( "https://example.com/y" ); } } finally { spy.mockRestore(); } }); it("resolves URL from a Request-like input", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce(new Error("boom")); try { const fetchFn = defaultFetch(); try { await fetchFn({ url: "https://example.com/z" } as Request); } catch (err) { expect((err as StorageHttpError).url).toBe( "https://example.com/z" ); } } finally { spy.mockRestore(); } }); it("falls back to no URL when input is unrecognised shape", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce(new Error("boom")); try { const fetchFn = defaultFetch(); try { await fetchFn({} as Request); } catch (err) { expect((err as StorageHttpError).url).toBeUndefined(); } } finally { spy.mockRestore(); } }); it("uses String(err) when the reject value is not an Error", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce("boom-string"); try { const fetchFn = defaultFetch(); try { await fetchFn("https://example.com/x"); } catch (err) { expect((err as StorageHttpError).message).toContain( "boom-string" ); expect((err as StorageHttpError).cause).toBeUndefined(); } } finally { spy.mockRestore(); } }); it("maps non-Error rejection on an input with no URL", { timeout: 5000, }, async () => { const spy = vi .spyOn(globalThis, "fetch") .mockRejectedValueOnce("boom-no-url"); try { const fetchFn = defaultFetch(); try { await fetchFn({} as Request); } catch (err) { expect((err as StorageHttpError).url).toBeUndefined(); expect((err as StorageHttpError).cause).toBeUndefined(); } } finally { spy.mockRestore(); } }); }); describe("isAbortError", () => { it("returns true for AbortError-shaped objects", { timeout: 5000 }, () => { expect(isAbortError({ name: "AbortError" })).toBe(true); }); it("returns false for null and primitives", { timeout: 5000 }, () => { expect(isAbortError(null)).toBe(false); expect(isAbortError("x")).toBe(false); expect(isAbortError(42)).toBe(false); }); it("returns false for Errors with a different name", { timeout: 5000, }, () => { expect(isAbortError(new Error("boom"))).toBe(false); }); });