- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react - Update ESLint config path syntax to use node_modules paths - Add ESLint dependencies to React packages (messaging-hooks, react-query-utils, websocket-client, analytics-client) - Fix duplicate exports in @core/types (remove redundant re-exports) - Auto-fix import order issues across all packages - Add ESLint config for status-dashboard/server extending @eslint/config-base - Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages - Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system - Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock) - Relax strict type-checking rules in base config for existing code Packages configured: - @infrastructure/api-client, service-discovery, websocket-client, analytics-client - @testing/msw-handlers, mocks - @utils/text-utils - @core/types, design-tokens - @utility/zname - @hooks/messaging-hooks, react-query-utils All packages now pass ESLint with 0 errors (warnings only). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
307 lines
8.7 KiB
TypeScript
307 lines
8.7 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|