test: Ajouter les tests unitaires manquants (backend et frontend)
Couverture des processors (RefreshToken, RequestPasswordReset, ResetPassword, SwitchRole, UpdateUserRoles), des query handlers (HasGradesInPeriod, HasStudentsInClass), des messaging handlers (SendActivationConfirmation, SendPasswordResetEmail), et côté frontend des modules auth, roles, monitoring, types et E2E tokens.
This commit is contained in:
178
frontend/tests/unit/lib/features/roles/api/roles.test.ts
Normal file
178
frontend/tests/unit/lib/features/roles/api/roles.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
/**
|
||||
* Unit tests for the roles API module.
|
||||
*
|
||||
* Tests getMyRoles(), switchRole(), and updateUserRoles() which all rely on
|
||||
* authenticatedFetch from $lib/auth and getApiBaseUrl from $lib/api.
|
||||
*/
|
||||
|
||||
// Mock $lib/api
|
||||
vi.mock('$lib/api', () => ({
|
||||
getApiBaseUrl: () => 'http://test.classeo.local:18000/api'
|
||||
}));
|
||||
|
||||
// Mock $lib/auth - authenticatedFetch is the primary dependency
|
||||
const mockAuthenticatedFetch = vi.fn();
|
||||
vi.mock('$lib/auth', () => ({
|
||||
authenticatedFetch: (...args: unknown[]) => mockAuthenticatedFetch(...args)
|
||||
}));
|
||||
|
||||
import { getMyRoles, switchRole, updateUserRoles } from '$lib/features/roles/api/roles';
|
||||
|
||||
describe('roles API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// getMyRoles
|
||||
// ==========================================================================
|
||||
describe('getMyRoles', () => {
|
||||
it('should return roles array and activeRole on success', async () => {
|
||||
const mockResponse = {
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Administrateur' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Administrateur'
|
||||
};
|
||||
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse)
|
||||
});
|
||||
|
||||
const result = await getMyRoles();
|
||||
|
||||
expect(mockAuthenticatedFetch).toHaveBeenCalledWith(
|
||||
'http://test.classeo.local:18000/api/me/roles'
|
||||
);
|
||||
expect(result.roles).toHaveLength(2);
|
||||
expect(result.roles[0]).toEqual({ value: 'ROLE_ADMIN', label: 'Administrateur' });
|
||||
expect(result.activeRole).toBe('ROLE_ADMIN');
|
||||
expect(result.activeRoleLabel).toBe('Administrateur');
|
||||
});
|
||||
|
||||
it('should throw Error when the API response is not ok', async () => {
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500
|
||||
});
|
||||
|
||||
await expect(getMyRoles()).rejects.toThrow('Failed to fetch roles');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// switchRole
|
||||
// ==========================================================================
|
||||
describe('switchRole', () => {
|
||||
it('should return new activeRole on success', async () => {
|
||||
const mockResponse = {
|
||||
activeRole: 'ROLE_TEACHER',
|
||||
activeRoleLabel: 'Enseignant'
|
||||
};
|
||||
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse)
|
||||
});
|
||||
|
||||
const result = await switchRole('ROLE_TEACHER');
|
||||
|
||||
expect(mockAuthenticatedFetch).toHaveBeenCalledWith(
|
||||
'http://test.classeo.local:18000/api/me/switch-role',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'ROLE_TEACHER' })
|
||||
})
|
||||
);
|
||||
expect(result.activeRole).toBe('ROLE_TEACHER');
|
||||
expect(result.activeRoleLabel).toBe('Enseignant');
|
||||
});
|
||||
|
||||
it('should throw Error when the API response is not ok', async () => {
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400
|
||||
});
|
||||
|
||||
await expect(switchRole('ROLE_INVALID')).rejects.toThrow('Failed to switch role');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// updateUserRoles
|
||||
// ==========================================================================
|
||||
describe('updateUserRoles', () => {
|
||||
it('should complete without error on 2xx success', async () => {
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204
|
||||
});
|
||||
|
||||
// Should resolve without throwing
|
||||
await expect(
|
||||
updateUserRoles('user-uuid-123', ['ROLE_ADMIN', 'ROLE_TEACHER'])
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockAuthenticatedFetch).toHaveBeenCalledWith(
|
||||
'http://test.classeo.local:18000/api/users/user-uuid-123/roles',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ roles: ['ROLE_ADMIN', 'ROLE_TEACHER'] })
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with hydra:description when present in error response', async () => {
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 422,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
'hydra:description': 'Le rôle ROLE_INVALID est inconnu.'
|
||||
})
|
||||
});
|
||||
|
||||
await expect(
|
||||
updateUserRoles('user-uuid-123', ['ROLE_INVALID'])
|
||||
).rejects.toThrow('Le rôle ROLE_INVALID est inconnu.');
|
||||
});
|
||||
|
||||
it('should throw with detail when present in error response', async () => {
|
||||
mockAuthenticatedFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
detail: 'Accès refusé.'
|
||||
})
|
||||
});
|
||||
|
||||
await expect(
|
||||
updateUserRoles('user-uuid-123', ['ROLE_ADMIN'])
|
||||
).rejects.toThrow('Accès refusé.');
|
||||
});
|
||||
|
||||
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(
|
||||
updateUserRoles('user-uuid-123', ['ROLE_ADMIN'])
|
||||
).rejects.toThrow('Erreur lors de la mise à jour des rôles (500)');
|
||||
});
|
||||
});
|
||||
});
|
||||
351
frontend/tests/unit/lib/features/roles/roleContext.test.ts
Normal file
351
frontend/tests/unit/lib/features/roles/roleContext.test.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
/**
|
||||
* Unit tests for the role context (roleContext.svelte.ts).
|
||||
*
|
||||
* This module uses Svelte 5 $state runes for reactive state management.
|
||||
* We test it through its public exported API and mock the underlying
|
||||
* roles API module to isolate the context logic.
|
||||
*/
|
||||
|
||||
// Mock the roles API module
|
||||
const mockGetMyRoles = vi.fn();
|
||||
const mockSwitchRole = vi.fn();
|
||||
vi.mock('$lib/features/roles/api/roles', () => ({
|
||||
getMyRoles: (...args: unknown[]) => mockGetMyRoles(...args),
|
||||
switchRole: (...args: unknown[]) => mockSwitchRole(...args)
|
||||
}));
|
||||
|
||||
describe('roleContext', () => {
|
||||
let roleContext: typeof import('$lib/features/roles/roleContext.svelte');
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Fresh import to reset $state between tests
|
||||
vi.resetModules();
|
||||
roleContext = await import('$lib/features/roles/roleContext.svelte');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// Initial state
|
||||
// ==========================================================================
|
||||
describe('initial state', () => {
|
||||
it('should have no roles initially', () => {
|
||||
expect(roleContext.getRoles()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have null activeRole initially', () => {
|
||||
expect(roleContext.getActiveRole()).toBeNull();
|
||||
});
|
||||
|
||||
it('should have null activeRoleLabel initially', () => {
|
||||
expect(roleContext.getActiveRoleLabel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should not be loading initially', () => {
|
||||
expect(roleContext.getIsLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be switching initially', () => {
|
||||
expect(roleContext.getIsSwitching()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not have multiple roles initially', () => {
|
||||
expect(roleContext.hasMultipleRoles()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// fetchRoles
|
||||
// ==========================================================================
|
||||
describe('fetchRoles', () => {
|
||||
it('should load roles from API and set state', async () => {
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Administrateur' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Administrateur'
|
||||
});
|
||||
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
expect(mockGetMyRoles).toHaveBeenCalledOnce();
|
||||
expect(roleContext.getRoles()).toHaveLength(2);
|
||||
expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN');
|
||||
expect(roleContext.getActiveRoleLabel()).toBe('Administrateur');
|
||||
expect(roleContext.getIsLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('should guard against double loading (isFetched)', async () => {
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
|
||||
// First call loads data
|
||||
await roleContext.fetchRoles();
|
||||
expect(mockGetMyRoles).toHaveBeenCalledOnce();
|
||||
|
||||
// Second call should be a no-op due to isFetched guard
|
||||
await roleContext.fetchRoles();
|
||||
expect(mockGetMyRoles).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully without throwing', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockGetMyRoles.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
// Should not throw
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[roleContext] Failed to fetch roles:',
|
||||
expect.any(Error)
|
||||
);
|
||||
// State should remain empty
|
||||
expect(roleContext.getRoles()).toEqual([]);
|
||||
expect(roleContext.getActiveRole()).toBeNull();
|
||||
expect(roleContext.getIsLoading()).toBe(false);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// switchTo
|
||||
// ==========================================================================
|
||||
describe('switchTo', () => {
|
||||
it('should return true immediately when switching to same role', async () => {
|
||||
// First, load roles so activeRole is set
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
// Switch to the same role
|
||||
const result = await roleContext.switchTo('ROLE_ADMIN');
|
||||
|
||||
expect(result).toBe(true);
|
||||
// API should NOT be called for same-role switch
|
||||
expect(mockSwitchRole).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call API and update state when switching to different role', async () => {
|
||||
// Load initial roles
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Admin' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
// Switch to a different role
|
||||
mockSwitchRole.mockResolvedValueOnce({
|
||||
activeRole: 'ROLE_TEACHER',
|
||||
activeRoleLabel: 'Enseignant'
|
||||
});
|
||||
|
||||
const result = await roleContext.switchTo('ROLE_TEACHER');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSwitchRole).toHaveBeenCalledWith('ROLE_TEACHER');
|
||||
expect(roleContext.getActiveRole()).toBe('ROLE_TEACHER');
|
||||
expect(roleContext.getActiveRoleLabel()).toBe('Enseignant');
|
||||
expect(roleContext.getIsSwitching()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when the API call fails', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Load initial roles
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Admin' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
// Switch fails
|
||||
mockSwitchRole.mockRejectedValueOnce(new Error('Server error'));
|
||||
|
||||
const result = await roleContext.switchTo('ROLE_TEACHER');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN'); // unchanged
|
||||
expect(roleContext.getIsSwitching()).toBe(false);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// hasMultipleRoles
|
||||
// ==========================================================================
|
||||
describe('hasMultipleRoles', () => {
|
||||
it('should return true when user has more than one role', async () => {
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Admin' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
expect(roleContext.hasMultipleRoles()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when user has zero roles', () => {
|
||||
// No fetch, so roles is empty
|
||||
expect(roleContext.hasMultipleRoles()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when user has exactly one role', async () => {
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
expect(roleContext.hasMultipleRoles()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// resetRoleContext
|
||||
// ==========================================================================
|
||||
describe('resetRoleContext', () => {
|
||||
it('should clear all state back to initial values', async () => {
|
||||
// Load some data first
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Admin' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
// Verify state is set
|
||||
expect(roleContext.getRoles()).toHaveLength(2);
|
||||
expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN');
|
||||
|
||||
// Reset
|
||||
roleContext.resetRoleContext();
|
||||
|
||||
expect(roleContext.getRoles()).toEqual([]);
|
||||
expect(roleContext.getActiveRole()).toBeNull();
|
||||
expect(roleContext.getActiveRoleLabel()).toBeNull();
|
||||
expect(roleContext.hasMultipleRoles()).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow fetchRoles to be called again after reset', async () => {
|
||||
// Load initial data
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
expect(mockGetMyRoles).toHaveBeenCalledOnce();
|
||||
|
||||
// Reset clears isFetched
|
||||
roleContext.resetRoleContext();
|
||||
|
||||
// Now fetchRoles should call the API again
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [{ value: 'ROLE_TEACHER', label: 'Enseignant' }],
|
||||
activeRole: 'ROLE_TEACHER',
|
||||
activeRoleLabel: 'Enseignant'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
expect(mockGetMyRoles).toHaveBeenCalledTimes(2);
|
||||
expect(roleContext.getActiveRole()).toBe('ROLE_TEACHER');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// getIsLoading / getIsSwitching
|
||||
// ==========================================================================
|
||||
describe('loading and switching state', () => {
|
||||
it('should set isLoading to true during fetchRoles and false after', async () => {
|
||||
// Use a deferred promise to control resolution timing
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockGetMyRoles.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const fetchPromise = roleContext.fetchRoles();
|
||||
|
||||
// While the API call is pending, isLoading should be true
|
||||
expect(roleContext.getIsLoading()).toBe(true);
|
||||
|
||||
// Resolve the API call
|
||||
resolvePromise!({
|
||||
roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
|
||||
await fetchPromise;
|
||||
|
||||
expect(roleContext.getIsLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isSwitching to true during switchTo and false after', async () => {
|
||||
// Load roles first so activeRole is set
|
||||
mockGetMyRoles.mockResolvedValueOnce({
|
||||
roles: [
|
||||
{ value: 'ROLE_ADMIN', label: 'Admin' },
|
||||
{ value: 'ROLE_TEACHER', label: 'Enseignant' }
|
||||
],
|
||||
activeRole: 'ROLE_ADMIN',
|
||||
activeRoleLabel: 'Admin'
|
||||
});
|
||||
await roleContext.fetchRoles();
|
||||
|
||||
// Use a deferred promise for switch
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockSwitchRole.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const switchPromise = roleContext.switchTo('ROLE_TEACHER');
|
||||
|
||||
// While switching, isSwitching should be true
|
||||
expect(roleContext.getIsSwitching()).toBe(true);
|
||||
|
||||
// Resolve
|
||||
resolvePromise!({
|
||||
activeRole: 'ROLE_TEACHER',
|
||||
activeRoleLabel: 'Enseignant'
|
||||
});
|
||||
|
||||
await switchPromise;
|
||||
|
||||
expect(roleContext.getIsSwitching()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user