114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
const originalTimeoutEnv = process.env.FETCH_TIMEOUT_MS;
|
|
|
|
async function loadFetchTimeoutModule(tag) {
|
|
return import(`../../src/shared/utils/fetchTimeout.ts?case=${tag}-${Date.now()}`);
|
|
}
|
|
|
|
test.afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
if (originalTimeoutEnv === undefined) {
|
|
delete process.env.FETCH_TIMEOUT_MS;
|
|
} else {
|
|
process.env.FETCH_TIMEOUT_MS = originalTimeoutEnv;
|
|
}
|
|
});
|
|
|
|
test("fetchWithTimeout forwards options and exposes the configured timeout", async () => {
|
|
process.env.FETCH_TIMEOUT_MS = "3210";
|
|
const mod = await loadFetchTimeoutModule("configured-timeout");
|
|
|
|
let seenUrl = null;
|
|
let seenOptions = null;
|
|
const expectedResponse = { ok: true, status: 204 };
|
|
|
|
globalThis.fetch = async (url, options) => {
|
|
seenUrl = url;
|
|
seenOptions = options;
|
|
return expectedResponse;
|
|
};
|
|
|
|
const response = await mod.fetchWithTimeout("https://example.test/ping", {
|
|
method: "POST",
|
|
headers: { "x-test": "1" },
|
|
});
|
|
|
|
assert.equal(mod.getConfiguredTimeout(), 3210);
|
|
assert.equal(response, expectedResponse);
|
|
assert.equal(seenUrl, "https://example.test/ping");
|
|
assert.equal(seenOptions.method, "POST");
|
|
assert.equal(seenOptions.headers["x-test"], "1");
|
|
assert.equal(seenOptions.signal instanceof AbortSignal, true);
|
|
});
|
|
|
|
test("fetchWithTimeout converts both pre-aborted and externally aborted requests into FetchTimeoutError", async () => {
|
|
const mod = await loadFetchTimeoutModule("abort");
|
|
|
|
const preAborted = new AbortController();
|
|
preAborted.abort();
|
|
|
|
globalThis.fetch = async (_url, options) => {
|
|
assert.equal(options.signal.aborted, true);
|
|
const error = new Error("aborted");
|
|
error.name = "AbortError";
|
|
throw error;
|
|
};
|
|
|
|
await assert.rejects(
|
|
mod.fetchWithTimeout("https://example.test/pre-aborted", {
|
|
signal: preAborted.signal,
|
|
timeoutMs: 9,
|
|
}),
|
|
(error) => {
|
|
assert.equal(error instanceof mod.FetchTimeoutError, true);
|
|
assert.equal((error as any).timeoutMs, 9);
|
|
(assert as any).equal((error as any).url, "https://example.test/pre-aborted");
|
|
(assert as any).match((error as any).message, /timed out after 9ms/);
|
|
return true;
|
|
}
|
|
);
|
|
|
|
const external = new AbortController();
|
|
let fetchSawSignal = false;
|
|
|
|
globalThis.fetch = async (_url, options) => {
|
|
fetchSawSignal = true;
|
|
setTimeout(() => external.abort(), 0);
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
const error = new Error("aborted later");
|
|
error.name = "AbortError";
|
|
throw error;
|
|
};
|
|
|
|
await assert.rejects(
|
|
mod.fetchWithTimeout("https://example.test/external-abort", {
|
|
signal: external.signal,
|
|
timeoutMs: 15,
|
|
}),
|
|
(error) => {
|
|
assert.equal(fetchSawSignal, true);
|
|
assert.equal(error instanceof mod.FetchTimeoutError, true);
|
|
assert.equal((error as any).timeoutMs, 15);
|
|
assert.equal((error as any).url, "https://example.test/external-abort");
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test("fetchWithTimeout rethrows non-timeout failures unchanged", async () => {
|
|
const mod = await loadFetchTimeoutModule("generic-error");
|
|
const failure = new Error("network down");
|
|
|
|
globalThis.fetch = async () => {
|
|
throw failure;
|
|
};
|
|
|
|
await assert.rejects(
|
|
mod.fetchWithTimeout("https://example.test/fail", { timeoutMs: 5 }),
|
|
(error) => error === failure
|
|
);
|
|
});
|