125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
import { fetchHtml } from "../src/utils/http";
|
|
|
|
describe("fetchHtml", () => {
|
|
const originalFetch = global.fetch;
|
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
const originalClearTimeout = globalThis.clearTimeout;
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
process.env.NODE_ENV = originalNodeEnv;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
globalThis.clearTimeout = originalClearTimeout;
|
|
});
|
|
|
|
test("does not schedule throttle timers during tests", async () => {
|
|
process.env.NODE_ENV = "test";
|
|
const scheduledDelays: number[] = [];
|
|
|
|
global.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
headers: { get: () => null },
|
|
text: () => Promise.resolve("<html></html>"),
|
|
}),
|
|
) as unknown as typeof fetch;
|
|
globalThis.setTimeout = mock((handler: TimerHandler, timeout?: number) => {
|
|
scheduledDelays.push(Number(timeout));
|
|
if (timeout !== 30_000 && typeof handler === "function") {
|
|
handler();
|
|
}
|
|
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
}) as unknown as typeof setTimeout;
|
|
globalThis.clearTimeout = mock(() => {}) as unknown as typeof clearTimeout;
|
|
|
|
await fetchHtml("https://example.com", 1000, { timeoutMs: 30_000 });
|
|
|
|
expect(scheduledDelays).not.toContain(1000);
|
|
});
|
|
|
|
test("fetchHtml returns responseUrl when includeResponseUrl is true", async () => {
|
|
process.env.NODE_ENV = "test";
|
|
global.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
url: "https://example.test/final",
|
|
headers: { get: () => null },
|
|
text: () => Promise.resolve("<html></html>"),
|
|
}),
|
|
) as unknown as typeof fetch;
|
|
|
|
const result = await fetchHtml("https://example.test", 0, {
|
|
includeResponseUrl: true,
|
|
});
|
|
expect(result.html).toBe("<html></html>");
|
|
expect(result.responseUrl).toBe("https://example.test/final");
|
|
});
|
|
|
|
test("rate limit epoch reset uses bounded wait", async () => {
|
|
process.env.NODE_ENV = "production";
|
|
const scheduledDelays: number[] = [];
|
|
const farFutureEpochSeconds = Math.floor(Date.now() / 1000) + 315_360_000;
|
|
let calls = 0;
|
|
|
|
global.fetch = mock(() => {
|
|
calls += 1;
|
|
return Promise.resolve({
|
|
ok: calls > 1,
|
|
status: calls > 1 ? 200 : 429,
|
|
url: "https://example.test",
|
|
headers: {
|
|
get: (name: string) =>
|
|
name === "X-RateLimit-Reset" ? String(farFutureEpochSeconds) : null,
|
|
},
|
|
text: () => Promise.resolve("<html></html>"),
|
|
});
|
|
}) as unknown as typeof fetch;
|
|
globalThis.setTimeout = mock((handler: TimerHandler, timeout?: number) => {
|
|
scheduledDelays.push(Number(timeout));
|
|
if (timeout !== 1_234_567 && typeof handler === "function") {
|
|
handler();
|
|
}
|
|
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
}) as unknown as typeof setTimeout;
|
|
globalThis.clearTimeout = mock(() => {}) as unknown as typeof clearTimeout;
|
|
|
|
await fetchHtml("https://example.test", 0, {
|
|
maxRetries: 1,
|
|
timeoutMs: 1_234_567,
|
|
});
|
|
|
|
expect(scheduledDelays).toContain(30_000);
|
|
expect(scheduledDelays).not.toContain(farFutureEpochSeconds * 1000);
|
|
});
|
|
|
|
test("custom Accept header overrides default accept without duplicate casing", async () => {
|
|
process.env.NODE_ENV = "test";
|
|
const customAccept = "text/plain";
|
|
let requestHeaders: HeadersInit | undefined;
|
|
|
|
global.fetch = mock((_url: string | URL | Request, init?: RequestInit) => {
|
|
requestHeaders = init?.headers;
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
url: "https://example.test",
|
|
headers: { get: () => null },
|
|
text: () => Promise.resolve("<html></html>"),
|
|
});
|
|
}) as unknown as typeof fetch;
|
|
|
|
await fetchHtml("https://example.test", 0, {
|
|
headers: { Accept: customAccept },
|
|
});
|
|
|
|
expect(requestHeaders).toBeDefined();
|
|
expect((requestHeaders as Record<string, string>).accept).toBe(
|
|
customAccept,
|
|
);
|
|
expect((requestHeaders as Record<string, string>).Accept).toBeUndefined();
|
|
});
|
|
});
|