60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { createFetchHttpClient } from '../src/http/types.js';
|
|
|
|
type FetchImpl = typeof fetch;
|
|
|
|
function okResponse(body: string, status = 200): Response {
|
|
return new Response(body, { status, headers: { 'content-type': 'application/json' } });
|
|
}
|
|
|
|
describe('createFetchHttpClient', () => {
|
|
it('rejects a non-positive timeout at construction', () => {
|
|
expect(() => createFetchHttpClient({ timeoutMs: 0 })).toThrow(/timeoutMs/);
|
|
});
|
|
|
|
it('attaches an AbortSignal deadline to every request and parses JSON bodies', async () => {
|
|
const fetchImpl = vi.fn<FetchImpl>().mockResolvedValue(okResponse('{"arenaSessionId":"s1"}'));
|
|
const client = createFetchHttpClient({ timeoutMs: 30_000, fetchImpl });
|
|
|
|
const res = await client.request({ method: 'POST', url: 'https://arena.invalid/v1/login', body: { a: 1 } });
|
|
|
|
expect(res).toEqual({ status: 200, body: { arenaSessionId: 's1' } });
|
|
const init = fetchImpl.mock.calls[0]?.[1];
|
|
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
|
expect(init?.signal?.aborted).toBe(false);
|
|
expect(init?.method).toBe('POST');
|
|
expect(init?.body).toBe('{"a":1}');
|
|
expect((init?.headers as Record<string, string>)['Content-Type']).toBe('application/json');
|
|
});
|
|
|
|
it('rejects with a TimeoutError when the upstream does not answer within timeoutMs', async () => {
|
|
// A fetch that only settles when its signal aborts — i.e. a hung socket.
|
|
const fetchImpl: FetchImpl = (_url, init) =>
|
|
new Promise<Response>((_resolve, reject) => {
|
|
init?.signal?.addEventListener('abort', () => reject(init.signal?.reason as Error));
|
|
});
|
|
const client = createFetchHttpClient({ timeoutMs: 20, fetchImpl });
|
|
|
|
await expect(client.request({ method: 'GET', url: 'https://arena.invalid/v1/items' })).rejects.toMatchObject({
|
|
name: 'TimeoutError',
|
|
});
|
|
});
|
|
|
|
it('returns raw text when the body is not JSON and null when empty', async () => {
|
|
const fetchImpl = vi
|
|
.fn<FetchImpl>()
|
|
.mockResolvedValueOnce(okResponse('not json', 502))
|
|
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
const client = createFetchHttpClient({ timeoutMs: 1000, fetchImpl });
|
|
|
|
expect(await client.request({ method: 'GET', url: 'https://arena.invalid/x' })).toEqual({
|
|
status: 502,
|
|
body: 'not json',
|
|
});
|
|
expect(await client.request({ method: 'GET', url: 'https://arena.invalid/y' })).toEqual({
|
|
status: 204,
|
|
body: null,
|
|
});
|
|
});
|
|
});
|