the-tip-top-frontend/__tests__/services/api.test.ts
soufiane 0a00c04b54 fix: reduce code duplication and add tests for SonarQube quality gate
- Consolidate API logic: hooks/useApi.ts now uses services/api.ts
- Create BaseFormField component to reduce form duplication
- Refactor FormField, FormSelect, FormTextarea to use BaseFormField
- Add centralized theme utility (utils/theme.ts) for colors/styles
- Add comprehensive tests for api, auth.service, useApi hooks, AuthContext
- Add tests for theme utility

This reduces duplication from 11.45% and improves test coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 14:54:24 +01:00

277 lines
7.3 KiB
TypeScript

/**
* 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');
}
});
});
});