/** * Tests: API-Client (public/api.js) * Fokus: CSRF-Token-Handling, auth:expired-Dispatch-Verhalten */ import { test } from 'node:test'; import assert from 'node:assert/strict'; // Browser-Globals für Node-Kontext simulieren global.CustomEvent = class CustomEvent { constructor(type, init) { this.type = type; this.detail = init?.detail; } }; let dispatchedEvents = []; global.window = { dispatchEvent(e) { dispatchedEvents.push(e); }, addEventListener() {}, }; global.document = { cookie: '' }; // fetch-Mock: wird pro Test überschrieben let _mockFetch = null; global.fetch = (...args) => _mockFetch(...args); function mockResponse(status, body = {}, headers = {}) { return Promise.resolve({ status, ok: status >= 200 && status < 300, headers: { get(name) { return headers[name] ?? null; }, }, json: () => Promise.resolve(body), }); } const { api, auth, ApiError } = await import('./public/api.js'); function setup() { dispatchedEvents = []; document.cookie = ''; } // ─── 401 auf Login-Endpunkt ────────────────────────────────────────────────── test('auth.login: 401 feuert kein auth:expired', async () => { setup(); _mockFetch = () => mockResponse(401, { error: 'Ungültige Anmeldedaten.', code: 401 }); await assert.rejects( () => auth.login('user', 'wrong'), (err) => { assert.equal(err.constructor.name, 'ApiError'); assert.equal(err.status, 401); return true; }, ); const expired = dispatchedEvents.filter((e) => e.type === 'auth:expired'); assert.equal(expired.length, 0, 'auth:expired darf bei Login-401 nicht gefeuert werden'); }); test('auth.login: 401 wirft ApiError mit status 401', async () => { setup(); _mockFetch = () => mockResponse(401, { error: 'Ungültige Anmeldedaten.', code: 401 }); let thrownErr; try { await auth.login('user', 'wrong'); } catch (e) { thrownErr = e; } assert.ok(thrownErr instanceof ApiError, 'Muss ApiError sein'); assert.equal(thrownErr.status, 401); }); // ─── 401 auf anderen Endpunkten ───────────────────────────────────────────── test('api.get: 401 auf geschütztem Endpunkt feuert auth:expired', async () => { setup(); _mockFetch = () => mockResponse(401, { error: 'Nicht authentifiziert.', code: 401 }); await assert.rejects(() => api.get('/tasks')); const expired = dispatchedEvents.filter((e) => e.type === 'auth:expired'); assert.equal(expired.length, 1, 'auth:expired muss bei 401 auf geschütztem Endpunkt gefeuert werden'); }); test('api.post: 401 auf Logout-Endpunkt feuert auth:expired', async () => { setup(); _mockFetch = () => mockResponse(401, { error: 'Nicht authentifiziert.', code: 401 }); await assert.rejects(() => api.post('/auth/logout', {})); const expired = dispatchedEvents.filter((e) => e.type === 'auth:expired'); assert.equal(expired.length, 1, 'auth:expired muss bei 401 auf /auth/logout gefeuert werden'); }); // ─── Erfolgreicher Login ───────────────────────────────────────────────────── test('auth.login: Erfolg speichert csrfToken aus Body', async () => { setup(); const token = 'abc123def456'; _mockFetch = () => mockResponse(200, { user: { id: 1, username: 'admin' }, csrfToken: token, }); const result = await auth.login('admin', 'password'); assert.equal(result.user.username, 'admin'); assert.equal(result.csrfToken, token); assert.equal(dispatchedEvents.length, 0, 'Kein Event bei erfolgreichem Login'); });