214 lines
7.1 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|
|
});
|