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

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

Date: 2026-05-08

Compared commits:

This file: old version on GitHub (237 lines, develop, created 2019-03-15) → new version (157 lines)

Note: Links to new code (SHA e67067aa7) point to the fork MaurerAnton/projectforge (branch draft43npm). Links to old code (SHA 9ed5fbe0f) — to the main repository micromata/projectforge (develop).

Section 1: Imports (lines 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';

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

Before: The old test used fetch-mock — an external library for mocking fetch (added 2019-03-15). Version es5/client was needed for compatibility with old Jest/CRA. [develop source]

After: Deleted. Three reasons:

  1. Vitest bundles built-in vi.fn() — no separate package needed.
  2. fetch-mock version ^12.6.0 [package.json develop] has an API incompatible with modern fetch standards and requires polyfills [fetch-mock source on GitHub].
  3. Y toc no longer depends on fetch-mock — one less package, fewer vulnerabilities.

Line 3: -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.

Line 3 (new): +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:

Line 5: -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.

Section 2: Imports from './authentication' (lines 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';

Removed from import:

Added to import:

Note: The old test file imported 5 symbols that don't exist in the source 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.

Section 3: Configuration (lines 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 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.

Section 4: Action creator tests (lines 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' },
+ });
+ });
+});

Three action creator tests, grouped in describe('action creators').

TestOldNewDifference
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.

🔍 Typo in the original: two tests with identical name

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.

Section 5: Old login test without keepSignedIn (lines 61–97)

What is keepSignedIn / stayLoggedIn: The "stay logged in" checkbox on the login form. When checked, the following chain occurs:

  1. Frontend: login(username, password, keepSignedIn) (authentication.js:65) → POST body: { stayLoggedIn: keepSignedIn } (line 77)
  2. Controller: LoginPageRest.kt receives PostData<LoginData> (line 96), field stayLoggedIn in LoginData.kt:34
  3. Service: LoginService.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)
  4. Cookie: CookieService sets a cookie named "stayLoggedIn" (CookieService.kt:201) with 30-day expiry (line 200)
  5. Restore: on next launch, if JSESSIONID expired, LoginService.checkStayLoggedIn() (LoginService.kt:250) reads the cookie via CookieService (CookieService.kt:65-75) — server authenticates the user without a password

The old test tried to check this via cookies.loadAll() — but the cookie never reaches document.cookie in the mock environment (see 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- });

Problems with the old test:

  1. fetchMock.mock(matcher, response) — clunky API: the matcher function parses the request body and checks the URL itself. [line 62-80] [fetch-mock source]
  2. 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.
  3. .catch({ throws: new Error('mock failed') }) — if no matcher matched — throw an error. A workaround that masks real problems [line 80].
  4. expectedActions = [BEGIN, SUCCESS] — INCORRECT. 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]loginloadUserStatus()(dispatch) inside .then().
  5. cookies.loadAll()toEqual({}) — tests the react-cookies browser library, not the application logic [line 94].

🏗 Architectural analysis: why the old test was doomed

Wrong abstraction layer. The old test tried to verify cookie behavior — but cookies are managed by the browser, not by fetch():

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

react-cookies.loadAll() ──→  {}
expect(cookies.loadAll()).toEqual({})  ✓
Test passed — verified emptiness, not behavior
✓ Real Browser
fetch() ──→  HTTP Response {
  Set-Cookie: JSESSIONID=ABC
}
      │
      │ Browser processes Set-Cookie
      ▼
document.cookie = "JSESSIONID=ABC"

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

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-Cookiedocument.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.

Section 6: Old login test with keepSignedIn and /√/login bug (lines 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- });

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.

Section 7: Old login failure test (lines 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-});

Problems:

  1. 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].
  2. payload: { error: 'Unauthorized' } — in the actual code handleHTTPErrors throws Error('Fetch failed: Error 401') [rest.js:34]. The test expects the wrong message.

Section 8: New describe('login') — successful login (lines 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 — replacement of .then() with async/await. Shorter, more readable, errors are not swallowed.

global.fetch = vi.fn().mockResolvedValueOnce() — two fetch calls are mocked sequentially:

  1. First: response to POST /rsPublic/login — empty {}
  2. Second: response to GET /rs/userStatus (from loadUserStatus() inside login()) — with userData, systemData, alertMessage

[BEGIN, BEGIN, SUCCESS] — FIXED. Three actions:

The old test expected 2 ([BEGIN, SUCCESS]) — the second BEGIN was missing.

Section 9: New describe('login') — wrong password and network (lines 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' } },
+ ]);
+ });

Wrong password (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' } },
+ ]);
+ });
+});

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]

Section 10: describe('logout') deleted (lines 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-});

Deleted entirely (2 tests, ~30 lines).

Reasons:

  1. userLogout() returns { type: USER_LOGOUT } — trivial, same level as action creator. USER_LOGOUT he exported from authentication.js [only 3 types].
  2. logout() dispatches a single action — no async code.
  3. Both depend on cookies.loadAll() (react-cookies), which is removed.
  4. Logout functionality is covered by USER_LOGIN_BEGIN (state reset).

Section 11: Old → New loadUserStatus (lines 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-});

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.

New describe('loadUserStatus') — valid session (lines 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,
+ },
+ },
+ ]);
+ });

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).

New describe('loadUserStatus') — expired session (lines 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 } });
+ });
+});

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.

Concepts

Redux thunk
Middleware enabling dispatch of functions (not just objects). The function receives (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]
Action creator vs Thunk
An action creator (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.
JSESSIONID
Standard Java session cookie. Not created by ProjectForge code — set by the servlet container (Tomcat/Jetty) on the first request.getSession(true). ProjectForge manages its lifecycle:
  • After login — invalidation and recreation (session fixation protection): 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)  // ← new JSESSIONID
    session.setAttribute(SESSION_KEY_USER, userContext)
  • Sending back — every fetch() sends the cookie via credentials: 'include' in authentication.js:37:
    fetch(getServiceURL('userStatus'), {
        method: 'GET',
        credentials: 'include',  // ← sends JSESSIONID to server
    })
When it expires — session dead, GET /rs/userStatus returns 401. Cookie is HTTP-only: JavaScript cannot read it via document.cookie. [Wikipedia: Session ID]
KEEP_SIGNED_IN / stayLoggedIn
"Stay logged in" checkbox on the login form. Frontend: parameter 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_INold 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.
HTTP-only cookie
Cookie with the 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]
vi.fn()
Vitest's mock function constructor. Equivalent to 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]
import.meta.env.DEV / MODE
Vite's replacement for process.env.NODE_ENV. import.meta.env.DEVtrue 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() → useSelector/useDispatch
Two ways to connect React components to Redux store. 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]
loadUserStatus()
Thunk called on app startup and after login. Performs 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]

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

— 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.