Initial Commit
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user