⚠️ Draft documentation
Work in progress — some sections may be incomplete, typos possible. Last updated: 2026-05-10.
EN | DE | RU | FR | ES

Commentaire : git diff origin/develop -- actions/authentication.test.js

Date : 2026-05-08

Commits comparés :

Ce fichier : ancienne version sur GitHub (237 lignes, develop, créé le 2019-03-15) → nouvelle version (157 lignes)

Note : Les liens vers le nouveau code (SHA e67067aa7) pointent vers le fork MaurerAnton/projectforge (branche draft43npm). Les liens vers l'ancien code (SHA 9ed5fbe0f) pointent vers le dépôt principal micromata/projectforge (develop).

Section 1 : Imports (Lignes 1–5)

@@ -1,5 +1,4 @@
1 /* eslint-disable */
2-import fetchMock from 'fetch-mock/es5/client';
3-import cookies from 'react-cookies';
+import { vi } from 'vitest';
2 import configureMockStore from 'redux-mock-store';
5-import thunk from 'redux-thunk';
+import { thunk } from 'redux-thunk';

Ligne 2 : -import fetchMock from 'fetch-mock/es5/client'

Avant : L'ancien test utilisait fetch-mock — une bibliothèque externe pour simuler fetch (ajouté le 2019-03-15). La version es5/client était nécessaire pour la compatibilité avec les anciennes versions de Jest/CRA. [Code source develop]

Après : Supprimé. Trois raisons :

  1. Vitest fournit vi.fn() directement — aucun paquet séparé n'est nécessaire.
  2. fetch-mock version ^12.6.0 [package.json develop] possède une API incompatible avec les normes Fetch modernes et nécessite des polyfills [source fetch-mock sur GitHub].
  3. Pas de dépendance fetch-mock restante — un paquet en moins, moins de vulnérabilités.

Ligne 3 : -import cookies from 'react-cookies'

Avant : Bibliothèque pour la lecture/écriture de cookies dans le navigateur react-cookies@0.1.1 (ajouté le 2019-03-09, package.json develop [source sur GitHub]). Utilisée dans les anciens tests pour vérifier KEEP_SIGNED_IN (Ligne 94, Ligne 132, Ligne 183).

Pourquoi supprimé : Les cookies sont HTTP-only, JavaScript ne peut pas les lire (le test appelait cookies.loadAll(), qui appelle cookie.parse(document.cookie) — vrais cookies navigateur, pas de mock). fetch-mock place Set-Cookie dans les en-têtes de la Response, mais document.cookie n'est jamais peuplé. Ainsi, cookies.loadAll() renvoie toujours {} — vide. Le test passait non pas parce que la logique était correcte, mais parce que fetch-mock et react-cookies évoluent dans des univers séparés. react-cookies n'est plus dans les dépendances.

Ligne 3 (nouvelle) : +import { vi } from 'vitest'

Après : API de mock de Vitest. Équivalent à jest.fn() [docs Jest], mais fourni directement par Vitest lui-même [vitest.dev] (version ^4.1.5 [package.json] ).

Méthodes principales utilisées :

Ligne 5 : -import thunk … +import { thunk }

Ce qui a changé : L'import de redux-thunk est passé de default (import thunk from) à named (import { thunk } from).

Dans redux-thunk version 3.x (^3.1.0 [source: export const thunk]), l'export par défaut a été supprimé — désormais l'export nommé { thunk } est requis.

configureMockStore([thunk])middleware permettant le dispatch de fonctions (thunks) au lieu de simples objets. Sans elle, store.dispatch(login(...)) ne fonctionne pas.

Section 2 : Imports depuis './authentication' (Lignes 6–15)

@@ -6,13 +5,10 @@
6 import {
7- loadSessionIfAvailable,
8- login,
9- logout,
5 USER_LOGIN_BEGIN,
6 USER_LOGIN_FAILURE,
7 USER_LOGIN_SUCCESS,
12- USER_LOGOUT,
9 userLoginBegin,
10 userLoginFailure,
11 userLoginSuccess,
17- userLogout,
+ login,
+ loadUserStatus,
14 } from './authentication';

Retiré de l'import :

Ajouté à l'import :

Remarque : L'ancien fichier de test importait 5 symboles qui n'existent pas dans le code source authentication.js (loadSessionIfAvailable, logout, USER_LOGOUT, userLogout, storeLoginSession). Le test était défectueux pendant 7 ans — les symboles ont été retirés du code source (userLogout/logout supprimés en juil. 2019, loadSessionIfAvailable renommé en mars 2019), mais le test n'a jamais été mis à jour.

Section 3 : Configuration (Lignes 20–28)

@@ -20,77 +15,33 @@
20-describe('login', () => {
21- const username = 'demo';
22- const password = 'demo123';
23-
24- Object.freeze(username);
25- Object.freeze(password);
26-
27- const mockStore = configureMockStore([thunk]);
+const mockStore = configureMockStore([thunk]);
29- afterEach(() => fetchMock.restore());

mockStore déplacé au niveau du module — était créé 3× (dans login, logout, check session), maintenant créé une seule fois au début du fichier. [redux-mock-store]

Object.freeze()[MDN]. L'ancien test gelait username et password (inutile — les primitives sont immuables). Dans le nouveau fichier, Object.freeze n'est utilisé que sur les objets d'état dans les tests du reducer, où c'est pertinent (protection contre les mutations dans une fonction pure).

afterEach(() => fetchMock.restore())beforeEach(() => vi.restoreAllMocks()) — remplacement de l'appel spécifique à fetch-mock par l'appel universel de Vitest vi.restoreAllMocks(). Passage de « après » à « avant » — garantit que les mocks ne fuient pas entre les tests.

Section 4 : Tests des Action Creators (Lignes 29–43)

@@ -98,112 +89,133 @@
31- it('should create an action to start the login', () => {
32- const expectedAction = {
33- type: USER_LOGIN_BEGIN,
34- };
35-
36- expect(userLoginBegin())
37- .toEqual(expectedAction);
+describe('action creators', () => {
+ it('userLoginBegin', () => {
+ expect(userLoginBegin()).toEqual({ type: USER_LOGIN_BEGIN });
+ });
40- it('should create an action to mark the login as success', () => {
41- const expectedAction = {
42- type: USER_LOGIN_SUCCESS,
43- };
44-
45- expect(userLoginSuccess())
46- .toEqual(expectedAction);
+ it('userLoginSuccess', () => {
+ expect(userLoginSuccess('user', '1.0', '2024', undefined))
+ .toEqual({
+ type: USER_LOGIN_SUCCESS,
+ payload: { user: 'user', version: '1.0', buildTimestamp: '2024', alertMessage: undefined },
+ });
+ });
49- it('should create an action to mark the login as success', () => {
+ it('userLoginFailure', () => {
+ expect(userLoginFailure('Some error'))
+ .toEqual({
+ type: USER_LOGIN_FAILURE,
+ payload: { error: 'Some error' },
+ });
+ });
+});

Trois tests d'Action Creators, regroupés dans describe('action creators').

TestAncienNouveauDifférence
userLoginBegin 11 lignes, variable expectedAction 3 lignes, inline Seulement du style. Logique inchangée.
userLoginSuccess Appel sans arguments : userLoginSuccess(). Attendu : { type: USER_LOGIN_SUCCESS } [Ligne 45] Appel avec 4 arguments. Attendu : payload complète avec user, version, buildTimestamp, alertMessage [nouveau] Correction : userLoginSuccess prend 4 paramètres requis (authentication.js:11). L'ancien test vérifiait un comportement incomplet.
userLoginFailure 10 lignes, description avec faute de frappe « mark the login as success » [Ligne 49] 5 lignes, description correcte Faute de frappe corrigée. Logique inchangée.

🔍 Erreur de frappe dans l'original : deux tests portant le même nom

Ligne 40 — SUCCESS ✓ Ligne 49 — FAILURE, le nom dit « success » ✗
it('should create an action to mark
    the login as success', () => {
  const expectedAction = {
    type: USER_LOGIN_SUCCESS
  };
  expect(userLoginSuccess())
    .toEqual(expectedAction);
});
it('should create an action to mark
    the login as success', () => {
  const expectedAction = {
    type: USER_LOGIN_FAILURE,
    payload: { error: 'Some uncool...' }
  };
  expect(userLoginFailure('Some...'))
    .toEqual(expectedAction);
});

Cela a traîné ainsi pendant 7 ans. Dans la version mise à jour, les noms ont été corrigés.

Section 5 : Ancien test de connexion sans keepSignedIn (Lignes 61–97)

Que sont keepSignedIn / stayLoggedIn : La case à cocher « Rester connecté » dans le formulaire de connexion. Lorsqu'elle est activée, la chaîne suivante s'enclenche :

  1. Frontend : login(username, password, keepSignedIn) (authentication.js:65) → corps POST : { stayLoggedIn: keepSignedIn } (Ligne 77)
  2. Contrôleur : LoginPageRest.kt reçoit PostData<LoginData> (Ligne 96), champ stayLoggedIn dans LoginData.kt:34
  3. Service : LoginService.authenticate() vérifie if (loginData.stayLoggedIn == true) (LoginService.kt:175), génère un jeton STAY_LOGGED_IN_KEY (UserTokenType.kt:31) et appelle addStayLoggedInCookie() (Lignes 177-178)
  4. Cookie : CookieService définit le cookie avec le nom "stayLoggedIn" (CookieService.kt:201) et une durée de vie de 30 jours (Ligne 200)
  5. Récupération : au prochain démarrage, si JSESSIONID a expiré, LoginService.checkStayLoggedIn() (LoginService.kt:250) lit le cookie via CookieService (CookieService.kt:65-75) — le serveur authentifie l'utilisateur sans mot de passe

L'ancien test tentait de le vérifier via cookies.loadAll() — mais le cookie n'arrive pas dans document.cookie dans le mock (voir Section 2).

@@ -98,112 +89,133 @@
61- it('creates USER_LOGIN_SUCCESS when fetching login has been done without keepSignedIn', () => {
62- fetchMock
63- .mock(
64- (url, options) => {
65- if (url !== '/rsPublic/login' || options.method !== 'POST') {
66- return false;
67- }
68-
69- const body = JSON.parse(options.body);
70-
71- return body.username === username
72- && body.password === password
73- && !body.stayLoggedIn;
74- },
75- {
76- status: 200,
77- headers: { 'Set-Cookie': 'JSESSIONID=ABCDEF0123456789' },
78- },
79- )
80- .catch({ throws: new Error('mock failed') });
81-
82- const expectedActions = [
83- { type: USER_LOGIN_BEGIN },
84- { type: USER_LOGIN_SUCCESS },
85- ];
86-
87- const store = mockStore({});
88-
89- return store.dispatch(login(username, password, false))
90- .then(() => {
91- expect(store.getActions())
92- .toEqual(expectedActions);
93-
94- expect(cookies.loadAll())
95- .toEqual({});
96- });
97- });

Problèmes de l'ancien test :

  1. fetchMock.mock(matcher, response) — API verbeuse : la fonction de correspondance analyse elle-même le corps de la requête et vérifie l'URL. [Ligne 62-80] [source fetch-mock]
  2. headers: { 'Set-Cookie': '...' } — simule la définition d'un cookie de session, mais fetch-mock ne peut pas vraiment définir de cookies [Ligne 77]. Pourquoi : fetch-mock renvoie un objet Response avec des en-têtes — mais n'écrit pas dans document.cookie. react-cookies.loadAll() lit quant à lui exactement document.cookie = cookie.parse(document.cookie). La chaîne : fetch-mock → Set-Cookie dans l'en-tête → document.cookie reste vide → loadAll() renvoie {} → le test passe, mais vérifie le vide plutôt que le vrai comportement.
  3. .catch({ throws: new Error('mock failed') }) — lancer une erreur si aucun matcher ne correspond. Un workaround qui masque les vrais problèmes [Ligne 80].
  4. expectedActions = [BEGIN, SUCCESS] — INCORRECT. login() dispatch BEGIN, puis appelle loadUserStatus()(dispatch), qui dispatch un autre BEGIN puis SUCCESS. Au total, il doit y avoir 3 Actions, pas 2. [authentication.js:65-84]loginloadUserStatus()(dispatch) à l'intérieur de .then().
  5. cookies.loadAll()toEqual({}) — teste la bibliothèque navigateur react-cookies, pas la logique de l'application [Ligne 94].

🏗 Analyse architecturale : pourquoi l'ancien test était voué à l'échec

Niveau d'abstraction incorrect. L'ancien test tentait de vérifier le comportement des cookies — mais les cookies sont gérés par le navigateur, pas par fetch() :

❌ Test mocké (jsdom)
fetch-mock ──→  Response {
  headers: { 'Set-Cookie': 'JSESSIONID' }
}
      │
      │ Set-Cookie NON traité
      ▼
document.cookie = ""

react-cookies.loadAll() ──→  {}
expect(cookies.loadAll()).toEqual({})  ✓
Test réussi — a vérifié le vide, pas le comportement
✓ Navigateur réel
fetch() ──→  HTTP Response {
  Set-Cookie: JSESSIONID=ABC
}
      │
      │ Le navigateur traite Set-Cookie
      ▼
document.cookie = "JSESSIONID=ABC"

react-cookies.loadAll() ──→  { JSESSIONID: "ABC" }
Cookie visible — comportement réel

Deux mondes de mock incompatibles. fetch-mock et react-cookies opèrent à des niveaux fondamentalement différents de la pile navigateur sans connexion dans l'environnement de test. fetch-mock remplace la couche HTTP (réseau), react-cookies lit la couche DOM (document.cookie). Dans un vrai navigateur, il y a un cookie jar entre les deux — un composant navigateur qui traite Set-Cookie depuis la réponse HTTP et l'écrit dans document.cookie. Dans jsdom avec fetch mocké, cette couche manque — le test vérifiait une illusion.

Le test a réussi par coïncidence. Le navigateur n'a pas traité Set-Cookiedocument.cookie vide → cookies.loadAll() a renvoyé {}. Mais le test attendait {} ! Deux zéros se sont rencontrés — le vide a rencontré le vide — et ont engendré l'illusion d'un test fonctionnel.

Limite de responsabilité. Un test unitaire frontend doit vérifier quelles Actions Redux sont dispatchées en réponse à différentes réponses Fetch, pas comment le navigateur traite les en-têtes HTTP. Test correct : « fetch a renvoyé 200 → dispatch de [BEGIN, BEGIN, SUCCESS] ». Incorrect : « fetch a renvoyé Set-Cookie → vérifier si le cookie est arrivé dans document.cookie » — c'est un test navigateur, pas notre code.

Section 6 : Ancien test de connexion avec keepSignedIn et bug /√/login (Lignes 99–137)

@@ -98,112 +89,133 @@
99- it('creates USER_LOGIN_SUCCESS when fetching login has been done with keepSignedIn', () => {
100- fetchMock
101- .mock(
102- (url, options) => {
103- if (url !== '/√/login' || options.method !== 'POST') {
104- return false;
105- }
106-
107- const body = JSON.parse(options.body);
108-
109- return body.username === username
110- && body.password === password
111- && body.stayLoggedIn;
112- },
113- {
114- status: 200,
115- headers: { 'Set-Cookie': 'JSESSIONID=ABCDEF0123456789' },
116- },
117- )
118- .catch({ throws: new Error('mock failed') });
119-
120- const expectedActions = [
121- { type: USER_LOGIN_BEGIN },
122- { type: USER_LOGIN_SUCCESS },
123- ];
124-
125- const store = mockStore({});
126-
127- return store.dispatch(login(username, password, true))
128- .then(() => {
129- expect(store.getActions())
130- .toEqual(expectedActions);
131-
132- expect(cookies.loadAll())
133- .toEqual({
134- KEEP_SIGNED_IN: true,
135- });
136- });
137- });

Bug critique dans l'original :

if (url !== '/√/login' || options.method !== 'POST') — L'URL a été écrite avec le symbole racine carrée /√/login (U+221A), bien que l'URL correcte soit /rsPublic/login. [Ligne 103]

Ce test n'a jamais été exécuté — la requête POST /rsPublic/login ne correspondait pas à /√/login, tombant donc dans .catch({ throws: new Error('mock failed') }).

Le test pouvait néanmoins passer si l'erreur dans la chaîne .then().catch() était capturée — catchError dans authentication.js:84 dispatch FAILURE au lieu de relancer l'exception.

Section 7 : Ancien test d'échec de connexion (Lignes 139–161)

@@ -139,24 +131,14 @@
139- it('creates USER_LOGIN_FAILURE when fetching login has been failed', () => {
140- fetchMock
141- .mock('/rsPublic/login', 401)
142- .catch(() => {
143- throw new Error('mock failed');
144- });
145-
146- const expectedActions = [
147- { type: USER_LOGIN_BEGIN },
148- {
149- type: USER_LOGIN_FAILURE,
150- payload: { error: 'Unauthorized' },
151- },
152- ];
153-
154- const store = mockStore({});
155-
156- return store.dispatch(login(username, password, false))
157- .then(() => {
158- expect(store.getActions())
159- .toEqual(expectedActions);
160- });
161- });
162-});

Problèmes :

  1. fetchMock.mock('/rsPublic/login', 401) — ancienne API, simple URL et statut [Ligne 141]. Renvoyait { status: 401 } sans ok: false, mais handleHTTPErrors vérifie response.ok [rest.js:32].
  2. payload: { error: 'Unauthorized' } — dans le vrai code, handleHTTPErrors lance Error('Fetch failed: Error 401') [rest.js:34]. Le test attend un message incorrect.

Section 8 : Nouveau describe('login') — connexion réussie (Lignes 44–75)

@@ -39,24 +44,106 @@
+describe('login', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('dispatches BEGIN + BEGIN + SUCCESS for valid credentials', async () => {
+ const userData = { username: 'demo', admin: false };
+ const systemData = { version: '2.0.0', buildTimestamp: '2025-01-01 00:00' };
+
+ global.fetch = vi.fn()
+ .mockResolvedValueOnce(
+ { ok: true, status: 200, json: () => Promise.resolve({}) },
+ )
+ .mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve({ userData, systemData, alertMessage: undefined }),
+ });
+
+ const store = mockStore({});
+ await store.dispatch(login('demo', 'demo123', false));
+
+ expect(store.getActions()).toEqual([
+ { type: USER_LOGIN_BEGIN },
+ { type: USER_LOGIN_BEGIN },
+ {
+ type: USER_LOGIN_SUCCESS,
+ payload: {
+ user: userData,
+ version: systemData.version,
+ buildTimestamp: systemData.buildTimestamp,
+ alertMessage: undefined,
+ },
+ },
+ ]);
+ });

async/await — remplacement de .then() par async/await. Plus court, plus lisible, les erreurs ne sont pas avalies.

global.fetch = vi.fn().mockResolvedValueOnce() — deux appels fetch sont mockés séquentiellement :

  1. Premier : réponse à POST /rsPublic/login{} vide
  2. Deuxième : réponse à GET /rs/userStatus (issu de loadUserStatus() dans login()) — avec userData, systemData, alertMessage

[BEGIN, BEGIN, SUCCESS] — CORRIGÉ. Trois Actions :

L'ancien test attendait 2 ([BEGIN, SUCCESS]) — le second BEGIN manquait.

Section 9 : Nouveau describe('login') — mauvais mot de passe et réseau (Lignes 76–106)

@@ -44,14 +106,17 @@
+ it('dispatches BEGIN + FAILURE for invalid credentials', async () => {
+ global.fetch = vi.fn()
+ .mockResolvedValue({
+ ok: false,
+ status: 401,
+ json: () => Promise.resolve({}),
+ });
+
+ const store = mockStore({});
+ await store.dispatch(login('demo', 'wrong', false));
+
+ expect(store.getActions()).toEqual([
+ { type: USER_LOGIN_BEGIN },
+ { type: USER_LOGIN_FAILURE, payload: { error: 'Fetch failed: Error 401' } },
+ ]);
+ });

Mauvais mot de passe (401) :

@@ -61,9 +124,10 @@
+ it('dispatches BEGIN + FAILURE for network error', async () => {
+ global.fetch = vi.fn()
+ .mockRejectedValue(new Error('Network error'));
+
+ const store = mockStore({});
+ await store.dispatch(login('demo', 'demo123', false));
+
+ expect(store.getActions()).toEqual([
+ { type: USER_LOGIN_BEGIN },
+ { type: USER_LOGIN_FAILURE, payload: { error: 'Network error' } },
+ ]);
+ });
+});

Erreur réseau — NOUVEAU TEST (n'existait pas dans l'ancien fichier) :

mockRejectedValue(new Error('Network error'))[docs vitest]. Simule une erreur réseau/DNS. fetch ne renvoie pas de réponse, mais lance une exception. login()fetch(...) → réseau indisponible → .catch(catchError(dispatch))dispatch(USER_LOGIN_FAILURE('Network error')). [authentication.js:84]

Section 10 : describe('logout') supprimé (Lignes 164–192)

@@ -164,30 +143,14 @@
164-describe('logout', () => {
165- const mockStore = configureMockStore([thunk]);
166-
167- it('should create USER_LOGOUT action', () => {
168- const expectedAction = {
169- type: USER_LOGOUT,
170- };
171-
172- expect(userLogout())
173- .toEqual(expectedAction);
174- });
175-
176- it('creates USER_LOGOUT during logout', () => {
177- const expectedActions = [
178- { type: USER_LOGOUT },
179- ];
180-
181- const store = mockStore({});
182-
183- cookies.save('KEEP_SIGNED_IN', 'ABCDEF');
184-
185- store.dispatch(logout());
186-
187- expect(store.getActions())
188- .toEqual(expectedActions);
189-
190- expect(cookies.loadAll())
191- .toEqual({});
192- });
193-});

Entièrement supprimé (2 testss, ~30 lignes).

Raisons :

  1. userLogout() renvoie { type: USER_LOGOUT } — trivial, même niveau que l'Action Creator. USER_LOGOUT n'est pas exporté depuis authentication.js [seulement 3 types].
  2. logout() dispatch une Action — pas de code asynchrone.
  3. Les deux dépendent de cookies.loadAll() (react-cookies), que nous avons supprimé.
  4. La fonctionnalité de déconnexion est couverte par USER_LOGIN_BEGIN (réinitialisation d'état).

Section 11 : Ancien → Nouveau loadUserStatus (Lignes 195–236 → 107–157)

@@ -195,43 +158,17 @@
195-describe('check session', () => {
+describe('loadUserStatus', () => {
196- const mockStore = configureMockStore([thunk]);
197-
198- afterEach(() => fetchMock.restore());
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
200- it('creates no action at all', () => {
201- const store = mockStore({});
202-
203- expect(store.dispatch(loadSessionIfAvailable()))
204- .toEqual(null);
205- expect(store.getActions())
206- .toEqual([]);
207- });
208-
209- it('creates USER_LOGIN_SUCCESS', () => {
210- fetchMock
211- // TODO: ADD AUTHENTICATION TEST ENDPOINT
212- .getOnce('/rs/userStatus', 200)
213- .catch((url, a, b) => {
214- throw new Error('mock failed');
215- });
216-
217- const expectedActions = [
218- { type: USER_LOGIN_BEGIN },
219- { type: USER_LOGIN_SUCCESS },
220- ];
221-
222- cookies.save('KEEP_SIGNED_IN', true);
223-
224- const store = mockStore({});
225-
226- return store.dispatch(loadSessionIfAvailable())
227- .then(() => {
228- expect(store.getActions())
229- .toEqual(expectedActions);
230-
231- expect(cookies.loadAll())
232- .toEqual({
233- KEEP_SIGNED_IN: true,
234- });
235- });
236- });
237-});

TODO critique : 7 ans.

// TODO: ADD AUTHENTICATION TEST ENDPOINT [Ligne 211]ajouté le 2019-03-17, est resté ainsi 7 ans jusqu'à la PR 7e78f3741. L'ancien test loadSessionIfAvailable ne renvoyait que le statut 200, sans corps JSON. response.json() échouait. Le test n'a jamais été exécuté.

« creates no action at all » supprimé — vérifiait que loadSessionIfAvailable() renvoie null sans mock. Dans le code actuel, loadUserStatus() exécute toujours fetch.

Nouveau describe('loadUserStatus') — session valide (Lignes 108–140)

@@ -158,24 +133,28 @@
+ it('dispatches BEGIN + SUCCESS on valid session', async () => {
+ const userData = { username: 'existinguser', admin: true };
+ const systemData = { version: '2.0.0', buildTimestamp: '2025-05-05 10:00' };
+ const alertMessage = 'Some alert';
+
+ global.fetch = vi.fn()
+ .mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve({ userData, systemData, alertMessage }),
+ });
+
+ const store = mockStore({});
+ await store.dispatch(loadUserStatus());
+
+ expect(store.getActions()).toEqual([
+ { type: USER_LOGIN_BEGIN },
+ {
+ type: USER_LOGIN_SUCCESS,
+ payload: {
+ user: userData,
+ version: systemData.version,
+ buildTimestamp: systemData.buildTimestamp,
+ alertMessage,
+ },
+ },
+ ]);
+ });

Réponse JSON correcte : json: () => Promise.resolve({ userData, systemData, alertMessage }) — la structure correspond à ce que loadUserStatus() attend dans authentication.js:42.

alertMessage: 'Some alert' — vérifie la transmission du message système (dans le test de connexion, c'était undefined — chemins de code différents).

Nouveau describe('loadUserStatus') — session expirée (Lignes 142–157)

@@ -170,9 +147,12 @@
+ it('dispatches BEGIN + FAILURE on session expired', async () => {
+ global.fetch = vi.fn()
+ .mockResolvedValue({
+ ok: false,
+ status: 401,
+ json: () => Promise.resolve({}),
+ });
+
+ const store = mockStore({});
+ await store.dispatch(loadUserStatus());
+
+ const actions = store.getActions();
+ expect(actions[0]).toEqual({ type: USER_LOGIN_BEGIN });
+ expect(actions[1]).toEqual({ type: USER_LOGIN_FAILURE, payload: { error: undefined } });
+ });
+});

Nouveau test — n'existait pas dans l'ancien fichier.

payload: { error: undefined } — particularité de loadUserStatus() : dans le gestionnaire de capture, catchError(dispatch)({ message: undefined }) est appelé. catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message))error.message est undefined car un objet { message: undefined } est passé.

Vérification via actions[0] et actions[1] (pas toEqual sur tout le tableau) — car loadUserStatus() en cas d'erreur exécute window.location.href = (redirection vers /login), ce qui peut ne pas fonctionner dans jsdom.

Glossaire

Redux Thunk
Middleware permettant le dispatch de fonctions (au lieu de simples objets). La fonction reçoit (dispatch, getState) — peut exécuter fetch et dispatch plusieurs Actions. login() et loadUserStatus() sont des Thunks : ils renvoient function(dispatch) { ... }. Sans redux-thunk, Redux les rejeterait avec une erreur. [Documentation]
Créateur d'action → Thunk
Un Action Creator (userLoginBegin()) renvoie un objet { type, payload } — dispatché de manière synchrone. Un Thunk (login(username, password)) renvoie une fonction — la middleware l'exécute de manière asynchrone, effectue Fetch et dispatch plusieurs Actions. Deux couches différentes : Creator = usine d'objets synchrone, Thunk = orchestrateur asynchrone.
JSESSIONID
Cookie de session Java standard. N'est pas créé par le code ProjectForge, mais par le conteneur de servlets (Tomcat/Jetty) lors du premier request.getSession(true). ProjectForge n'en gère que le cycle de vie :
  • Après connexion — invalidation et recréation (protection contre la fixation de session) : LoginService.kt:333-342
    // Session Fixation: Change JSESSIONID after login
    request.getSession(false)?.let { session ->
        if (!session.isNew) { session.invalidate() }
    }
    val session = request.getSession(true)  // ← nouvelle JSESSIONID
    session.setAttribute(SESSION_KEY_USER, userContext)
  • Renforcement — chaque fetch() envoie le cookie via credentials: 'include' dans authentication.js:37:
    fetch(getServiceURL('userStatus'), {
        method: 'GET',
        credentials: 'include',  // ← envoie JSESSIONID au serveur
    })
S'il expire — session morte, GET /rs/userStatus renvoie 401. Le cookie est HTTP-only : JavaScript ne peut pas le lire via document.cookie. [Wikipedia : Session ID]
KEEP_SIGNED_IN / stayLoggedIn
Case à cocher « Rester connecté » dans le formulaire de connexion. Frontend : paramètre keepSignedIn dans authentication.js:65 → envoyé comme { stayLoggedIn: true } dans le corps POST (Ligne 77). Le serveur génère le jeton STAY_LOGGED_IN_KEY et définit un cookie nommé "stayLoggedIn" (CookieService.kt:201). KEEP_SIGNED_INancien nom de la constante côté client, supprimé en mars 2019. L'ancien test utilisait exactement celui-ci (cookies.save('KEEP_SIGNED_IN', ...)) — mauvais nom + fetch-mock ne définit pas de cookies = le test vérifiait le vide.
Cookie HTTP-only
Cookie avec le drapeau HttpOnly. Le navigateur l'envoie au serveur, mais JavaScript n'y a pas accès — document.cookie ne le contient pas. JSESSIONID est HTTP-only (protection contre les attaques XSS). C'est pourquoi cookies.loadAll() (qui lit document.cookie) ne voit jamais le cookie de session. [MDN]
vi.fn()
Constructeur de fonction mock de Vitest. Équivalent de jest.fn(). mockResolvedValueOnce(x) — l'appel suivant renvoie Promise.resolve(x) (chaîne). mockResolvedValue(x)tous les appels renvoient Promise.resolve(x). mockRejectedValue(e) — l'appel lance une exception (panne réseau). La différence Once / sans Once est critique : dans le test de connexion, deux appels Fetch sont mockés séquentiellement. [vitest]
import.meta.env.DEV / MODE
Équivalent de Vite pour process.env.NODE_ENV. import.meta.env.DEVtrue en mode développement. import.meta.env.MODE — chaîne : 'development', 'production' ou 'test'. Remplacement nécessaire car les variables CRA process.env.NODE_ENV n'existent pas dans Vite. Dans rest.js:10 : DEV && MODE !== 'test' — serveur de dev, mais pas d'exécution de tests. [docs vite]
connect() → useSelector/useDispatch
Deux façons de connecter des composants React au store Redux. connect(mapStateToProps)(Component) — pattern HOC (avant 2019), enveloppe le composant. useSelector(state => state.user) + useDispatch() — Hooks (React 16.8+), plus simple, compatible TypeScript, tree-shakeable. loggedIn: boolean est devenu user: object|null précisément à cause du passage aux Hooks : le sélecteur state => state.authentication.user !== null lit un champ concret au lieu d'un drapeau. [react-redux]
loadUserStatus()
Thunk appelé au démarrage de l'application et après la connexion. Exécute GET /rs/userStatus avec le cookie (credentials: 'include'). Le serveur vérifie la session : si valide — renvoie { userData, systemData, alertMessage } (200). Si expirée — 401 → redirection vers /react/public/login. C'est la première chose que React fait au chargement (voir ProjectForge.jsx) : « Qui suis-je ? » [authentication.js:30]

Change summary

AspectOld file (237 lines)New file (157 lines)
Fetch mock fetch-mock [line 2] vi.fn() [vitest]
Cookies react-cookies [line 3], stayLoggedIn tests Removed — browser not tested
Async style Promise .then() [MDN] async/await [MDN]
describe structure 3 blocks: login, logout, check session 3 blocks: action creators, login, loadUserStatus
login valid actions 2: [BEGIN, SUCCESS]wrong [line 82] 3: [BEGIN, BEGIN, SUCCESS]correct [auth.js:62-84]
login wrong credentials 'Unauthorized' — URL /√/login bug [line 103] 'Fetch failed: Error 401' — correct URL [rest.js:32-38]
Network failure No such test mockRejectedValue [line 96]
loadUserStatus valid session TODO: 7 years, never worked [line 211] Full test [lines 108-140]
loadUserStatus expired session No such test 401 → FAILURE [lines 142-157]
logout 2 tests, symbol doesn't exist in source [removed Jul 2019] Removed — trivial action, covered by USER_LOGIN_BEGIN

— Lien vers le code dans la branche fix/vite-eslint-upgrade, pas encore mergé dans develop. Le code pourrait ne pas être disponible sur GitHub jusqu'à la fusion de la PR.