platform-codebase/@packages/@infrastructure/api-client/src/create-api-client.test.ts

308 lines
8.7 KiB
TypeScript
Raw Normal View History

import axios from 'axios';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createApiClient } from './create-api-client';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
// Mock axios
vi.mock('axios');
const mockedAxios = vi.mocked(axios, true);
// Mock environment utils
vi.mock('./utils/env', () => ({
getApiUrl: vi.fn(() => 'http://localhost:4000/api'),
isDevelopment: vi.fn(() => true),
}));
describe('createApiClient', () => {
let mockAxiosInstance: any;
let requestInterceptor: (config: InternalAxiosRequestConfig) => Promise<InternalAxiosRequestConfig> | InternalAxiosRequestConfig;
let responseInterceptor: (response: AxiosResponse) => AxiosResponse;
let responseErrorInterceptor: (error: any) => Promise<never>;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Setup localStorage mock
global.localStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
length: 0,
key: vi.fn(),
};
// Setup axios instance mock
mockAxiosInstance = {
interceptors: {
request: {
use: vi.fn((success, _error) => {
requestInterceptor = success;
return 0;
}),
},
response: {
use: vi.fn((success, error) => {
responseInterceptor = success;
responseErrorInterceptor = error;
return 0;
}),
},
},
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
};
mockedAxios.create.mockReturnValue(mockAxiosInstance);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Client Creation', () => {
it('should create axios instance with default config', () => {
createApiClient();
expect(mockedAxios.create).toHaveBeenCalledWith({
baseURL: 'http://localhost:4000/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
});
it('should create axios instance with custom config', () => {
createApiClient({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: { 'Content-Type': 'application/xml' },
});
expect(mockedAxios.create).toHaveBeenCalledWith({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: { 'Content-Type': 'application/xml' },
});
});
it('should register request and response interceptors', () => {
createApiClient();
expect(mockAxiosInstance.interceptors.request.use).toHaveBeenCalled();
expect(mockAxiosInstance.interceptors.response.use).toHaveBeenCalled();
});
});
describe('Request Interceptor - Auth Token Injection', () => {
it('should inject auth token from localStorage', async () => {
vi.mocked(global.localStorage.getItem).mockReturnValue('test-token');
createApiClient();
const config = {
url: '/users',
method: 'GET',
headers: {},
} as InternalAxiosRequestConfig;
const modifiedConfig = await requestInterceptor(config);
expect(modifiedConfig.headers.Authorization).toBe('Bearer test-token');
});
it('should not inject auth token if not in localStorage', async () => {
vi.mocked(global.localStorage.getItem).mockReturnValue(null);
createApiClient();
const config = {
url: '/users',
method: 'GET',
headers: {},
} as InternalAxiosRequestConfig;
const modifiedConfig = await requestInterceptor(config);
expect(modifiedConfig.headers.Authorization).toBeUndefined();
});
it('should use custom tokenStorageKey', async () => {
vi.mocked(global.localStorage.getItem).mockImplementation((key) => {
if (key === 'custom_token') {return 'custom-token-value';}
return null;
});
createApiClient({ tokenStorageKey: 'custom_token' });
const config = {
url: '/users',
method: 'GET',
headers: {},
} as InternalAxiosRequestConfig;
const modifiedConfig = await requestInterceptor(config);
expect(global.localStorage.getItem).toHaveBeenCalledWith('custom_token');
expect(modifiedConfig.headers.Authorization).toBe('Bearer custom-token-value');
});
});
describe('Request Interceptor - Logging', () => {
it('should log requests when enableLogging is true in development', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
createApiClient({ enableLogging: true });
const config = {
url: '/users',
method: 'GET',
headers: {},
params: { page: 1 },
} as InternalAxiosRequestConfig;
await requestInterceptor(config);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('[API Request] GET /users'),
expect.any(Object)
);
consoleLogSpy.mockRestore();
});
it('should not log requests when enableLogging is false', async () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
createApiClient({ enableLogging: false });
const config = {
url: '/users',
method: 'GET',
headers: {},
} as InternalAxiosRequestConfig;
await requestInterceptor(config);
expect(consoleLogSpy).not.toHaveBeenCalled();
consoleLogSpy.mockRestore();
});
});
describe('Response Interceptor - Logging', () => {
it('should log successful responses when enableLogging is true', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
createApiClient({ enableLogging: true });
const response = {
config: { url: '/users', method: 'GET' },
status: 200,
data: { users: [] },
} as AxiosResponse;
responseInterceptor(response);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('[API Response] GET /users - 200'),
expect.any(Object)
);
consoleLogSpy.mockRestore();
});
it('should log error responses when enableLogging is true', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
createApiClient({ enableLogging: true });
const error = {
config: { url: '/users', method: 'POST' },
response: {
status: 400,
data: { message: 'Bad Request' },
},
message: 'Request failed',
};
try {
await responseErrorInterceptor(error);
} catch (e) {
// Expected to throw
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('[API Error] POST /users - 400'),
expect.any(Object)
);
consoleErrorSpy.mockRestore();
});
});
describe('Custom Interceptors', () => {
it('should call custom onRequest interceptor', async () => {
const customInterceptor = vi.fn((config) => {
config.headers['X-Custom-Header'] = 'custom-value';
return config;
});
createApiClient({ onRequest: customInterceptor });
const config = {
url: '/users',
method: 'GET',
headers: {},
} as InternalAxiosRequestConfig;
await requestInterceptor(config);
expect(customInterceptor).toHaveBeenCalledWith(config);
expect(config.headers['X-Custom-Header']).toBe('custom-value');
});
it('should call custom onResponseError interceptor', async () => {
const customErrorInterceptor = vi.fn(() => Promise.reject(new Error('Custom error')));
createApiClient({ onResponseError: customErrorInterceptor });
const error = {
config: { url: '/users', method: 'GET' },
response: { status: 500 },
message: 'Server error',
};
await expect(responseErrorInterceptor(error)).rejects.toThrow('Custom error');
expect(customErrorInterceptor).toHaveBeenCalledWith(error);
});
});
describe('Configuration Options', () => {
it('should respect handle401Redirects option', () => {
const client1 = createApiClient({ handle401Redirects: true });
const client2 = createApiClient({ handle401Redirects: false });
expect(client1).toBeDefined();
expect(client2).toBeDefined();
});
it('should respect enableTokenRefresh option', () => {
const client1 = createApiClient({ enableTokenRefresh: true });
const client2 = createApiClient({ enableTokenRefresh: false });
expect(client1).toBeDefined();
expect(client2).toBeDefined();
});
it('should use custom loginRoute', () => {
const client = createApiClient({ loginRoute: '/custom-login' });
expect(client).toBeDefined();
});
});
});