Files
suaveplan/packages/persistence/storage/storage-http/src/fetch-adapter/fetch-adapter.test.ts
2026-05-27 23:47:53 +01:00

176 lines
5.5 KiB
TypeScript

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);
});
});