/** * Tests for the API service * @jest-environment jsdom */ import { api, ApiError } from '@/services/api'; // Mock fetch globally const mockFetch = jest.fn(); global.fetch = mockFetch; // Mock helpers jest.mock('@/utils/helpers', () => ({ getToken: jest.fn(), })); import { getToken } from '@/utils/helpers'; describe('API Service', () => { beforeEach(() => { jest.clearAllMocks(); (getToken as jest.Mock).mockReturnValue(null); }); describe('ApiError class', () => { it('should create an error with status and message', () => { const error = new ApiError(404, 'Not found'); expect(error.status).toBe(404); expect(error.message).toBe('Not found'); expect(error.name).toBe('ApiError'); }); it('should create an error with data', () => { const errorData = { field: 'email', reason: 'invalid' }; const error = new ApiError(400, 'Validation error', errorData); expect(error.data).toEqual(errorData); }); }); describe('api.get', () => { it('should make a GET request without token', async () => { const mockResponse = { data: 'test' }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); const result = await api.get('/test'); expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/test', expect.objectContaining({ method: 'GET', headers: expect.objectContaining({ 'Content-Type': 'application/json', }), }) ); expect(result).toEqual(mockResponse); }); it('should include Authorization header when token exists', async () => { (getToken as jest.Mock).mockReturnValue('test-token'); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}), }); await api.get('/test'); expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer test-token', }), }) ); }); it('should handle full URLs', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}), }); await api.get('http://external.api/endpoint'); expect(mockFetch).toHaveBeenCalledWith( 'http://external.api/endpoint', expect.any(Object) ); }); it('should throw ApiError on non-ok response', async () => { mockFetch.mockResolvedValue({ ok: false, status: 401, statusText: 'Unauthorized', json: async () => ({ message: 'Invalid token' }), }); await expect(api.get('/test')).rejects.toThrow(ApiError); // Need a fresh mock for the second assertion mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', json: async () => ({ message: 'Invalid token' }), }); await expect(api.get('/test')).rejects.toMatchObject({ status: 401, message: 'Invalid token', }); }); it('should handle network errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(api.get('/test')).rejects.toThrow(ApiError); // Fresh mock for the second assertion mockFetch.mockRejectedValueOnce(new Error('Network error')); await expect(api.get('/test')).rejects.toMatchObject({ status: 0, message: 'Erreur de connexion au serveur', }); }); }); describe('api.post', () => { it('should make a POST request with data', async () => { const postData = { email: 'test@test.com', password: 'password' }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); await api.post('/auth/login', postData); expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/auth/login', expect.objectContaining({ method: 'POST', body: JSON.stringify(postData), }) ); }); it('should make a POST request without data', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); await api.post('/auth/logout'); expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ method: 'POST', body: undefined, }) ); }); }); describe('api.put', () => { it('should make a PUT request with data', async () => { const updateData = { name: 'New Name' }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); await api.put('/users/1', updateData); expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/users/1', expect.objectContaining({ method: 'PUT', body: JSON.stringify(updateData), }) ); }); }); describe('api.patch', () => { it('should make a PATCH request with data', async () => { const patchData = { status: 'active' }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); await api.patch('/users/1', patchData); expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/users/1', expect.objectContaining({ method: 'PATCH', body: JSON.stringify(patchData), }) ); }); }); describe('api.delete', () => { it('should make a DELETE request', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); await api.delete('/users/1'); expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/users/1', expect.objectContaining({ method: 'DELETE', }) ); }); }); describe('Error handling', () => { it('should extract error message from response', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request', json: async () => ({ message: 'Email already exists' }), }); try { await api.post('/auth/register', {}); } catch (error) { expect(error).toBeInstanceOf(ApiError); expect((error as ApiError).message).toBe('Email already exists'); } }); it('should use error field if message is not present', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request', json: async () => ({ error: 'Validation failed' }), }); try { await api.post('/auth/register', {}); } catch (error) { expect((error as ApiError).message).toBe('Validation failed'); } }); it('should use statusText if JSON parsing fails', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', json: async () => { throw new Error('Invalid JSON'); }, }); try { await api.get('/test'); } catch (error) { expect((error as ApiError).message).toBe('Internal Server Error'); } }); }); });