This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:22 +07:00
parent 3d6b0e0d4e
commit 5386bc51d1
65 changed files with 4058 additions and 161 deletions
@@ -0,0 +1,89 @@
import { HttpClient } from './http-client';
/**
* Regression tests for two bugs that only manifested in a browser, and so
* survived every server-side and curl-based check.
*/
describe('HttpClient', () => {
const okResponse = () =>
new Response(JSON.stringify({ success: true, data: { ok: true }, meta: {} }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
it('calls the default fetch with the global receiver, not the client instance', async () => {
// Browsers require `fetch` to be invoked with Window as `this`. Storing
// `globalThis.fetch` on the instance and calling `this.fetchImpl(...)`
// passes the HttpClient as the receiver and throws "Illegal invocation" —
// in the browser only. Node does not care, which is precisely why every
// server-side check and every curl passed while the browser was broken.
const original = globalThis.fetch;
const receivers: unknown[] = [];
globalThis.fetch = function (this: unknown) {
// Pushed rather than assigned to a local: capturing the receiver is the
// point of the test, and a plain alias trips `no-this-alias`.
receivers.push(this);
return Promise.resolve(okResponse());
} as unknown as typeof fetch;
try {
// No fetchImpl — exercise the default path, which is the one that broke.
const client = new HttpClient({ baseUrl: 'http://api.test' });
await client.get('/thing');
} finally {
globalThis.fetch = original;
}
expect(receivers).toHaveLength(1);
expect(receivers[0]).toBe(globalThis);
expect(receivers[0]).not.toBeInstanceOf(HttpClient);
});
it('does not retry a request that opted out, so refresh cannot recurse', async () => {
// `onUnauthorized` refreshes by calling the refresh endpoint. If that call
// is itself retryable, its own 401 triggers another refresh — an unbounded
// loop that hammers the API from the browser.
let calls = 0;
let refreshes = 0;
const fetchImpl = (() => {
calls += 1;
return Promise.resolve(new Response('{}', { status: 401 }));
}) as unknown as typeof fetch;
const client = new HttpClient({
baseUrl: 'http://api.test',
fetchImpl,
onUnauthorized: () => {
refreshes += 1;
return false;
},
});
await expect(
client.post('/auth/refresh', undefined, { skipAuthRetry: true }),
).rejects.toThrow();
expect(calls).toBe(1);
expect(refreshes).toBe(0);
});
it('still offers one retry for ordinary requests', async () => {
let calls = 0;
const fetchImpl = (() => {
calls += 1;
return Promise.resolve(calls === 1 ? new Response('{}', { status: 401 }) : okResponse());
}) as unknown as typeof fetch;
const client = new HttpClient({
baseUrl: 'http://api.test',
fetchImpl,
onUnauthorized: () => true,
});
await expect(client.get('/protected')).resolves.toEqual({ ok: true });
expect(calls).toBe(2);
});
});