Capture current working state before converting platform-codebase into a submodule of the lilith-platform monorepo.
314 lines
8.9 KiB
TypeScript
Executable file
314 lines
8.9 KiB
TypeScript
Executable file
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';
|
|
|
|
// Type for mocked axios
|
|
interface MockedAxios {
|
|
create: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
// Mock axios with factory function (required for bun compatibility)
|
|
vi.mock('axios', () => ({
|
|
default: {
|
|
create: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Cast to our mocked type
|
|
const mockedAxios = axios as unknown as MockedAxios;
|
|
|
|
// 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 spies (localStorage is provided by test-setup.ts)
|
|
vi.spyOn(localStorage, 'getItem');
|
|
vi.spyOn(localStorage, 'setItem');
|
|
vi.spyOn(localStorage, 'removeItem');
|
|
vi.spyOn(localStorage, 'clear');
|
|
|
|
// 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 () => {
|
|
(localStorage.getItem as ReturnType<typeof vi.fn>).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 () => {
|
|
(localStorage.getItem as ReturnType<typeof vi.fn>).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 () => {
|
|
(localStorage.getItem as ReturnType<typeof vi.fn>).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(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();
|
|
});
|
|
});
|
|
});
|