import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { LoginPage } from './LoginPage'; import { clearTokens } from './authApi'; /** Baut eine Antwort, wie sie die API liefert. */ function buildResponse(responseBody: unknown, statusCode = 200): Response { return new Response(JSON.stringify(responseBody), { status: statusCode, headers: { 'Content-Type': 'application/json' }, }); } /** Ein vollstaendiger Benutzer, wie ihn die API zurueckgibt. */ const testUser = { id: '11111111-1111-4111-8111-111111111111', username: 'admin', status: 'active', mfa_enabled: false, roles: ['super_administrator'], permissions: ['users.read'], }; /** Ein Tokenpaar, wie es die API zurueckgibt. */ const testTokens = { access_token: 'zugriffstoken', // secretscan:erlaubt: erfundener Testwert refresh_token: 'erneuerungstoken', // secretscan:erlaubt: erfundener Testwert access_expires_at: '2026-08-10T16:00:00Z', refresh_expires_at: '2026-08-11T04:00:00Z', }; afterEach(() => { vi.unstubAllGlobals(); clearTokens(); }); describe('LoginPage', () => { it('meldet einen Benutzer ohne zweiten Faktor direkt an', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( buildResponse({ data: { mfa_required: false, tokens: testTokens, user: testUser }, meta: { request_id: 'req-1' }, }), ), ); const handleAuthenticated = vi.fn(); render(); await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(screen.getByLabelText('Passwort'), 'geheim'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); await waitFor(() => { expect(handleAuthenticated).toHaveBeenCalledWith(testUser); }); }); it('fordert bei aktivem zweiten Faktor den Code an', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( buildResponse({ data: { mfa_required: true, challenge_id: '22222222-2222-4222-8222-222222222222' }, meta: { request_id: 'req-2' }, }), ), ); const handleAuthenticated = vi.fn(); render(); await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(screen.getByLabelText('Passwort'), 'geheim'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); // Der zweite Schritt erscheint, und noch ist niemand angemeldet. await waitFor(() => { expect(screen.getByLabelText('Code')).toBeInTheDocument(); }); expect(handleAuthenticated).not.toHaveBeenCalled(); }); it('schliesst die Anmeldung nach gueltigem Code ab', async () => { const fetchMock = vi .fn() .mockResolvedValueOnce( buildResponse({ data: { mfa_required: true, challenge_id: '22222222-2222-4222-8222-222222222222' }, meta: { request_id: 'req-3' }, }), ) .mockResolvedValueOnce( buildResponse({ data: { mfa_required: false, tokens: testTokens, user: { ...testUser, mfa_enabled: true } }, meta: { request_id: 'req-4' }, }), ); vi.stubGlobal('fetch', fetchMock); const handleAuthenticated = vi.fn(); render(); await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(screen.getByLabelText('Passwort'), 'geheim'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); await waitFor(() => expect(screen.getByLabelText('Code')).toBeInTheDocument()); await userEvent.type(screen.getByLabelText('Code'), '123456'); await userEvent.click(screen.getByRole('button', { name: 'Bestaetigen' })); await waitFor(() => { expect(handleAuthenticated).toHaveBeenCalled(); }); }); it('zeigt die Fehlermeldung des Servers verstaendlich an', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( buildResponse( { error: { code: 'UNAUTHENTICATED', message: 'Benutzername oder Passwort ist falsch.', request_id: 'req-5', }, }, 401, ), ), ); render(); await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(screen.getByLabelText('Passwort'), 'falsch'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('Benutzername oder Passwort ist falsch.'); }); }); it('leert das Passwortfeld nach einem Fehlversuch', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( buildResponse( { error: { code: 'UNAUTHENTICATED', message: 'Falsch.', request_id: 'req-6' } }, 401, ), ), ); render(); const passwordField = screen.getByLabelText('Passwort') as HTMLInputElement; await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(passwordField, 'falsch'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); // Ein stehengebliebenes Passwort waere auf einem gemeinsam genutzten // Bildschirm sichtbar. await waitFor(() => { expect(passwordField.value).toBe(''); }); }); it('kehrt bei abgelaufener Herausforderung zur Anmeldung zurueck', async () => { const fetchMock = vi .fn() .mockResolvedValueOnce( buildResponse({ data: { mfa_required: true, challenge_id: '22222222-2222-4222-8222-222222222222' }, meta: { request_id: 'req-7' }, }), ) .mockResolvedValueOnce( buildResponse( { error: { code: 'MFA_CHALLENGE_EXPIRED', message: 'Die Anmeldung ist abgelaufen. Bitte erneut anmelden.', request_id: 'req-8', }, }, 401, ), ); vi.stubGlobal('fetch', fetchMock); render(); await userEvent.type(screen.getByLabelText('Benutzername'), 'admin'); await userEvent.type(screen.getByLabelText('Passwort'), 'geheim'); await userEvent.click(screen.getByRole('button', { name: 'Anmelden' })); await waitFor(() => expect(screen.getByLabelText('Code')).toBeInTheDocument()); await userEvent.type(screen.getByLabelText('Code'), '123456'); await userEvent.click(screen.getByRole('button', { name: 'Bestaetigen' })); // Ohne Rueckkehr sässe der Benutzer in einem Schritt fest, der nicht mehr gilt. await waitFor(() => { expect(screen.getByLabelText('Benutzername')).toBeInTheDocument(); }); }); });