git diff origin/develop -- actions/authentication.test.jse67067aa7) point to the fork MaurerAnton/projectforge (branch draft43npm). Links to old code (SHA 9ed5fbe0f) — to the main repository micromata/projectforge (develop).
-import fetchMock from 'fetch-mock/es5/client'-import cookies from 'react-cookies'Before: Library for reading/writing cookies in the browser react-cookies@0.1.1 (added 2019-03-09, package.json develop [source on GitHub]). Was used in old tests to check KEEP_SIGNED_IN (line 94, line 132, line 183).
Why deleted: Cookies are HTTP-only, JavaScript cannot read them (but the test called cookies.loadAll(), which calls cookie.parse(document.cookie) — real browser cookies, not a mock). fetch-mock puts Set-Cookie into the Response headers, but document.cookie is never populated. So cookies.loadAll() always returns {} — emptiness. The test passed not because no cookies existed, but because fetch-mock and react-cookies live in separate worlds. react-cookies is no longer in the dependencies.
+import { vi } from 'vitest'After: Vitest's mock API. Equivalent to jest.fn() [Jest docs], but from Vitest itself [vitest.dev] (version ^4.1.5 [package.json] ⚡).
Key methods used:
vi.fn() — creates a mock function (replaces global.fetch) — L49, L78, L95, L118, L143fn.mockResolvedValueOnce(value) — next call returns Promise.resolve(value) (cascade of two) — L50, L53fn.mockResolvedValue(value) — all calls return Promise.resolve(value) — L79, L119, L144fn.mockRejectedValue(error) — call throws (network failure) — L96vi.restoreAllMocks() — resets all mocks between tests — L42, L110-import thunk … +import { thunk }What changed: Import of redux-thunk from default (import thunk from) to named (import { thunk } from).
In redux-thunk version 3.x (^3.1.0 [source: export const thunk]) the default export was removed — the named export is now required { thunk }.
configureMockStore([thunk]) — middleware, which enables dispatches? functions (thunks) instead of plain objects. Without it store.dispatch(login(...)) won't work.
Removed from import:
authentication.js this function does not exist — it was renamed to loadUserStatus (line 30, renamed 2019-03-18).describe('logout'). Function logout does not exist in the current authentication.js [source] (deleted 2019-07-13).authentication.js [only 3 types] (deleted 2019-07-13).Added to import:
loadSessionIfAvailable. to? function b authentication.js:30.authentication.js (loadSessionIfAvailable, logout, USER_LOGOUT, userLogout, storeLoginSession). The test was broken for 7 years — symbols were removed from the source (userLogout/logout removed Jul 2019, loadSessionIfAvailable renamed Mar 2019), but the test was never updated.
mockStore moved to module level — was created 3 times (inside login, logout, check session), now once at the top of the file. [redux-mock-store]
Object.freeze() — [MDN]. The old test froze username u password (pointless — primitives are immutable). In the new file, Object.freeze is only used on state objects in the reducer test, where it actually matters (mutation protection in a pure function).
afterEach(() => fetchMock.restore()) → beforeEach(() => vi.restoreAllMocks()) — replacement of the fetch-mock-specific call with the universal Vitest vi.restoreAllMocks(). Reset moved from 'after' to 'before' — ensures mocks don't leak between tests.
Three action creator tests, grouped in describe('action creators').
| Test | Old | New | Difference |
|---|---|---|---|
userLoginBegin |
11 lines, expectedAction variable | 3 lines, inline | Style only. Logic unchanged. |
userLoginSuccess |
Call without arguments: userLoginSuccess(). Expects: { type: USER_LOGIN_SUCCESS } [line 45] |
Call with 4 arguments. Expects full payload with user, version, buildTimestamp, alertMessage [new]⚡ | Fix: userLoginSuccess takes 4 required parameters (authentication.js:11). The old test verified incomplete behavior. |
userLoginFailure |
10 lines, description with typo 'mark the login as success' [line 49] | 5 lines, correct description | Typo fixed. Logic unchanged. |
| Line 40 — SUCCESS ✓ | Line 49 — FAILURE, but name says "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);
}); |
Hung for 7 years like this. Fixed in the updated version.
What is keepSignedIn / stayLoggedIn: The "stay logged in" checkbox on the login form. When checked, the following chain occurs:
login(username, password, keepSignedIn) (authentication.js:65) → POST body: { stayLoggedIn: keepSignedIn } (line 77)LoginPageRest.kt receives PostData<LoginData> (line 96), field stayLoggedIn in LoginData.kt:34LoginService.authenticate() checks if (loginData.stayLoggedIn == true) (LoginService.kt:175), generates a STAY_LOGGED_IN_KEY token (UserTokenType.kt:31) and calls addStayLoggedInCookie() (line 177-178)CookieService sets a cookie named "stayLoggedIn" (CookieService.kt:201) with 30-day expiry (line 200)JSESSIONID expired, LoginService.checkStayLoggedIn() (LoginService.kt:250) reads the cookie via CookieService (CookieService.kt:65-75) — server authenticates the user without a passwordThe old test tried to check this via cookies.loadAll() — but the cookie never reaches document.cookie in the mock environment (see Section 2).
Problems with the old test:
fetchMock.mock(matcher, response) — clunky API: the matcher function parses the request body and checks the URL itself. [line 62-80] [fetch-mock source]headers: { 'Set-Cookie': '...' } — simulates setting a session cookie, but fetch-mock cannot actually set them [line 77]. Why: fetch-mock returns a Response object with headers — but never writes to document.cookie. Meanwhile, react-cookies.loadAll() reads precisely document.cookie = cookie.parse(document.cookie). The chain: fetch-mock → Set-Cookie in headers → document.cookie untouched → loadAll() returns {} → test passes, but verifies emptiness, not real behavior..catch({ throws: new Error('mock failed') }) — if no matcher matched — throw an error. A workaround that masks real problems [line 80].login() dispatches BEGIN, then calls loadUserStatus()(dispatch), which dispatches another BEGIN and then SUCCESS. Total should be 3 actions, not 2. [authentication.js:65-84] — login → loadUserStatus()(dispatch) inside .then().cookies.loadAll() → toEqual({}) — tests the react-cookies browser library, not the application logic [line 94].Wrong abstraction layer. The old test tried to verify cookie behavior — but cookies are managed by the browser, not by fetch():
Two incompatible mock worlds. fetch-mock and react-cookies operate at fundamentally different levels of the browser stack with no connection in a test environment. fetch-mock replaces the HTTP layer (network), while react-cookies reads the DOM layer (document.cookie). In a real browser, a cookie jar sits between them — a browser component that processes Set-Cookie from the HTTP response and writes to document.cookie. In jsdom with mocked fetch, this layer doesn't exist — the test was verifying an illusion.
The test passed by coincidence. The browser didn't process Set-Cookie → document.cookie empty → cookies.loadAll() returned {}. But the test expected {}! Two zeros matching — emptiness matched emptiness — created the illusion of a working test.
Boundary of responsibility. A frontend unit test should verify which Redux actions are dispatched for different fetch responses, not how the browser processes HTTP headers. Correct test: "fetch returned 200 → dispatched [BEGIN, BEGIN, SUCCESS]". Incorrect: "fetch returned Set-Cookie → verify the cookie landed in document.cookie" — that's a browser test, not application code.
Critical bug in the original:
if (url !== '/√/login' || options.method !== 'POST') — URL written as /√/login (square-root symbol U+221A), even though the correct URL is /rsPublic/login. [line 103]
This test never actually ran — the request POST /rsPublic/login didn't match the matcher /√/login, therefore fell into .catch({ throws: new Error('mock failed') }).
However, the test could pass because the error was suppressed in the .then().catch() chain — catchError b authentication.js:84 dispatches FAILURE instead of rethrowing the exception.
Problems:
fetchMock.mock('/rsPublic/login', 401) — old API, just URL and status [line 141]. Returned { status: 401 } without ok: false, a handleHTTPErrors checks response.ok [rest.js:32].payload: { error: 'Unauthorized' } — in the actual code handleHTTPErrors throws Error('Fetch failed: Error 401') [rest.js:34]. The test expects the wrong message.async/await — replacement of .then() with async/await. Shorter, more readable, errors are not swallowed.
global.fetch = vi.fn().mockResolvedValueOnce() — two fetch calls are mocked sequentially:
POST /rsPublic/login — empty {}GET /rs/userStatus (from loadUserStatus() inside login()) — with userData, systemData, alertMessage[BEGIN, BEGIN, SUCCESS] — FIXED. Three actions:
BEGIN — from login() (authentication.js:65)BEGIN — from loadUserStatus() (authentication.js:30)SUCCESS — from loadUserStatus() (authentication.js:42)The old test expected 2 ([BEGIN, SUCCESS]) — the second BEGIN was missing.
Wrong password (401):
mockResolvedValue (without Once) — all calls return 401. loadUserStatus() is not called (login failed earlier).error: 'Fetch failed: Error 401' — theny that handleHTTPErrors throws Error('Fetch failed: Error ${status}').'Unauthorized' — wrong.Network failure — NEW TEST (was not in the old file):
mockRejectedValue(new Error('Network error')) — [vitest docs]. Simulates a network/DNS error. fetch returns no response, instead throws an exception. login() → fetch(...) → network down → .catch(catchError(dispatch)) → dispatch(USER_LOGIN_FAILURE('Network error')). [authentication.js:84]
Deleted entirely (2 tests, ~30 lines).
Reasons:
userLogout() returns { type: USER_LOGOUT } — trivial, same level as action creator. USER_LOGOUT he exported from authentication.js [only 3 types].logout() dispatches a single action — no async code.cookies.loadAll() (react-cookies), which is removed.USER_LOGIN_BEGIN (state reset).Key TODO: 7 years.
// TODO: ADD AUTHENTICATION TEST ENDPOINT [line 211] — added 2019-03-17, hung for 7 years until PR 7e78f3741 resolved it. The old test loadSessionIfAvailable only returned status 200, without a JSON body. response.json() would break. The test never ran.
'creates no action at all' deleted — verified that loadSessionIfAvailable() returns null without a mock. In the current code loadUserStatus() always performs a fetch.
describe('loadUserStatus') — valid session (lines 108–140)Correct JSON response: json: () => Promise.resolve({ userData, systemData, alertMessage }) — structure matches what loadUserStatus() expects in authentication.js:42.
alertMessage: 'Some alert' — verifies the alert message propagation (it was undefined in the login test — different code paths).
describe('loadUserStatus') — expired session (lines 142–157)New test — did not exist in the old file.
payload: { error: undefined } — peculiarity of loadUserStatus(): in the catch handler is called catchError(dispatch)({ message: undefined }). catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message)) — error.message this is undefined because an object { message: undefined } is passed.
Check via actions[0] and actions[1] (not toEqual of the entire array) — because loadUserStatus() on error executes window.location.href = (redirect to /login), which may not work in jsdom.
(dispatch, getState) — can fetch and dispatch multiple actions. login() and loadUserStatus() are thunks: they return function(dispatch) { ... }. Without redux-thunk middleware, Redux would reject them. [documentation]userLoginBegin()) returns an object { type, payload } — dispatched synchronously. A thunk (login(username, password)) returns a function — middleware runs it asynchronously, it fetches and dispatches multiple actions. Two different layers: creators = synchronous object factory, thunks = asynchronous orchestrator.request.getSession(true). ProjectForge manages its lifecycle:
// Session Fixation: Change JSESSIONID after login
request.getSession(false)?.let { session ->
if (!session.isNew) { session.invalidate() }
}
val session = request.getSession(true) // ← new JSESSIONID
session.setAttribute(SESSION_KEY_USER, userContext)fetch() sends the cookie via credentials: 'include' in authentication.js:37:fetch(getServiceURL('userStatus'), {
method: 'GET',
credentials: 'include', // ← sends JSESSIONID to server
})GET /rs/userStatus returns 401. Cookie is HTTP-only: JavaScript cannot read it via document.cookie. [Wikipedia: Session ID]
keepSignedIn in authentication.js:65 → sent as { stayLoggedIn: true } in the POST body (line 77). Server generates STAY_LOGGED_IN_KEY token and sets cookie named "stayLoggedIn" (CookieService.kt:201). KEEP_SIGNED_IN — old client-side constant name, removed in Mar 2019. The old test used exactly this (cookies.save('KEEP_SIGNED_IN', ...)) — wrong name + fetch-mock doesn't set cookies = test was verifying emptiness.HttpOnly flag. Browser sends it to the server, but JavaScript has no access — document.cookie doesn't include it. JSESSIONID is HTTP-only (XSS protection). This is why cookies.loadAll() (which reads document.cookie) never sees the session cookie. [MDN]jest.fn(). mockResolvedValueOnce(x) — next call returns Promise.resolve(x) (cascade). mockResolvedValue(x) — all calls return Promise.resolve(x). mockRejectedValue(e) — call throws (network failure). The Once / non-Once distinction is critical: in the login test, two fetch calls are mocked sequentially. [vitest]process.env.NODE_ENV. import.meta.env.DEV — true in dev mode. import.meta.env.MODE — string: 'development', 'production', or 'test'. Replacement needed because CRA's process.env.NODE_ENV doesn't exist in Vite. In rest.js:10: DEV && MODE !== 'test' — dev server, but not test runner. [vite docs]connect(mapStateToProps)(Component) — HOC pattern (pre-2019), wraps the component. useSelector(state => state.user) + useDispatch() — hooks (React 16.8+), simpler, TypeScript-friendly, tree-shakeable. loggedIn: boolean changed to user: object|null precisely due to the hooks switch: the selector state => state.authentication.user !== null reads a specific field instead of a flag. [react-redux]GET /rs/userStatus with cookie (credentials: 'include'). Server checks the session: if valid — returns { userData, systemData, alertMessage } (200). If expired — 401 → redirect to /react/public/login. This is the first thing React does on load (see ProjectForge.jsx): "who am I?" [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 |
⚡ — link to code b branch fix/vite-eslint-upgrade, not merged into develop. Code may not be available on GitHub until the PR is merged.
Before: The old test used
fetch-mock— an external library for mockingfetch(added 2019-03-15). Versiones5/clientwas needed for compatibility with old Jest/CRA. [develop source]After: Deleted. Three reasons:
vi.fn()— no separate package needed.fetch-mockversion^12.6.0[package.json develop] has an API incompatible with modern fetch standards and requires polyfills [fetch-mock source on GitHub].fetch-mock— one less package, fewer vulnerabilities.