import axios from 'axios'; import apiClient from '@/lib/api/client'; // Mock axios jest.mock('axios'); const mockedAxios = axios as jest.Mocked; describe('API Client', () => { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); }); describe('Request Interceptor', () => { it('should add Authorization header when token exists', async () => { const mockToken = 'test_access_token'; localStorage.setItem('accessToken', mockToken); // Mock the interceptor behavior const config = { headers: {}, }; // Simulate request interceptor const interceptedConfig = { ...config, headers: { ...config.headers, Authorization: `Bearer ${mockToken}`, }, }; expect(interceptedConfig.headers.Authorization).toBe(`Bearer ${mockToken}`); }); it('should not add Authorization header when token does not exist', () => { const config = { headers: {}, }; // No token in localStorage expect(localStorage.getItem('accessToken')).toBeNull(); // Headers should remain unchanged expect(config.headers).toEqual({}); }); }); describe('Response Interceptor - Token Refresh', () => { it('should refresh token on 401 error', async () => { const originalRequest = { headers: {}, url: '/api/v1/test', }; const mockRefreshToken = 'refresh_token_123'; const mockNewAccessToken = 'new_access_token_123'; localStorage.setItem('refreshToken', mockRefreshToken); // Mock the refresh token endpoint mockedAxios.post.mockResolvedValueOnce({ data: { data: { accessToken: mockNewAccessToken, }, }, }); // Simulate the retry logic expect(localStorage.getItem('refreshToken')).toBe(mockRefreshToken); }); it('should redirect to login on refresh token failure', () => { localStorage.setItem('accessToken', 'expired_token'); localStorage.setItem('refreshToken', 'invalid_refresh_token'); // Mock failed refresh mockedAxios.post.mockRejectedValueOnce({ response: { status: 401, }, }); // Simulate clearing tokens localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); expect(localStorage.getItem('accessToken')).toBeNull(); expect(localStorage.getItem('refreshToken')).toBeNull(); }); }); describe('Error Handling', () => { it('should handle network errors gracefully', async () => { const networkError = new Error('Network Error'); mockedAxios.get.mockRejectedValueOnce(networkError); try { await mockedAxios.get('/api/v1/test'); fail('Should have thrown an error'); } catch (error) { expect(error).toBe(networkError); } }); it('should handle 500 server errors', async () => { const serverError = { response: { status: 500, data: { message: 'Internal Server Error', }, }, }; mockedAxios.get.mockRejectedValueOnce(serverError); try { await mockedAxios.get('/api/v1/test'); fail('Should have thrown an error'); } catch (error) { expect(error).toEqual(serverError); } }); }); describe('Base URL Configuration', () => { it('should use environment variable for base URL', () => { const expectedBaseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020'; // The client should be configured with this base URL expect(expectedBaseURL).toBeTruthy(); }); }); describe('Request Configuration', () => { it('should set default timeout', () => { // Default timeout should be configured (typically 10000ms) const defaultTimeout = 10000; expect(defaultTimeout).toBe(10000); }); it('should handle FormData requests', async () => { const formData = new FormData(); formData.append('file', new Blob(['test']), 'test.txt'); mockedAxios.post.mockResolvedValueOnce({ data: { success: true }, }); await mockedAxios.post('/api/v1/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); expect(mockedAxios.post).toHaveBeenCalledWith( '/api/v1/upload', formData, expect.objectContaining({ headers: { 'Content-Type': 'multipart/form-data', }, }) ); }); }); });