⚠️ Documentación en borrador. Puede contener errores tipográficos e imprecisiones.
EN | DE | RU | FR | ES

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

Fecha: 2026-05-08

Commits comparados:

Este archivo: versión antigua en GitHub (237 líneas, develop, creado el 15-03-2019) → versión nueva (157 líneas)

Nota: Los enlaces al código nuevo (SHA e67067aa7) apuntan al fork MaurerAnton/projectforge (rama draft43npm). Los enlaces al código antiguo (SHA 9ed5fbe0f) apuntan al repositorio principal micromata/projectforge (develop).

Sección 1: Importaciones (líneas 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';

Línea 2: -import fetchMock from 'fetch-mock/es5/client'

Antes: La prueba antigua usaba fetch-mock — una librería externa para simular fetch (añadida el 15-03-2019). La versión es5/client era necesaria para compatibilidad con Jest/CRA antiguos. [fuente develop]

Después: Eliminado. Tres razones:

  1. Vitest incluye vi.fn() integrado — no se necesita un paquete separado.
  2. fetch-mock versión ^12.6.0 [package.json develop] tiene una API incompatible con los estándares modernos de fetch y requiere polyfills [fuente fetch-mock en GitHub].
  3. Toc ya no depende de fetch-mock — un paquete menos, menos vulnerabilidades.

Línea 3: -import cookies from 'react-cookies'

Antes: Librería para leer/escribir cookies en el navegador react-cookies@0.1.1 (añadida el 09-03-2019, package.json develop [fuente en GitHub]). Se usaba en pruebas antiguas para verificar KEEP_SIGNED_IN (línea 94, línea 132, línea 183).

Por qué se eliminó: Las cookies son HTTP-only, JavaScript no puede leerlas (pero la prueba llamaba a cookies.loadAll(), lo cual llama a cookie.parse(document.cookie) — cookies reales del navegador, no simuladas). fetch-mock coloca Set-Cookie en los encabezados de la Response, pero document.cookie nunca se llena. Entonces cookies.loadAll() siempre devuelve {} — vacío. La prueba pasaba no porque no existieran cookies, sino porque fetch-mock y react-cookies viven en mundos separados. react-cookies ya no está en las dependencias.

Línea 3 (nueva): +import { vi } from 'vitest'

Después: API de simulación de Vitest. Equivalente a jest.fn() [documentación Jest], pero desde Vitest mismo [vitest.dev] (versión ^4.1.5 [package.json] ).

Métodos clave utilizados:

Línea 5: -import thunk … +import { thunk }

Qué cambió: Importación de redux-thunk desde predeterminada (import thunk from) a nombrada (import { thunk } from).

En la versión 3.x de redux-thunk (^3.1.0 [fuente: export const thunk]) se eliminó la exportación predeterminada — ahora es obligatorio el export nombrado { thunk }.

configureMockStore([thunk])middleware que permite despachar funciones (thunks) en lugar de objetos simples. Sin él store.dispatch(login(...)) no funcionaría.

Sección 2: Importaciones desde './authentication' (líneas 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';

Eliminado de la importación:

Añadido a la importación:

Nota: El archivo de prueba antiguo importaba 5 símbolos que no existen en el authentication.js fuente (loadSessionIfAvailable, logout, USER_LOGOUT, userLogout, storeLoginSession). La prueba llevaba 7 años rota — los símbolos se eliminaron de la fuente (userLogout/logout eliminados Jul 2019, loadSessionIfAvailable renombrado Mar 2019), pero nunca se actualizó la prueba.

Sección 3: Configuración (líneas 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 movido al nivel del módulo — se creaba 3 veces (dentro de login, logout, check session), ahora una sola vez en la parte superior del archivo. [redux-mock-store]

Object.freeze()[MDN]. La prueba antigua congelaba username y password (innecesario — los primitivos son inmutables). En el archivo nuevo, Object.freeze solo se usa en objetos de estado en la prueba del reducer, donde realmente importa (protección contra mutaciones en una función pura).

afterEach(() => fetchMock.restore())beforeEach(() => vi.restoreAllMocks()) — reemplazo de la llamada específica de fetch-mock por la universal de Vitest vi.restoreAllMocks(). El reinicio se movió de 'after' a 'before' — garantiza que las simulaciones no se filtren entre pruebas.

Sección 4: Pruebas de los creadores de acciones (líneas 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' },
+ });
+ });
+});

Tres pruebas de creadores de acciones, agrupadas en describe('action creators').

PruebaAntiguaNuevaDiferencia
userLoginBegin 11 líneas, variable expectedAction 3 líneas, en línea Solo estilo. La lógica es la misma.
userLoginSuccess Llamada sin argumentos: userLoginSuccess(). Espera: { type: USER_LOGIN_SUCCESS } [línea 45] Llamada con 4 argumentos. Espera payload completo con user, version, buildTimestamp, alertMessage [nuevo] Corrección: userLoginSuccess recibe 4 parámetros obligatorios (authentication.js:11). La prueba antigua verificaba un comportamiento incompleto.
userLoginFailure 10 líneas, descripción con error tipográfico 'mark the login as success' [línea 49] 5 líneas, descripción correcta Error tipográfico corregido. La lógica es la misma.

🔍 Error tipográfico en el original: dos pruebas con el mismo nombre

Línea 40 — SUCCESS ✓ Línea 49 — FAILURE, pero el nombre dice "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);
});

Permaneció así durante 7 años. Corregido en la versión actualizada.

Sección 5: Prueba antigua de inicio de sesión sin keepSignedIn (líneas 61–97)

¿Qué es keepSignedIn / stayLoggedIn: La casilla de verificación "mantener sesión iniciada" en el formulario de login. Cuando está marcada, ocurre la siguiente cadena:

  1. Frontend: login(username, password, keepSignedIn) (authentication.js:65) → cuerpo POST: { stayLoggedIn: keepSignedIn } (línea 77)
  2. Controlador: LoginPageRest.kt recibe PostData<LoginData> (línea 96), campo stayLoggedIn en LoginData.kt:34
  3. Servicio: LoginService.authenticate() verifica if (loginData.stayLoggedIn == true) (LoginService.kt:175), genera un token STAY_LOGGED_IN_KEY (UserTokenType.kt:31) y llama a addStayLoggedInCookie() (líneas 177-178)
  4. Cookie: CookieService establece una cookie llamada "stayLoggedIn" (CookieService.kt:201) con vencimiento de 30 días (línea 200)
  5. Restauración: al inicio siguiente, si JSESSIONID expiró, LoginService.checkStayLoggedIn() (LoginService.kt:250) lee la cookie vía CookieService (CookieService.kt:65-75) — el servidor autentica al usuario sin contraseña

La prueba antigua intentaba verificar esto vía cookies.loadAll() — pero la cookie nunca llega a document.cookie en el entorno simulado (ver Sección 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- });

Problemas de la prueba antigua:

  1. fetchMock.mock(matcher, response) — API engorrosa: la función matcher analiza el cuerpo de la solicitud y verifica la URL por sí misma. [línea 62-80] [fuente fetch-mock]
  2. headers: { 'Set-Cookie': '...' } — simula el establecimiento de una cookie de sesión, pero fetch-mock no puede establecerlas realmente [línea 77]. Por qué: fetch-mock devuelve un objeto Response con encabezados — pero nunca escribe en document.cookie. Mientras tanto, react-cookies.loadAll() lee precisamente document.cookie = cookie.parse(document.cookie). La cadena: fetch-mock → Set-Cookie en headers → document.cookie intacto → loadAll() devuelve {} → la prueba pasa, pero verifica vacío, no comportamiento real.
  3. .catch({ throws: new Error('mock failed') }) — si ningún matcher coincidió — lanza un error. Un workaround que enmascara problemas reales [línea 80].
  4. expectedActions = [BEGIN, SUCCESS] — INCORRECTO. login() despacha BEGIN, luego llama a loadUserStatus()(dispatch), que despacha otro BEGIN y luego SUCCESS. Total debería ser 3 acciones, no 2. [authentication.js:65-84]loginloadUserStatus()(dispatch) dentro de .then().
  5. cookies.loadAll()toEqual({}) — prueba la librería de navegador react-cookies, no nuestra lógica [línea 94].

🏗 Análisis arquitectónico: por qué la prueba antigua estaba condenada

Capa de abstracción incorrecta. La prueba antigua intentaba verificar el comportamiento de cookies — pero las cookies son gestionadas por el navegador, no por fetch():

❌ Prueba mockeada (jsdom)
fetch-mock ──→  Response {
  headers: { 'Set-Cookie': 'JSESSIONID' }
}
      │
      │ Set-Cookie NO procesado
      ▼
document.cookie = ""

react-cookies.loadAll() ──→  {}
expect(cookies.loadAll()).toEqual({})  ✓
Prueba pasó — verificó el vacío, no el comportamiento
✓ Navegador real
fetch() ──→  HTTP Response {
  Set-Cookie: JSESSIONID=ABC
}
      │
      │ Navegador procesa Set-Cookie
      ▼
document.cookie = "JSESSIONID=ABC"

react-cookies.loadAll() ──→  { JSESSIONID: "ABC" }
Cookie visible — comportamiento real

Dos mundos de simulación incompatibles. fetch-mock y react-cookies operan en niveles fundamentalmente diferentes de la pila del navegador sin conexión en un entorno de prueba. fetch-mock reemplaza la capa HTTP (red), mientras que react-cookies lee la capa DOM (document.cookie). En un navegador real, un almacén de cookies se encuentra entre ellos — un componente del navegador que procesa Set-Cookie desde la respuesta HTTP y escribe en document.cookie. En jsdom con fetch simulado, esta capa no existe — la prueba verificaba una ilusión.

La prueba pasó por casualidad. El navegador no procesó Set-Cookiedocument.cookie vacío → cookies.loadAll() devolvió {}. ¡Pero la prueba esperaba {}! Dos ceros coincidiendo — vacío coincidía con vacío — creó la ilusión de una prueba funcional.

Límite de responsabilidad. Una prueba unitaria de frontend debería verificar qué acciones de Redux se despachan para diferentes respuestas fetch, no cómo el navegador procesa encabezados HTTP. Prueba correcta: "fetch devolvió 200 → se despacharon [BEGIN, BEGIN, SUCCESS]". Prueba incorrecta: "fetch devolvió Set-Cookie → verifica que la cookie llegó a document.cookie" — eso es una prueba de navegador, no de nuestro código.

Sección 6: Prueba antigua de inicio de sesión con keepSignedIn y error en /√/login (líneas 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- });

Error crítico en el original:

if (url !== '/√/login' || options.method !== 'POST') — URL escrita con /√/login (símbolo de raíz cuadrada U+221A), aunque la URL correcta es /rsPublic/login. [línea 103]

Esta prueba nunca se ejecutó realmente — la solicitud POST /rsPublic/login no coincidía con el matcher /√/login, por lo que caía en .catch({ throws: new Error('mock failed') }).

Sin embargo, la prueba podía pasar porque el error se suprimía en la cadena .then().catch()catchError en authentication.js:84 despacha FAILURE en lugar de re-lanzar la excepción.

Sección 7: Prueba antigua de fallo de inicio de sesión (líneas 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-});

Problemas:

  1. fetchMock.mock('/rsPublic/login', 401) — API antigua, solo URL y estado [línea 141]. Devolvía { status: 401 } sin ok: false, un handleHTTPErrors verifica response.ok [rest.js:32].
  2. payload: { error: 'Unauthorized' } — en el código real handleHTTPErrors lanza Error('Fetch failed: Error 401') [rest.js:34]. La prueba espera el mensaje incorrecto.

Sección 8: Nuevo describe('login') — inicio de sesión exitoso (líneas 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 — reemplazo de .then() por async/await. Más corto, más legible, los errores no se ocultan.

global.fetch = vi.fn().mockResolvedValueOnce() — dos llamadas fetch se simulan secuencialmente:

  1. Primera: respuesta a POST /rsPublic/login{} vacío
  2. Segunda: respuesta a GET /rs/userStatus (desde loadUserStatus() dentro de login()) — con userData, systemData, alertMessage

[BEGIN, BEGIN, SUCCESS] — CORREGIDO. Tres acciones:

La prueba antigua esperaba 2 ([BEGIN, SUCCESS]) — faltaba el segundo BEGIN.

Sección 9: Nuevo describe('login') — contraseña incorrecta y red (líneas 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' } },
+ ]);
+ });

Contraseña incorrecta (401):

  • mockResolvedValue (sin Once) — todas las llamadas devuelven 401. loadUserStatus() no se llama (el login falló antes).
  • error: 'Fetch failed: Error 401' correcto — entonces handleHTTPErrors lanza Error('Fetch failed: Error ${status}').
  • La prueba antigua esperaba 'Unauthorized' — incorrecto.
@@ -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' } },
+ ]);
+ });
+});

Fallo de red — PRUEBA NUEVA (no existía en el archivo antiguo):

mockRejectedValue(new Error('Network error'))[documentación vitest]. Simula un error de red/DNS. fetch no devuelve respuesta, en su lugar lanza una excepción. login()fetch(...) → red caida → .catch(catchError(dispatch))dispatch(USER_LOGIN_FAILURE('Network error')). [authentication.js:84]

Sección 10: describe('logout') eliminado (líneas 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-});

Eliminado por completo (2 pruebas, ~30 líneas).

Razones:

  1. userLogout() devuelve { type: USER_LOGOUT } — trivial, mismo nivel que creador de acciones. USER_LOGOUT no se exporta desde authentication.js [solo 3 tipos].
  2. logout() despacha una sola acción — sin código asíncrono.
  3. Ambas dependen de cookies.loadAll() (react-cookies), que se eliminó.
  4. La funcionalidad de cierre de sesión está cubierta por USER_LOGIN_BEGIN (reinicialización del estado).

Sección 11: loadUserStatus de antiguo → nuevo (líneas 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 clave: 7 años.

// TODO: ADD AUTHENTICATION TEST ENDPOINT [línea 211]añadido el 17-03-2019, permaneció así 7 años hasta que el PR 7e78f3741 lo resolvió. La prueba antigua loadSessionIfAvailable solo devolvía estado 200, sin cuerpo JSON. response.json() habría fallado. La prueba nunca se ejecutó.

'creates no action at all' eliminado — verificaba que loadSessionIfAvailable() devolvía null sin simulación. En el código actual loadUserStatus() siempre realiza un fetch.

Nuevo describe('loadUserStatus') — sesión válida (líneas 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,
+ },
+ },
+ ]);
+ });

Respuesta JSON correcta: json: () => Promise.resolve({ userData, systemData, alertMessage }) — la estructura coincide con lo que loadUserStatus() espera en authentication.js:42.

alertMessage: 'Some alert' — verifica la propagación del mensaje de alerta (era undefined en la prueba de login — diferentes caminos de código).

Nuevo describe('loadUserStatus') — sesión expirada (líneas 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 } });
+ });
+});

Prueba nueva — no existía en el archivo antiguo.

payload: { error: undefined } — peculiaridad de loadUserStatus(): en el manejador catch se llama catchError(dispatch)({ message: undefined }). catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message))error.message es undefined porque se pasa el objeto { message: undefined }.

Verificación vía actions[0] y actions[1] (no toEqual de todo el array) — porque loadUserStatus() en error ejecuta window.location.href = (redirección a /login), lo cual puede no funcionar en jsdom.

Conceptos

Thunk de Redux
Middleware que permite despachar funciones (no solo objetos). La función recibe (dispatch, getState) — puede realizar fetch y despachar múltiples acciones. login() y loadUserStatus() son thunks: devuelven function(dispatch) { ... }. Sin el middleware redux-thunk, Redux los rechazaría. [documentación]
Creador de acciones vs Thunk
Un creador de acciones (userLoginBegin()) devuelve un objeto { type, payload } — se despacha síncronamente. Un thunk (login(username, password)) devuelve una función — el middleware la ejecuta asíncronamente, realiza fetch y despacha múltiples acciones. Dos capas diferentes: creadores = fábrica síncrona de objetos, thunks = orquestador asíncrono.
JSESSIONID
Cookie de sesión estándar de Java. No creada por el código de ProjectForge — la establece el contenedor de servlets (Tomcat/Jetty) en la primera request.getSession(true). ProjectForge gestiona su ciclo de vida:
  • Después del login — invalidación y recreación (protección contra fijación de sesión): LoginService.kt:333-342
    // Fijación de Sesión: Cambiar JSESSIONID después del login
    request.getSession(false)?.let { session ->
        if (!session.isNew) { session.invalidate() }
    }
    val session = request.getSession(true)  // ← nueva JSESSIONID
    session.setAttribute(SESSION_KEY_USER, userContext)
  • Envío de vuelta — cada fetch() envía la cookie vía credentials: 'include' en authentication.js:37:
    fetch(getServiceURL('userStatus'), {
        method: 'GET',
        credentials: 'include',  // ← envía JSESSIONID al servidor
    })
Cuando expira — la sesión muere, GET /rs/userStatus devuelve 401. La cookie es HTTP-only: JavaScript no puede leerla vía document.cookie. [Wikipedia: Session ID]
KEEP_SIGNED_IN / stayLoggedIn
Casilla de verificación "mantener sesión iniciada" en el formulario de login. Frontend: parámetro keepSignedIn en authentication.js:65 → se envía como { stayLoggedIn: true } en el cuerpo POST (línea 77). El servidor genera el token STAY_LOGGED_IN_KEY y establece una cookie llamada "stayLoggedIn" (CookieService.kt:201). KEEP_SIGNED_IN — nombre antiguo del lado del cliente, eliminado en Mar 2019. La prueba antigua usaba exactamente ese (cookies.save('KEEP_SIGNED_IN', ...)) — nombre incorrecto + fetch-mock no establece cookies = la prueba verificaba vacío.
Cookie HTTP-only
Cookie con la bandera HttpOnly. El navegador la envía al servidor, pero JavaScript no tiene acceso — document.cookie no la incluye. JSESSIONID es HTTP-only (protección contra XSS). Por eso cookies.loadAll() (que lee document.cookie) nunca ve la cookie de sesión. [MDN]
vi.fn()
Constructor de función simulada de Vitest. Equivalente a jest.fn(). mockResolvedValueOnce(x) — la próxima llamada devuelve Promise.resolve(x) (cadena). mockResolvedValue(x)todas las llamadas devuelven Promise.resolve(x). mockRejectedValue(e) — la llamada lanza un error (fallo de red). La distinción Once / no-Once es crítica: en la prueba de login, dos llamadas fetch se simulan secuencialmente. [vitest]
import.meta.env.DEV / MODE
Reemplazo de Vite para process.env.NODE_ENV. import.meta.env.DEVtrue en modo desarrollo. import.meta.env.MODE — string: 'development', 'production' o 'test'. Reemplazo necesario porque process.env.NODE_ENV de CRA no existe en Vite. En rest.js:10: DEV && MODE !== 'test' — servidor de desarrollo, pero no el runner de pruebas. [documentación vite]
connect() → useSelector/useDispatch
Dos formas de conectar componentes React con la tienda Redux. connect(mapStateToProps)(Component) — patrón HOC (pre-2019), envuelve el componente. useSelector(state => state.user) + useDispatch() — hooks (React 16.8+), más simples, amigables con TypeScript, optimizables por tree-shaking. loggedIn: boolean cambió a user: object|null precisamente por el cambio a hooks: el selector state => state.authentication.user !== null lee un campo específico en lugar de una bandera. [react-redux]
loadUserStatus()
Thunk llamado al inicio de la aplicación y después del login. Realiza GET /rs/userStatus con cookie (credentials: 'include'). El servidor verifica la sesión: si es válida — devuelve { userData, systemData, alertMessage } (200). Si expiró — 401 → redirección a /react/public/login. Esto es lo primero que hace React al cargar (ver ProjectForge.jsx): "¿quién soy?". [authentication.js:30]

Resumen de cambios

AspectoArchivo antiguo (237 líneas)Archivo nuevo (157 líneas)
Mock fetch fetch-mock [línea 2] vi.fn() [vitest]
Cookies react-cookies [línea 3], tests stayLoggedIn Eliminado — el navegador no se prueba
Estilo async Promise .then() [MDN] async/await [MDN]
Estructura describe 3 bloques: login, logout, check session 3 bloques: action creators, login, loadUserStatus
login válido 2: [BEGIN, SUCCESS]incorrecto [línea 82] 3: [BEGIN, BEGIN, SUCCESS]correcto [auth.js:62-84]
login credenciales incorrectas 'Unauthorized' — bug URL /√/login [línea 103] 'Fetch failed: Error 401' — URL correcta [rest.js:32-38]
Fallo de red ❌ Sin test mockRejectedValue [línea 96]
loadUserStatus sesión válida TODO: 7 años, nunca funcionó [línea 211] ✅ Test completo [líneas 108-140]
loadUserStatus sesión expirada ❌ Sin test ✅ 401 → FAILURE [líneas 142-157]
logout 2 tests, símbolo no existe en el código fuente [eliminado Jul 2019] Eliminado — acción trivial, cubierta por USER_LOGIN_BEGIN

— enlace al código en la rama fix/vite-eslint-upgrade, no fusionado en develop. El código puede no estar disponible en GitHub hasta que se fusione el PR.