⚠️ Entwurf der Dokumentation. Kann Tippfehler und Ungenauigkeiten enthalten.
EN | DE | RU | FR | ES

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

Datum: 2026-05-08

Verglichene Commits:

Diese Datei: alte Version auf GitHub (237 Zeilen, develop, erstellt 2019-03-15) → neue Version (157 Zeilen)

Hinweis: Links auf neuen Code (SHA e67067aa7) zeigen auf den Fork MaurerAnton/projectforge (Branch draft43npm). Links auf alten Code (SHA 9ed5fbe0f) — auf das Haupt-Repository micromata/projectforge (develop).

Abschnitt 1: Imports (Zeilen 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';

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

Vorher: Der alte Test verwendete fetch-mock — eine externe Bibliothek zum Mocken von fetch (hinzugefügt 2019-03-15). Die Version es5/client wurde für die Kompatibilität mit altem Jest/CRA benötigt. [Quellcode develop]

Nachher: Gelöscht. Drei Gründe:

  1. Vitest liefert vi.fn() direkt mit — kein separates Paket nötig.
  2. fetch-mock Version ^12.6.0 [package.json develop] hat eine API, die mit modernen Fetch-Standards inkompatibel ist und Polyfills benötigt [fetch-mock source on GitHub].
  3. Keine fetch-mock-Abhängigkeit mehr — ein Paket weniger, weniger Schwachstellen.

Zeile 3: -import cookies from 'react-cookies'

Vorher: Bibliothek zum Lesen/Schreiben von Cookies im Browser react-cookies@0.1.1 (hinzugefügt 2019-03-09, package.json develop [source on GitHub]). Wurde in alten Tests zur Prüfung von KEEP_SIGNED_IN verwendet (Zeile 94, Zeile 132, Zeile 183).

Warum gelöscht: Cookies sind HTTP-only, JavaScript kann sie nicht lesen (im Test wurde cookies.loadAll() aufgerufen, das cookie.parse(document.cookie) aufruft — echte Browser-Cookies, kein Mock). fetch-mock legt Set-Cookie in die Response-Header, aber document.cookie wird nie befüllt. Also gibt cookies.loadAll() immer {} zurück — Leere. Der Test bestand nicht, weil keine Cookies da waren, sondern weil fetch-mock und react-cookies in getrennten Welten leben. react-cookies ist nicht mehr in den Abhängigkeiten.

Zeile 3 (neu): +import { vi } from 'vitest'

Nachher: Vitests Mock-API. Äquivalent zu jest.fn() [Jest docs], sondern direkt von Vitest selbst [vitest.dev] (Version ^4.1.5 [package.json] ).

Wichtigste verwendete Methoden:

Zeile 5: -import thunk … +import { thunk }

Was sich geändert hat: Import von redux-thunk von default (import thunk from) auf named (import { thunk } from).

In redux-thunk Version 3.x (^3.1.0 [source: export const thunk]) wurde der Default-Export entfernt — jetzt ist der named export { thunk } erforderlich.

configureMockStore([thunk])middleware, die das Dispatchen von Funktionen (Thunk'u) statt einfacher Objekte. Ohne sie store.dispatch(login(...)) nicht funktioniert.

Abschnitt 2: Imports aus './authentication' (Zeilen 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';

Aus dem Import entfernt:

Zum Import hinzugefügt:

Anmerkung: Die alte Testdatei importierte 5 Symbole, die im Quellcode authentication.js nicht existieren (loadSessionIfAvailable, logout, USER_LOGOUT, userLogout, storeLoginSession). Der Test war 7 Jahre lang defekt — die Symbole wurden aus dem Quellcode entfernt (userLogout/logout entfernt Jul 2019, loadSessionIfAvailable umbenannt Mar 2019), aber der Test wurde nicht aktualisiert.

Abschnitt 3: Konfiguration (Zeilen 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 auf Modulebene verschoben — wurde 3× erstellt (in login, logout, check session), jetzt einmal am Anfang der Datei. [redux-mock-store]

Object.freeze()[MDN]. Der alte Test fror username und password ein (unnötig — Primitiven sind unveränderlich). In der neuen Datei wird Object.freeze nur auf State-Objekten im Reducer-Test verwendet, wo es tatsächlich wichtig ist (Schutz vor Mutation in einer Pure Function).

afterEach(() => fetchMock.restore())beforeEach(() => vi.restoreAllMocks()) — Ersetzung des fetch-mock-spezifischen Aufrufs durch den universellen Vitest-Aufruf vi.restoreAllMocks(). Reset von »after« auf »before« verschoben — stellt sicher, dass Mocks nicht zwischen Tests durchsickern.

Abschnitt 4: Action-Creator-Tests (Zeilen 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' },
+ });
+ });
+});

Drei Action-Creator-Tests, gruppiert in describe('action creators').

TestAltNeuUnterschied
userLoginBegin 11 Zeilen, Variable expectedAction 3 Zeilen, inline Nur Stil. Logik unverändert.
userLoginSuccess Aufruf ohne Argumente: userLoginSuccess(). Erwartung: { type: USER_LOGIN_SUCCESS } [Zeile 45] Aufruf mit 4 Argumenten. Erwartung: vollständige Payload mit user, version, buildTimestamp, alertMessage [neu] Korrektur: userLoginSuccess nimmt 4 erforderliche Parameter entgegen (authentication.js:11). Der alte Test prüfte unvollständiges Verhalten.
userLoginFailure 10 Zeilen, Beschreibung mit Tippfehler »mark the login as success« [Zeile 49] 5 Zeilen, korrekte Beschreibung Tippfehler korrigiert. Logik unverändert.

🔍 Tippfehler im Original: zwei Tests mit identischem Namen

Zeile 40 — SUCCESS ✓ Zeile 49 — FAILURE, Name sagt »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);
});

Hing 7 Jahre so. In der aktualisierten Version wurden die Namen korrigiert.

Abschnitt 5: Alter Login-Test ohne keepSignedIn (Zeilen 61–97)

Was ist keepSignedIn / stayLoggedIn: Die Checkbox »Angemeldet bleiben« auf dem Login-Formular. Wenn aktiviert, läuft folgende Kette ab:

  1. Frontend: login(username, password, keepSignedIn) (authentication.js:65) → POST-Body: { stayLoggedIn: keepSignedIn } (Zeile 77)
  2. Controller: LoginPageRest.kt empfängt PostData<LoginData> (Zeile 96), Feld stayLoggedIn in LoginData.kt:34
  3. Service: LoginService.authenticate() prüft if (loginData.stayLoggedIn == true) (LoginService.kt:175), generiert STAY_LOGGED_IN_KEY-Token (UserTokenType.kt:31) und ruft addStayLoggedInCookie() auf (Zeile 177-178)
  4. Cookie: CookieService setzt Cookie mit Name "stayLoggedIn" (CookieService.kt:201) und 30 Tagen Gültigkeit (Zeile 200)
  5. Wiederherstellung: beim nächsten Start, wenn JSESSIONID abgelaufen, liest LoginService.checkStayLoggedIn() (LoginService.kt:250) das Cookie via CookieService (CookieService.kt:65-75) — Server authentifiziert den Benutzer ohne Passwort

Der alte Test versuchte dies über cookies.loadAll() zu prüfen — aber das Cookie gelangt im Mock nicht in document.cookie (siehe Abschnitt 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- });

Probleme des alten Tests:

  1. fetchMock.mock(matcher, response) — sperrige API: die Matcher-Funktion parst selbst den Request-Body und prüft die URL. [Zeile 62-80] [fetch-mock source]
  2. headers: { 'Set-Cookie': '...' } — simuliert das Setzen eines Session-Cookies, aber fetch-mock kann Cookies nicht wirklich setzen [Zeile 77]. Warum: fetch-mock gibt ein Response-Objekt mit Headern zurück — schreibt aber nicht in document.cookie. react-cookies.loadAll() hingegen liest genau document.cookie = cookie.parse(document.cookie). Die Kette: fetch-mock → Set-Cookie im Header → document.cookie bleibt leer → loadAll() gibt {} zurück → Test besteht, prüft aber Leere statt echtes Verhalten.
  3. .catch({ throws: new Error('mock failed') }) — wenn kein Matcher zutraf — Fehler werfen. Ein Workaround, der echte Probleme verschleiert [Zeile 80].
  4. expectedActions = [BEGIN, SUCCESS] — FALSCH. login() dispatcht BEGIN, dann ruft loadUserStatus()(dispatch), die einen weiteren BEGIN und dann SUCCESS dispatcht. Insgesamt müssen es 3 Actions sein, nicht 2. [authentication.js:65-84]loginloadUserStatus()(dispatch) innerhalb von .then().
  5. cookies.loadAll()toEqual({}) — testet die Browser-Bibliothek react-cookies, nicht die Anwendungslogik [Zeile 94].

🏗 Architekturanalyse: warum der alte Test zum Scheitern verurteilt war

Falsche Abstraktionsebene. Der alte Test versuchte Cookie-Verhalten zu prüfen — aber Cookies werden vom Browser verwaltet, nicht von fetch():

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

react-cookies.loadAll() ──→  {}
expect(cookies.loadAll()).toEqual({})  ✓
Test bestand — prüfte Leere, nicht Verhalten
✓ Echter Browser
fetch() ──→  HTTP Response {
  Set-Cookie: JSESSIONID=ABC
}
      │
      │ Browser verarbeitet Set-Cookie
      ▼
document.cookie = "JSESSIONID=ABC"

react-cookies.loadAll() ──→  { JSESSIONID: "ABC" }
Cookie sichtbar — echtes Verhalten

Zwei inkompatible Mock-Welten. fetch-mock und react-cookies operieren auf grundverschiedenen Ebenen des Browser-Stacks ohne Verbindung in der Testumgebung. fetch-mock ersetzt die HTTP-Schicht (Netzwerk), react-cookies liest die DOM-Schicht (document.cookie). In einem echten Browser sitzt ein Cookie Jar dazwischen — eine Browser-Komponente, die Set-Cookie aus der HTTP-Antwort verarbeitet und in document.cookie schreibt. In jsdom mit gemocktem fetch fehlt diese Schicht — der Test prüfte eine Illusion.

Der Test bestand durch Zufall. Der Browser verarbeitete Set-Cookie nicht → document.cookie leer → cookies.loadAll() gab {} zurück. Aber der Test erwartete {}! Zwei Nullen trafen sich — Leere traf auf Leere — und erzeugten die Illusion eines funktionierenden Tests.

Verantwortungsgrenze. Ein Frontend-Unit-Test soll prüfen, welche Redux-Actions dispatcht werden bei verschiedenen Fetch-Antworten, nicht wie der Browser HTTP-Header verarbeitet. Korrekter Test: "fetch gab 200 zurück → [BEGIN, BEGIN, SUCCESS] dispatcht". Falsch: "fetch gab Set-Cookie zurück → prüfe ob Cookie in document.cookie gelandet ist" — das ist ein Browser-Test, nicht der Code der Anwendung.

Abschnitt 6: Alter Login-Test mit keepSignedIn und Bug /√/login (Zeilen 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- });

Kritischer Bug im Original:

if (url !== '/√/login' || options.method !== 'POST') — URL wurde als /√/login geschrieben (Quadratwurzelzeichen U+221A), obwohl die korrekte URL /rsPublic/login ist. [Zeile 103]

Dieser Test wurde nie ausgeführt — der Request POST /rsPublic/login matchte nicht mit /√/login, daher fiel er in .catch({ throws: new Error('mock failed') }).

Der Test konnte jedoch bestehen, wenn der Fehler in der .then().catch()-Kette unterdrückt wurde — catchError in authentication.js:84 dispatcht FAILURE, anstatt die Exception weiterzuwerfen.

Abschnitt 7: Alter Login-Fehlertest (Zeilen 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-});

Probleme:

  1. fetchMock.mock('/rsPublic/login', 401) — alte API, einfach URL und Status [Zeile 141]. Gab { status: 401 } ohne ok: false zurück, aber handleHTTPErrors prüft response.ok [rest.js:32].
  2. payload: { error: 'Unauthorized' } — im echten Code wirft handleHTTPErrors Error('Fetch failed: Error 401') [rest.js:34]. Der Test erwartet eine falsche Nachricht.

Abschnitt 8: Neues describe('login') — erfolgreicher Login (Zeilen 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 — Ersatz von .then() durch async/await. Kürzer, lesbarer, Fehler werden nicht verschluckt.

global.fetch = vi.fn().mockResolvedValueOnce() — zwei fetch-Aufrufe werden nacheinander gemockt:

  1. Erster: Antwort auf POST /rsPublic/login — leeres {}
  2. Zweiter: Antwort auf GET /rs/userStatus (aus loadUserStatus() innerhalb von login()) — mit userData, systemData, alertMessage

[BEGIN, BEGIN, SUCCESS] — KORRIGIERT. Drei Actions:

Alter Test erwartete 2 ([BEGIN, SUCCESS]) — der zweite BEGIN fehlte.

Abschnitt 9: Neues describe('login') — falsches Passwort und Netzwerk (Zeilen 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' } },
+ ]);
+ });

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

Netzwerkfehler — NEUER TEST (gab es in der alten Datei nicht):

mockRejectedValue(new Error('Network error'))[vitest docs]. Simuliert einen Netzwerk-/DNS-Fehler. fetch gibt keine Antwort zurück, sondern wirft eine Exception. login()fetch(...) → Netzwerk ausgefallen → .catch(catchError(dispatch))dispatch(USER_LOGIN_FAILURE('Network error')). [authentication.js:84]

Abschnitt 10: describe('logout') gelöscht (Zeilen 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-});

Komplett gelöscht (2 Tests, ~30 Zeilen).

Gründe:

  1. userLogout() gibt { type: USER_LOGOUT } zurück — trivial, gleiche Ebene wie Action-Creator. USER_LOGOUT wird nicht exportiert aus authentication.js [nur 3 Typen].
  2. logout() dispatcht eine Action — kein asynchroner Code.
  3. Beide hängen von cookies.loadAll() (react-cookies) ab, das wir entfernt haben.
  4. Die Logout-Funktionalität wird von USER_LOGIN_BEGIN abgedeckt (Zustands-Reset).

Abschnitt 11: Alt → Neu loadUserStatus (Zeilen 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-});

Kritischer TODO: 7 Jahre.

// TODO: ADD AUTHENTICATION TEST ENDPOINT [Zeile 211]hinzugefügt 2019-03-17, hing 7 Jahre bis zum PR 7e78f3741. Der alte Test loadSessionIfAvailable gab nur Status 200 zurück, ohne JSON-Body. response.json() schlug fehl. Der Test wurde nie ausgeführt.

»creates no action at all« gelöscht — prüfte, dass loadSessionIfAvailable() gibt zurück null ohne Mock. Im aktuellen Code loadUserStatus() führt immer fetch aus.

Neues describe('loadUserStatus') — gültige Session (Zeilen 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,
+ },
+ },
+ ]);
+ });

Korrekte JSON-Antwort: json: () => Promise.resolve({ userData, systemData, alertMessage }) — Struktur entspricht dem, was loadUserStatus() in authentication.js:42.

alertMessage: 'Some alert' — prüfen die Weitergabe der Systemmeldung (im Login-Test war es undefined — verschiedene Codepfade).

Neues describe('loadUserStatus') — Session abgelaufen (Zeilen 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 } });
+ });
+});

Neuer Test — gab es in der alten Datei nicht.

payload: { error: undefined } — Besonderheit von loadUserStatus(): in Catch-Handler wird catchError(dispatch)({ message: undefined }) aufgerufen. catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message))error.message ist undefined, weil ein Objekt { message: undefined } übergeben wird.

Prüfung über actions[0] und actions[1] (nicht toEqual des gesamten Arrays) — weil loadUserStatus() bei Fehler window.location.href = ausführt (Redirect auf /login), was in jsdom möglicherweise nicht funktioniert.

Begriffe

Redux Thunk
Middleware, die das Dispatchen von Funktionen (statt nur Objekten) ermöglicht. Die Funktion erhält (dispatch, getState) — kann fetch ausführen und mehrere Actions dispatchen. login() und loadUserStatus() sind Thunks: sie geben function(dispatch) { ... } zurück. Ohne redux-thunk würde Redux sie mit einem Fehler ablehnen. [Dokumentation]
Action Creator → Thunk
Ein Action Creator (userLoginBegin()) gibt ein Objekt { type, payload } zurück — wird synchron dispatched. Ein Thunk (login(username, password)) gibt eine Funktion zurück — die Middleware führt sie asynchron aus, sie macht Fetch und dispatched mehrere Actions. Zwei verschiedene Schichten: Creator = synchrone Objektfabrik, Thunk = asynchroner Orchestrator.
JSESSIONID
Standard-Java-Session-Cookie. Wird nicht vom ProjectForge-Code erstellt, sondern vom Servlet-Container (Tomcat/Jetty) beim ersten request.getSession(true). ProjectForge verwaltet nur seinen Lebenszyklus:
  • Nach Login — Invalidierung und Neuerstellung (Session-Fixation-Schutz): 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)  // ← neue JSESSIONID
    session.setAttribute(SESSION_KEY_USER, userContext)
  • Zurücksenden — jeder fetch() sendet das Cookie via credentials: 'include' in authentication.js:37:
    fetch(getServiceURL('userStatus'), {
        method: 'GET',
        credentials: 'include',  // ← sendet JSESSIONID an Server
    })
Wenn es abläuft — Session tot, GET /rs/userStatus gibt 401. Cookie ist HTTP-only: JavaScript kann es nicht über document.cookie lesen. [Wikipedia: Session ID]
KEEP_SIGNED_IN / stayLoggedIn
Checkbox »Angemeldet bleiben« im Login-Formular. Frontend: Parameter keepSignedIn in authentication.js:65 → gesendet als { stayLoggedIn: true } im POST-Body (Zeile 77). Server generiert Token STAY_LOGGED_IN_KEY und setzt Cookie mit Namen "stayLoggedIn" (CookieService.kt:201). KEEP_SIGNED_INalter Name der Client-seitigen Konstante, entfernt in März 2019. Der alte Test verwendete genau diesen (cookies.save('KEEP_SIGNED_IN', ...)) — falscher Name + fetch-mock setzt keine Cookies = Test prüfte Leere.
HTTP-only Cookie
Cookie mit dem HttpOnly-Flag. Der Browser sendet es an den Server, aber JavaScript hat keinen Zugriff — document.cookie enthält es nicht. JSESSIONID ist HTTP-only (Schutz vor XSS-Angriffen). Deshalb sieht cookies.loadAll() (das document.cookie liest) das Session-Cookie nie. [MDN]
vi.fn()
Vitests Mock-Funktions-Konstruktor. Äquivalent zu jest.fn(). mockResolvedValueOnce(x)nächster Aufruf gibt Promise.resolve(x) zurück (Kaskade). mockResolvedValue(x)alle Aufrufe geben Promise.resolve(x) zurück. mockRejectedValue(e) — Aufruf wirft Exception (Netzwerkausfall). Der Unterschied Once / ohne Once ist kritisch: im Login-Test werden zwei Fetch-Aufrufe nacheinander gemockt. [vitest]
import.meta.env.DEV / MODE
Vite-Äquivalent zu process.env.NODE_ENV. import.meta.env.DEVtrue im Dev-Modus. import.meta.env.MODE — String: 'development', 'production', oder 'test'. Ersatz war nötig, weil CRA-Variablen process.env.NODE_ENV in Vite nicht existieren. In rest.js:10: DEV && MODE !== 'test' — Dev-Server, aber nicht Test-Runner. [vite docs]
connect() → useSelector/useDispatch
Zwei Arten, React-Komponenten mit Redux-Store zu verbinden. connect(mapStateToProps)(Component) — HOC-Pattern (vor 2019), umschließt die Komponente. useSelector(state => state.user) + useDispatch() — Hooks (React 16.8+), einfacher, TypeScript-kompatibel, tree-shakeable. loggedIn: boolean wurde zu user: object|null genau wegen der Umstellung auf Hooks: der Selektor state => state.authentication.user !== null liest ein konkretes Feld statt eines Flags. [react-redux]
loadUserStatus()
Thunk, der beim App-Start und nach dem Login aufgerufen wird. Führt GET /rs/userStatus mit Cookie aus (credentials: 'include'). Server prüft die Session: wenn gültig — gibt { userData, systemData, alertMessage } (200) zurück. Wenn abgelaufen — 401 → Redirect auf /react/public/login. Das ist das Erste, was React beim Laden tut (siehe ProjectForge.jsx): »Wer bin ich?« [authentication.js:30]

Zusammenfassung der Änderungen

AspektAlte Datei (237 Zeilen)Neue Datei (157 Zeilen)
Fetch-Mock fetch-mock [Zeile 2] vi.fn() [vitest]
Cookies react-cookies [Zeile 3], StayLoggedIn-Tests Entfernt — Browser wird nicht getestet
Async-Stil Promise .then() [MDN] async/await [MDN]
Describe-Struktur 3 Blöcke: login, logout, check session 3 Blöcke: action creators, login, loadUserStatus
Login gültige Actions 2: [BEGIN, SUCCESS]falsch [Zeile 82] 3: [BEGIN, BEGIN, SUCCESS]korrekt [auth.js:62-84]
Login falsche Anmeldedaten 'Unauthorized' — URL /√/login Bug [Zeile 103] 'Fetch failed: Error 401' — korrekte URL [rest.js:32-38]
Netzwerkfehler Kein Test mockRejectedValue [Zeile 96]
loadUserStatus gültige Session TODO: 7 Jahre, funktionierte nie [Zeile 211] Vollständiger Test [Zeilen 108-140]
loadUserStatus abgelaufene Session Kein Test 401 → FAILURE [Zeilen 142-157]
logout 2 Tests, Symbol existiert nicht im Quellcode [entfernt Jul 2019] Gelöscht — triviale Action, abgedeckt durch USER_LOGIN_BEGIN

— Link auf Code im Branch fix/vite-eslint-upgrade, nicht in develop gemergt. Code möglicherweise bis zum PR-Merge nicht auf GitHub verfügbar.