git diff origin/develop -- actions/authentication.test.jse67067aa7) 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).
-import fetchMock from 'fetch-mock/es5/client'-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.
+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 :
vi.fn() — crée une fonction mock (remplace global.fetch) — L49, L78, L95, L118, L143fn.mockResolvedValueOnce(value) — le prochain appel renvoie Promise.resolve(value) (chaîne de deux) — L50, L53fn.mockResolvedValue(value) — tous les appels renvoient Promise.resolve(value) — L79, L119, L144fn.mockRejectedValue(error) — l'appel lance une exception (erreur réseau) — L96vi.restoreAllMocks() — réinitialise tous les mocks entre les tests — L42, L110-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.
Retiré de l'import :
authentication.js actuel, cette fonction n'existe plus — elle a été renommée en loadUserStatus (Ligne 30, renommé le 2019-03-18).describe('logout'). La fonction logout n'existe pas dans authentication.js actuel [code source] (supprimé le 2019-07-13).authentication.js [seulement 3 types] (supprimé le 2019-07-13).Ajouté à l'import :
loadSessionIfAvailable. La fonction réelle dans authentication.js:30.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.
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.
Trois tests d'Action Creators, regroupés dans describe('action creators').
| Test | Ancien | Nouveau | Diffé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. |
| 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.
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 :
login(username, password, keepSignedIn) (authentication.js:65) → corps POST : { stayLoggedIn: keepSignedIn } (Ligne 77)LoginPageRest.kt reçoit PostData<LoginData> (Ligne 96), champ stayLoggedIn dans LoginData.kt:34LoginService.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)CookieService définit le cookie avec le nom "stayLoggedIn" (CookieService.kt:201) et une durée de vie de 30 jours (Ligne 200)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 passeL'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).
Problèmes de l'ancien test :
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]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..catch({ throws: new Error('mock failed') }) — lancer une erreur si aucun matcher ne correspond. Un workaround qui masque les vrais problèmes [Ligne 80].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] — login → loadUserStatus()(dispatch) à l'intérieur de .then().cookies.loadAll() → toEqual({}) — teste la bibliothèque navigateur react-cookies, pas la logique de l'application [Ligne 94].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() :
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-Cookie → document.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.
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.
Problèmes :
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].payload: { error: 'Unauthorized' } — dans le vrai code, handleHTTPErrors lance Error('Fetch failed: Error 401') [rest.js:34]. Le test attend un message incorrect.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 :
POST /rsPublic/login — {} videGET /rs/userStatus (issu de loadUserStatus() dans login()) — avec userData, systemData, alertMessage[BEGIN, BEGIN, SUCCESS] — CORRIGÉ. Trois Actions :
BEGIN — par login() (authentication.js:65)BEGIN — par loadUserStatus() (authentication.js:30)SUCCESS — par loadUserStatus() (authentication.js:42)L'ancien test attendait 2 ([BEGIN, SUCCESS]) — le second BEGIN manquait.
Mauvais mot de passe (401) :
mockResolvedValue (sans Once) — tous les appels renvoient 401. loadUserStatus() n'est pas appelé (la connexion a échoué plus tôt).error: 'Fetch failed: Error 401' — car handleHTTPErrors lance Error('Fetch failed: Error ${status}').'Unauthorized' — incorrect.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]
Entièrement supprimé (2 testss, ~30 lignes).
Raisons :
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].logout() dispatch une Action — pas de code asynchrone.cookies.loadAll() (react-cookies), que nous avons supprimé.USER_LOGIN_BEGIN (réinitialisation d'état).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.
describe('loadUserStatus') — session valide (Lignes 108–140)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).
describe('loadUserStatus') — session expirée (Lignes 142–157)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.
(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]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.request.getSession(true). ProjectForge n'en gère que le cycle de vie :
// 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)fetch() envoie le cookie via credentials: 'include' dans authentication.js:37:fetch(getServiceURL('userStatus'), {
method: 'GET',
credentials: 'include', // ← envoie JSESSIONID au serveur
})GET /rs/userStatus renvoie 401. Le cookie est HTTP-only : JavaScript ne peut pas le lire via document.cookie. [Wikipedia : Session ID]
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_IN — ancien 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.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]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]process.env.NODE_ENV. import.meta.env.DEV — true 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(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]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]| Aspect | Old 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.
Avant : L'ancien test utilisait
fetch-mock— une bibliothèque externe pour simulerfetch(ajouté le 2019-03-15). La versiones5/clientétait nécessaire pour la compatibilité avec les anciennes versions de Jest/CRA. [Code source develop]Après : Supprimé. Trois raisons :
vi.fn()directement — aucun paquet séparé n'est nécessaire.fetch-mockversion^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].fetch-mockrestante — un paquet en moins, moins de vulnérabilités.