@reggieofarrell/axios-retry-client
Version:
A class based api client for both the server and browser built on `axios` and `axios-retry`, written in TypeScript
258 lines (257 loc) • 11.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_retry_client_1 = require("./axios-retry-client");
const axios_mock_adapter_1 = __importDefault(require("axios-mock-adapter"));
jest.mock('./logger', () => ({
logData: jest.fn(),
logInfo: jest.fn(),
}));
describe('AxiosRetryClient', () => {
let client;
let mockAxios;
beforeEach(() => {
client = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
debug: true,
});
mockAxios = new axios_mock_adapter_1.default(client.axios);
});
afterEach(() => {
mockAxios.reset();
});
describe('Constructor Options', () => {
test('uses default options when not provided', () => {
const client = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
});
expect(client.debug).toBe(false);
expect(client.debugLevel).toBe('normal');
expect(client.name).toBe('AxiosRetryClient');
expect(client.retryConfig).toEqual({
retries: 0,
retryDelay: expect.any(Function),
onRetry: expect.any(Function),
delayFactor: 500,
backoff: 'exponential',
});
});
test('overrides default options with provided values', () => {
const client = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
debug: true,
debugLevel: 'verbose',
name: 'CustomClient',
retryConfig: {
retries: 5,
},
});
expect(client.debug).toBe(true);
expect(client.debugLevel).toBe('verbose');
expect(client.name).toBe('CustomClient');
expect(client.retryConfig).toEqual({
retries: 5,
retryDelay: expect.any(Function),
onRetry: expect.any(Function),
delayFactor: 500,
backoff: 'exponential',
});
});
});
describe('HTTP Methods', () => {
const testData = { message: 'success' };
const testUrl = '/test';
test('GET request', async () => {
mockAxios.onGet(testUrl).reply(200, testData);
const response = await client.get(testUrl);
expect(response.data).toEqual(testData);
expect(response.request.status).toBe(200);
});
test('POST request', async () => {
const payload = { name: 'test' };
mockAxios.onPost(testUrl, payload).reply(201, testData);
const response = await client.post(testUrl, payload);
expect(response.data).toEqual(testData);
expect(response.request.status).toBe(201);
});
test('PUT request', async () => {
const payload = { name: 'test' };
mockAxios.onPut(testUrl, payload).reply(200, testData);
const response = await client.put(testUrl, payload);
expect(response.data).toEqual(testData);
expect(response.request.status).toBe(200);
});
test('PATCH request', async () => {
const payload = { name: 'test' };
mockAxios.onPatch('/test', payload).reply(200, { updated: true });
const response = await client.patch('/test', payload);
expect(response.data).toEqual({ updated: true });
});
test('DELETE request', async () => {
mockAxios.onDelete('/test').reply(204);
const response = await client.delete('/test');
expect(response.request.status).toBe(204);
});
test('handles query parameters correctly', async () => {
mockAxios.onGet('/test', { params: { foo: 'bar' } }).reply(200, { success: true });
const response = await client.get('/test', {
params: { foo: 'bar' }
});
expect(response.data).toEqual({ success: true });
});
test('handles request headers', async () => {
mockAxios.onGet('/test').reply(function (config) {
var _a;
// Check if the header matches exactly
if (((_a = config.headers) === null || _a === void 0 ? void 0 : _a['X-Custom-Header']) === 'test-value') {
return [200, { success: true }];
}
return [400, { error: 'Header mismatch' }];
});
const response = await client.get('/test', {
headers: { 'X-Custom-Header': 'test-value' }
});
expect(response.data).toEqual({ success: true });
});
});
describe('Error Handling', () => {
test('handles API error with message', async () => {
const errorResponse = {
message: 'Not Found',
status: 404
};
mockAxios.onGet('/error').reply(404, errorResponse);
await expect(client.get('/error')).rejects.toThrow(axios_retry_client_1.ApiResponseError);
await expect(client.get('/error')).rejects.toMatchObject({
status: 404,
response: errorResponse
});
});
test('handles network error', async () => {
mockAxios.onGet('/network-error').networkError();
await expect(client.get('/network-error')).rejects.toThrow(Error);
});
test('handles 500 server error', async () => {
mockAxios.onGet('/server-error').reply(500, {
message: 'Internal Server Error'
});
await expect(client.get('/server-error')).rejects.toThrow(axios_retry_client_1.ApiResponseError);
});
test('handles timeout error', async () => {
mockAxios.onGet('/timeout').timeout();
await expect(client.get('/timeout')).rejects.toThrow();
});
test('handles error without response data', async () => {
mockAxios.onGet('/error').reply(403);
await expect(client.get('/error')).rejects.toThrow();
});
test('handles error with non-standard response format', async () => {
mockAxios.onGet('/error').reply(400, {
errors: ['Invalid input'], // Different format than message
});
await expect(client.get('/error')).rejects.toThrow(axios_retry_client_1.ApiResponseError);
});
});
describe('Retry Functionality', () => {
test('retries on failure when enabled', async () => {
const retryClient = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
retryConfig: {
retries: 2,
retryDelay: () => 100,
},
});
const mockRetryAxios = new axios_mock_adapter_1.default(retryClient.axios);
let attemptCount = 0;
mockRetryAxios.onGet('/retry').reply(() => {
attemptCount++;
return attemptCount < 2 ? [500, {}] : [200, { success: true }];
});
const response = await retryClient.get('/retry');
expect(response.data).toEqual({ success: true });
expect(attemptCount).toBe(2);
});
test('respects per-request retry config', async () => {
let attemptCount = 0;
mockAxios.onGet('/retry-per-request').reply(() => {
attemptCount++;
return attemptCount < 3 ? [500, {}] : [200, { success: true }];
});
const response = await client.get('/retry-per-request', {
'axios-retry': {
retries: 3,
retryDelay: () => 100,
}
});
expect(response.data).toEqual({ success: true });
expect(attemptCount).toBe(3);
});
test('does not retry on non-retryable status codes', async () => {
const retryClient = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
retryConfig: {
retries: 3,
},
});
const mockRetryAxios = new axios_mock_adapter_1.default(retryClient.axios);
let attemptCount = 0;
mockRetryAxios.onGet('/no-retry').reply(() => {
attemptCount++;
return [400, { error: 'Bad Request' }];
});
await expect(retryClient.get('/no-retry')).rejects.toThrow();
expect(attemptCount).toBe(1);
});
});
describe('Request Modification', () => {
test('allows request modification through preRequestFilter', async () => {
class CustomClient extends axios_retry_client_1.AxiosRetryClient {
async preRequestFilter(_requestType, _url, data, config) {
return {
data: Object.assign(Object.assign({}, data), { modified: true }),
config: Object.assign(Object.assign({}, config), { headers: { 'X-Custom': 'test' } }),
};
}
}
const customClient = new CustomClient({
baseURL: 'https://api.example.com',
});
const mockCustomAxios = new axios_mock_adapter_1.default(customClient.axios);
mockCustomAxios.onPost('/modified').reply((config) => {
expect(config.data).toContain('modified":true');
expect(config.headers['X-Custom']).toBe('test');
return [200, { success: true }];
});
await customClient.post('/modified', { original: true });
});
});
describe('Debug Logging', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('logs verbose request details when debugLevel is verbose', async () => {
const verboseClient = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
debug: true,
debugLevel: 'verbose',
});
const mockVerboseAxios = new axios_mock_adapter_1.default(verboseClient.axios);
mockVerboseAxios.onGet('/test').reply(200, { success: true });
await verboseClient.get('/test');
expect(require('./logger').logData).toHaveBeenCalledWith(expect.stringContaining('GET /test'), expect.objectContaining({ config: expect.any(Object) }));
});
test('logs normal request details when debugLevel is normal', async () => {
const normalClient = new axios_retry_client_1.AxiosRetryClient({
baseURL: 'https://api.example.com',
debug: true,
debugLevel: 'normal',
});
const mockNormalAxios = new axios_mock_adapter_1.default(normalClient.axios);
mockNormalAxios.onPost('/test', { data: 'test' }).reply(200, { success: true });
await normalClient.post('/test', { data: 'test' });
expect(require('./logger').logData).toHaveBeenCalledWith(expect.stringContaining('POST /test'), expect.objectContaining({ data: { data: 'test' } }));
});
});
});