git diff origin/develop -- actions/authentication.test.jse67067aa7) apuntan al fork MaurerAnton/projectforge (rama draft43npm). Los enlaces al código antiguo (SHA 9ed5fbe0f) apuntan al repositorio principal micromata/projectforge (develop).
-import fetchMock from 'fetch-mock/es5/client'-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.
+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:
vi.fn() — crea una función simulada (reemplaza global.fetch) — L49, L78, L95, L118, L143fn.mockResolvedValueOnce(valor) — la próxima llamada devuelve Promise.resolve(valor) (cadena de dos) — L50, L53fn.mockResolvedValue(valor) — todas las llamadas devuelven Promise.resolve(valor) — L79, L119, L144fn.mockRejectedValue(error) — la llamada lanza un error (fallo de red) — L96vi.restoreAllMocks() — reinicia todas las simulaciones entre pruebas — L42, L110-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.
Eliminado de la importación:
authentication.js actual esta función no existe — fue renombrada a loadUserStatus (línea 30, renombrada el 18-03-2019).describe('logout'). La función logout no existe en el authentication.js actual [fuente] (eliminada el 13-07-2019).authentication.js [solo 3 tipos] (eliminada el 13-07-2019).Añadido a la importación:
loadSessionIfAvailable. Función en authentication.js:30.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.
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.
Tres pruebas de creadores de acciones, agrupadas en describe('action creators').
| Prueba | Antigua | Nueva | Diferencia |
|---|---|---|---|
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. |
| 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.
¿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:
login(username, password, keepSignedIn) (authentication.js:65) → cuerpo POST: { stayLoggedIn: keepSignedIn } (línea 77)LoginPageRest.kt recibe PostData<LoginData> (línea 96), campo stayLoggedIn en LoginData.kt:34LoginService.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)CookieService establece una cookie llamada "stayLoggedIn" (CookieService.kt:201) con vencimiento de 30 días (línea 200)JSESSIONID expiró, LoginService.checkStayLoggedIn() (LoginService.kt:250) lee la cookie vía CookieService (CookieService.kt:65-75) — el servidor autentica al usuario sin contraseñaLa 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).
Problemas de la prueba antigua:
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]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..catch({ throws: new Error('mock failed') }) — si ningún matcher coincidió — lanza un error. Un workaround que enmascara problemas reales [línea 80].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] — login → loadUserStatus()(dispatch) dentro de .then().cookies.loadAll() → toEqual({}) — prueba la librería de navegador react-cookies, no nuestra lógica [línea 94].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():
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-Cookie → document.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.
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.
Problemas:
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].payload: { error: 'Unauthorized' } — en el código real handleHTTPErrors lanza Error('Fetch failed: Error 401') [rest.js:34]. La prueba espera el mensaje incorrecto.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:
POST /rsPublic/login — {} vacíoGET /rs/userStatus (desde loadUserStatus() dentro de login()) — con userData, systemData, alertMessage[BEGIN, BEGIN, SUCCESS] — CORREGIDO. Tres acciones:
BEGIN — desde login() (authentication.js:65)BEGIN — desde loadUserStatus() (authentication.js:30)SUCCESS — desde loadUserStatus() (authentication.js:42)La prueba antigua esperaba 2 ([BEGIN, SUCCESS]) — faltaba el segundo BEGIN.
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}').'Unauthorized' — incorrecto.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]
Eliminado por completo (2 pruebas, ~30 líneas).
Razones:
userLogout() devuelve { type: USER_LOGOUT } — trivial, mismo nivel que creador de acciones. USER_LOGOUT no se exporta desde authentication.js [solo 3 tipos].logout() despacha una sola acción — sin código asíncrono.cookies.loadAll() (react-cookies), que se eliminó.USER_LOGIN_BEGIN (reinicialización del estado).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.
describe('loadUserStatus') — sesión válida (líneas 108–140)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).
describe('loadUserStatus') — sesión expirada (líneas 142–157)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.
(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]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.request.getSession(true). ProjectForge gestiona su ciclo de vida:
// 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)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
})GET /rs/userStatus devuelve 401. La cookie es HTTP-only: JavaScript no puede leerla vía document.cookie. [Wikipedia: Session ID]
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.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]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]process.env.NODE_ENV. import.meta.env.DEV — true 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(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]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]| Aspecto | Archivo 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.
Antes: La prueba antigua usaba
fetch-mock— una librería externa para simularfetch(añadida el 15-03-2019). La versiónes5/clientera necesaria para compatibilidad con Jest/CRA antiguos. [fuente develop]Después: Eliminado. Tres razones:
vi.fn()integrado — no se necesita un paquete separado.fetch-mockversió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].fetch-mock— un paquete menos, menos vulnerabilidades.