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();
});
});
});