import {describe, expect, it, beforeEach, afterEach, mock} from 'bun:test'; import {HttpRequests, HttpRequestError} from '../../../../src/net/http/HttpRequests'; import {HttpMethod} from '../../../../src/net/http/HttpMethod'; describe('HttpRequestError', () => { describe('when created with only a message', () => { it('should set the message and name properties', () => { const error = new HttpRequestError('Test error'); expect(error.message).toBe('Test error'); expect(error.name).toBe('HttpRequestError'); }); it('should leave optional properties undefined', () => { const error = new HttpRequestError('Test error'); expect(error.status).toBeUndefined(); expect(error.statusText).toBeUndefined(); expect(error.originalError).toBeUndefined(); }); }); describe('when created with all properties', () => { it('should store all provided values', () => { const originalError = new Error('Original'); const error = new HttpRequestError('Test error', 404, 'Not Found', originalError); expect(error.message).toBe('Test error'); expect(error.status).toBe(404); expect(error.statusText).toBe('Not Found'); expect(error.originalError).toBe(originalError); }); }); }); describe('HttpRequests', () => { let originalFetch: typeof globalThis.fetch; const createHttpRequests = (options: {requestTimeoutDuration?: number} = {}) => { const baseHeaders = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' }); return new HttpRequests('https://api.example.com', baseHeaders, options); }; const mockFetch = (response: Partial) => { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: new Headers({'content-type': 'application/json'}), json: async () => ({}), text: async () => '', ...response } as Response; globalThis.fetch = mock(() => Promise.resolve(mockResponse)); return globalThis.fetch; }; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); describe('when making a GET request', () => { it('should return parsed JSON response on success', async () => { const responseData = {status: 'ok', data: 'test'}; mockFetch({json: async () => responseData}); const httpRequests = createHttpRequests(); const result = await httpRequests.request(HttpMethod.GET, '/test'); expect(result).toEqual(responseData); }); it('should strip any body from the request', async () => { const fetchMock = mockFetch({json: async () => ({})}); const httpRequests = createHttpRequests(); await httpRequests.request(HttpMethod.GET, '/test', {body: JSON.stringify({key: 'value'})}); const [, options] = (fetchMock as any).mock.calls[0]; expect(options.body).toBeUndefined(); }); it('should construct full URL from base URI and path', async () => { const fetchMock = mockFetch({json: async () => ({})}); const httpRequests = createHttpRequests(); await httpRequests.request(HttpMethod.GET, '/api/v1/resource'); const [url] = (fetchMock as any).mock.calls[0]; expect(url).toBe('https://api.example.com/api/v1/resource'); }); }); describe('when making a POST request', () => { it('should include the body in the request', async () => { const fetchMock = mockFetch({json: async () => ({status: 'ok'})}); const httpRequests = createHttpRequests(); const body = {key: 'value'}; await httpRequests.request(HttpMethod.POST, '/test', {body: JSON.stringify(body)}); const [, options] = (fetchMock as any).mock.calls[0]; expect(options.body).toBe(JSON.stringify(body)); }); it('should set the correct HTTP method', async () => { const fetchMock = mockFetch({json: async () => ({})}); const httpRequests = createHttpRequests(); await httpRequests.request(HttpMethod.POST, '/test', {body: '{}'}); const [, options] = (fetchMock as any).mock.calls[0]; expect(options.method).toBe('POST'); }); }); describe('when making a PUT request with an object body', () => { it('should stringify the body automatically', async () => { const fetchMock = mockFetch({json: async () => ({status: 'ok'})}); const httpRequests = createHttpRequests(); const body = {key: 'value', nested: {foo: 'bar'}}; await httpRequests.request(HttpMethod.PUT, '/test', {body: body as any}); const [, options] = (fetchMock as any).mock.calls[0]; expect(options.body).toBe(JSON.stringify(body)); }); }); describe('when the response content-type is text/plain', () => { it('should return text response instead of JSON', async () => { mockFetch({ headers: new Headers({'content-type': 'text/plain'}), text: async () => 'plain text response' }); const httpRequests = createHttpRequests(); const result = await httpRequests.request(HttpMethod.GET, '/test'); expect(result).toBe('plain text response'); }); }); describe('when the server returns a non-ok response', () => { it('should throw HttpRequestError with status details', async () => { mockFetch({ ok: false, status: 404, statusText: 'Not Found' }); const httpRequests = createHttpRequests(); await expect(httpRequests.request(HttpMethod.GET, '/test')).rejects.toThrow(HttpRequestError); }); it('should include status code and status text in error', async () => { mockFetch({ ok: false, status: 500, statusText: 'Internal Server Error' }); const httpRequests = createHttpRequests(); try { await httpRequests.request(HttpMethod.GET, '/test'); expect.unreachable('Should have thrown'); } catch (e) { expect(e).toBeInstanceOf(HttpRequestError); const error = e as HttpRequestError; expect(error.status).toBe(500); expect(error.statusText).toBe('Internal Server Error'); } }); }); describe('when a network error occurs', () => { it('should wrap the error in HttpRequestError', async () => { globalThis.fetch = mock(() => Promise.reject(new Error('Network error'))); const httpRequests = createHttpRequests(); await expect(httpRequests.request(HttpMethod.GET, '/test')).rejects.toThrow('Request failed: Network error'); }); }); describe('when the request times out', () => { it('should throw HttpRequestError with timeout message', async () => { const abortError = new Error('Aborted'); abortError.name = 'AbortError'; globalThis.fetch = mock(() => Promise.reject(abortError)); const httpRequests = createHttpRequests({requestTimeoutDuration: 100}); try { await httpRequests.request(HttpMethod.GET, '/test'); expect.unreachable('Should have thrown'); } catch (e) { expect(e).toBeInstanceOf(HttpRequestError); expect((e as HttpRequestError).message).toContain('Request timeout after'); } }); }); });