Files
ToolsPCastApi-TS/test/unit/net/http/HttpRequests.test.ts

214 lines
7.1 KiB
TypeScript

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<Response>) => {
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<typeof responseData>(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<string>(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');
}
});
});
});