import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; vi.mock('$lib/api', () => ({ getApiBaseUrl: () => 'http://test.classeo.local:18000/api' })); const mockAuthenticatedFetch = vi.fn(); vi.mock('$lib/auth', () => ({ authenticatedFetch: (...args: unknown[]) => mockAuthenticatedFetch(...args) })); import { fetchStudents, fetchClasses, createStudent, changeStudentClass } from '$lib/features/students/api/students'; describe('students API', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); // ========================================================================== // fetchStudents // ========================================================================== describe('fetchStudents', () => { it('should return members and totalItems on success', async () => { const mockStudents = [ { id: 'student-1', firstName: 'Marie', lastName: 'Dupont', email: null, classId: 'class-1', className: '6ème A', classLevel: 'sixieme', statut: 'inscrit', studentNumber: null, dateNaissance: null } ]; mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ 'hydra:member': mockStudents, 'hydra:totalItems': 1 }) }); const result = await fetchStudents({ page: 1, itemsPerPage: 30 }); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/students?page=1&itemsPerPage=30', {} ); expect(result.members).toHaveLength(1); expect(result.members[0]!.firstName).toBe('Marie'); expect(result.totalItems).toBe(1); }); it('should pass search and classId params when provided', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ 'hydra:member': [], 'hydra:totalItems': 0 }) }); await fetchStudents({ page: 2, itemsPerPage: 30, search: 'Dupont', classId: 'class-1' }); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/students?page=2&itemsPerPage=30&search=Dupont&classId=class-1', {} ); }); it('should throw when API response is not ok', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(fetchStudents({ page: 1, itemsPerPage: 30 })).rejects.toThrow( 'Erreur lors du chargement des élèves' ); }); }); // ========================================================================== // fetchClasses // ========================================================================== describe('fetchClasses', () => { it('should return classes array on success', async () => { const mockClasses = [ { id: 'class-1', name: '6ème A', level: 'sixieme' }, { id: 'class-2', name: '5ème B', level: 'cinquieme' } ]; mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ 'hydra:member': mockClasses }) }); const result = await fetchClasses(); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/classes?itemsPerPage=200' ); expect(result).toHaveLength(2); expect(result[0]!.name).toBe('6ème A'); }); it('should throw when API response is not ok', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(fetchClasses()).rejects.toThrow('Erreur lors du chargement des classes'); }); }); // ========================================================================== // createStudent // ========================================================================== describe('createStudent', () => { it('should return created student on success', async () => { const created = { id: 'new-student-id', firstName: 'Marie', lastName: 'Dupont', email: null, classId: 'class-1', className: '6ème A', classLevel: 'sixieme', statut: 'inscrit', studentNumber: null, dateNaissance: null }; mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(created) }); const result = await createStudent({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1' }); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/students', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1' }) }) ); expect(result.id).toBe('new-student-id'); expect(result.firstName).toBe('Marie'); }); it('should include optional fields when provided', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: 'new-id', firstName: 'Marie', lastName: 'Dupont', email: 'marie@example.com', classId: 'class-1', className: '6ème A', classLevel: 'sixieme', statut: 'pending', studentNumber: '12345', dateNaissance: '2015-06-15' }) }); await createStudent({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1', email: 'marie@example.com', dateNaissance: '2015-06-15', studentNumber: '12345' }); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/students', expect.objectContaining({ body: JSON.stringify({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1', email: 'marie@example.com', dateNaissance: '2015-06-15', studentNumber: '12345' }) }) ); }); it('should throw with hydra:description on error', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 422, json: () => Promise.resolve({ 'hydra:description': 'Cet email est déjà utilisé.' }) }); await expect( createStudent({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1' }) ).rejects.toThrow('Cet email est déjà utilisé.'); }); it('should throw generic message when error body is not valid JSON', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500, json: () => Promise.reject(new Error('Unexpected token')) }); await expect( createStudent({ firstName: 'Marie', lastName: 'Dupont', classId: 'class-1' }) ).rejects.toThrow('Erreur lors de la création (500)'); }); }); // ========================================================================== // changeStudentClass // ========================================================================== describe('changeStudentClass', () => { it('should call PATCH endpoint with correct body', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true }); await changeStudentClass('student-1', 'class-2'); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/students/student-1/class', expect.objectContaining({ method: 'PATCH', headers: { 'Content-Type': 'application/merge-patch+json' }, body: JSON.stringify({ classId: 'class-2' }) }) ); }); it('should throw with hydra:description on error', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 404, json: () => Promise.resolve({ 'hydra:description': 'Élève non trouvé.' }) }); await expect(changeStudentClass('student-1', 'class-2')).rejects.toThrow( 'Élève non trouvé.' ); }); it('should throw generic message when error body is not valid JSON', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500, json: () => Promise.reject(new Error('Unexpected token')) }); await expect(changeStudentClass('student-1', 'class-2')).rejects.toThrow( 'Erreur lors du changement de classe (500)' ); }); }); });