git diff origin/develop -- actions/authentication.test.jse67067aa7) zeigen auf den Fork MaurerAnton/projectforge (Branch draft43npm). Links auf alten Code (SHA 9ed5fbe0f) — auf das Haupt-Repository micromata/projectforge (develop).
-import fetchMock from 'fetch-mock/es5/client'-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.
+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:
vi.fn() — erstellt eine Mock-Funktion (ersetzt global.fetch) — L49, L78, L95, L118, L143fn.mockResolvedValueOnce(value) — nächster Aufruf gibt Promise.resolve(value) zurück (Kaskade aus zwei) — L50, L53fn.mockResolvedValue(value) — alle Aufrufe geben Promise.resolve(value) zurück — L79, L119, L144fn.mockRejectedValue(error) — Aufruf wirft Exception (Netzwerkfehler) — L96vi.restoreAllMocks() — Reset aller Mocks zwischen Tests — L42, L110-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.
Aus dem Import entfernt:
authentication.js diese Funktion existiert nicht — sie wurde umbenannt in loadUserStatus (Zeile 30, umbenannt 2019-03-18).describe('logout') gelöscht. Die Funktion logout existiert nicht im aktuellen authentication.js [Quellcode] (gelöscht 2019-07-13).authentication.js [nur 3 Typen] (gelöscht 2019-07-13).Zum Import hinzugefügt:
loadSessionIfAvailable. Die tatsächliche Funktion in authentication.js:30.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.
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.
Drei Action-Creator-Tests, gruppiert in describe('action creators').
| Test | Alt | Neu | Unterschied |
|---|---|---|---|
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. |
| 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.
Was ist keepSignedIn / stayLoggedIn: Die Checkbox »Angemeldet bleiben« auf dem Login-Formular. Wenn aktiviert, läuft folgende Kette ab:
login(username, password, keepSignedIn) (authentication.js:65) → POST-Body: { stayLoggedIn: keepSignedIn } (Zeile 77)LoginPageRest.kt empfängt PostData<LoginData> (Zeile 96), Feld stayLoggedIn in LoginData.kt:34LoginService.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)CookieService setzt Cookie mit Name "stayLoggedIn" (CookieService.kt:201) und 30 Tagen Gültigkeit (Zeile 200)JSESSIONID abgelaufen, liest LoginService.checkStayLoggedIn() (LoginService.kt:250) das Cookie via CookieService (CookieService.kt:65-75) — Server authentifiziert den Benutzer ohne PasswortDer alte Test versuchte dies über cookies.loadAll() zu prüfen — aber das Cookie gelangt im Mock nicht in document.cookie (siehe Abschnitt 2).
Probleme des alten Tests:
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]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..catch({ throws: new Error('mock failed') }) — wenn kein Matcher zutraf — Fehler werfen. Ein Workaround, der echte Probleme verschleiert [Zeile 80].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] — login → loadUserStatus()(dispatch) innerhalb von .then().cookies.loadAll() → toEqual({}) — testet die Browser-Bibliothek react-cookies, nicht die Anwendungslogik [Zeile 94].Falsche Abstraktionsebene. Der alte Test versuchte Cookie-Verhalten zu prüfen — aber Cookies werden vom Browser verwaltet, nicht von fetch():
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.
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.
Probleme:
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].payload: { error: 'Unauthorized' } — im echten Code wirft handleHTTPErrors Error('Fetch failed: Error 401') [rest.js:34]. Der Test erwartet eine falsche Nachricht.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:
POST /rsPublic/login — leeres {}GET /rs/userStatus (aus loadUserStatus() innerhalb von login()) — mit userData, systemData, alertMessage[BEGIN, BEGIN, SUCCESS] — KORRIGIERT. Drei Actions:
BEGIN — von login() (authentication.js:65)BEGIN — von loadUserStatus() (authentication.js:30)SUCCESS — von loadUserStatus() (authentication.js:42)Alter Test erwartete 2 ([BEGIN, SUCCESS]) — der zweite BEGIN fehlte.
Falsches Passwort (401):
mockResolvedValue (ohne Once) — alle Aufrufe geben 401 zurück. loadUserStatus() wird nicht aufgerufen (Login fiel früher aus).error: 'Fetch failed: Error 401' — weil handleHTTPErrors wirft Error('Fetch failed: Error ${status}').'Unauthorized' — falsch.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]
Komplett gelöscht (2 Tests, ~30 Zeilen).
Gründe:
userLogout() gibt { type: USER_LOGOUT } zurück — trivial, gleiche Ebene wie Action-Creator. USER_LOGOUT wird nicht exportiert aus authentication.js [nur 3 Typen].logout() dispatcht eine Action — kein asynchroner Code.cookies.loadAll() (react-cookies) ab, das wir entfernt haben.USER_LOGIN_BEGIN abgedeckt (Zustands-Reset).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.
describe('loadUserStatus') — gültige Session (Zeilen 108–140)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).
describe('loadUserStatus') — Session abgelaufen (Zeilen 142–157)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.
(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]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.request.getSession(true). ProjectForge verwaltet nur seinen Lebenszyklus:
// 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)fetch() sendet das Cookie via credentials: 'include' in authentication.js:37:fetch(getServiceURL('userStatus'), {
method: 'GET',
credentials: 'include', // ← sendet JSESSIONID an Server
})GET /rs/userStatus gibt 401. Cookie ist HTTP-only: JavaScript kann es nicht über document.cookie lesen. [Wikipedia: Session ID]
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_IN — alter 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.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]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]process.env.NODE_ENV. import.meta.env.DEV — true 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(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]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]| Aspekt | Alte 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.
Vorher: Der alte Test verwendete
fetch-mock— eine externe Bibliothek zum Mocken vonfetch(hinzugefügt 2019-03-15). Die Versiones5/clientwurde für die Kompatibilität mit altem Jest/CRA benötigt. [Quellcode develop]Nachher: Gelöscht. Drei Gründe:
vi.fn()direkt mit — kein separates Paket nötig.fetch-mockVersion^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].fetch-mock-Abhängigkeit mehr — ein Paket weniger, weniger Schwachstellen.