import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; // Namespace-aware next-intl mock — same convention as // admin/groups/groups-page.test.tsx and admin/users/user-access-modal.test.tsx. // {param} placeholders are substituted plainly. const messages: Record> = { 'admin.users': { title: 'Benutzerverwaltung', create: 'Benutzer erstellen', edit: 'Benutzer bearbeiten', delete: 'Benutzer löschen', deleteConfirm: 'Möchten Sie diesen Benutzer wirklich löschen?', name: 'Name', username: 'Benutzername', email: 'E-Mail', displayName: 'Anzeigename', role: 'Rolle', status: 'Status', lastLogin: 'Letzte Anmeldung', neverLoggedIn: 'Noch nie', actions: 'Aktionen', password: 'Passwort', noUsers: 'Keine Benutzer gefunden', grants: { detailsButton: 'Details', }, errors: { serverRejected: 'Der Server hat die Aktion abgelehnt: {detail}', generic: 'Die Aktion konnte nicht durchgeführt werden. Bitte erneut versuchen.', network: 'Der Server ist nicht erreichbar. Bitte erneut versuchen.', loadFailed: 'Die Benutzerliste konnte nicht geladen werden. Bitte laden Sie die Seite neu.', }, }, 'admin.users.welcomeMail': { button: 'Willkommensmail senden', resend: 'Erneut senden', sentAt: 'Willkommensmail gesendet am {date}', noEmail: 'Keine E-Mail-Adresse hinterlegt – bitte zuerst eine Adresse eintragen.', noSmtp: 'Es ist noch kein SMTP-Server eingerichtet (Administrator → SMTP).', inactive: 'Das Konto ist deaktiviert.', dialogTitle: 'Willkommensmail senden', dialogText: 'Tessera schickt eine Willkommensmail an:', dialogResend: 'Eine Willkommensmail wurde bereits am {date} gesendet. Sie wird erneut verschickt.', send: 'Senden', sending: 'Wird gesendet …', success: 'Die Willkommensmail an {email} wurde gesendet.', }, header: { role: { SUPER_ADMIN: 'Super-Admin', ADMIN: 'Admin', USER: 'Benutzer', }, }, common: { loading: 'Laden...', cancel: 'Abbrechen', save: 'Speichern', delete: 'Löschen', edit: 'Bearbeiten', accessDenied: 'Zugriff verweigert', active: 'Aktiv', inactive: 'Inaktiv', close: 'Schließen', }, }; function resolve(ns: string, key: string, params?: Record): string { const parts = key.split('.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any let val: any = messages[ns] ?? {}; for (const part of parts) { val = val?.[part]; } if (typeof val !== 'string') return key; if (params) { for (const [k, v] of Object.entries(params)) { val = val.replace(`{${k}}`, String(v)); } } return val; } vi.mock('next-intl', () => ({ useTranslations: (ns: string) => (key: string, params?: Record) => resolve(ns, key, params), useLocale: () => 'de', })); const mockAuthStore = vi.fn(); vi.mock('@/lib/stores/auth-store', () => ({ useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector), })); // UserAccessModal is not exercised by these tests — stub it out so a click // on "Details" doesn't need its own fetch fixtures. vi.mock('./components/UserAccessModal', () => ({ UserAccessModal: () => null, })); import AdminUsersPage from './page'; interface User { id: string; username: string; email: string | null; displayName: string | null; role: 'SUPER_ADMIN' | 'ADMIN' | 'USER'; isActive: boolean; tenantId: string; createdAt: string; lastLoginAt?: string | null; welcomeMailSentAt?: string | null; } const mockUsers: User[] = [ { id: 'u1', username: 'admin.eins', email: 'admin.eins@ctl.de', displayName: 'Admin Eins', role: 'ADMIN', isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', }, { id: 'u2', username: 'user.zwei', email: 'user.zwei@ctl.de', displayName: 'User Zwei', role: 'USER', isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', }, ]; function stubAdmin(id = 'u1', role = 'ADMIN') { mockAuthStore.mockImplementation( (selector: (state: { user: { id: string; role: string; tenantId: string } }) => unknown) => selector({ user: { id, role, tenantId: 't1' } }), ); } const mockUsersWithSuperAdmin: User[] = [ { id: 'u0', username: 'super.null', email: 'super.null@ctl.de', displayName: 'Super Null', role: 'SUPER_ADMIN', isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', }, { id: 'u1', username: 'admin.eins', email: 'admin.eins@ctl.de', displayName: 'Admin Eins', role: 'ADMIN', isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', }, { id: 'u2', username: 'user.zwei', email: 'user.zwei@ctl.de', displayName: 'User Zwei', role: 'USER', isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', }, ]; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe('AdminUsersPage — Loeschweg (WINDOWS #36, Aufgabe 1)', () => { it('zeigt den Servertext im offenen Loeschdialog bei einer 403-Ablehnung', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'DELETE') { return Promise.resolve({ ok: false, status: 403, json: () => Promise.resolve({ statusCode: 403, message: 'Cannot delete a SUPER_ADMIN user' }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const deleteButtons = screen.getAllByText('Löschen'); await userEvent.click(deleteButtons[deleteButtons.length - 1]); await waitFor(() => { expect(screen.getByText('Möchten Sie diesen Benutzer wirklich löschen?')).toBeInTheDocument(); }); const confirmButtons = screen.getAllByText('Löschen'); await userEvent.click(confirmButtons[confirmButtons.length - 1]); await waitFor(() => { expect( screen.getByText('Der Server hat die Aktion abgelehnt: Cannot delete a SUPER_ADMIN user'), ).toBeInTheDocument(); }); // Dialog bleibt offen. expect(screen.getByText('Möchten Sie diesen Benutzer wirklich löschen?')).toBeInTheDocument(); }); it('zeigt die Ersatzmeldung, wenn die Antwort keinen verwertbaren Rumpf traegt', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'DELETE') { return Promise.resolve({ ok: false, status: 500, json: () => Promise.reject(new Error('not json')), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const deleteButtons = screen.getAllByText('Löschen'); await userEvent.click(deleteButtons[deleteButtons.length - 1]); const confirmButtons = screen.getAllByText('Löschen'); await userEvent.click(confirmButtons[confirmButtons.length - 1]); await waitFor(() => { expect( screen.getByText('Die Aktion konnte nicht durchgeführt werden. Bitte erneut versuchen.'), ).toBeInTheDocument(); }); expect(screen.queryByText(/Der Server hat die Aktion abgelehnt/)).not.toBeInTheDocument(); }); it('zeigt die Netzmeldung, wenn die Verbindung beim Loeschen scheitert', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'DELETE') { return Promise.reject(new Error('network down')); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const deleteButtons = screen.getAllByText('Löschen'); await userEvent.click(deleteButtons[deleteButtons.length - 1]); const confirmButtons = screen.getAllByText('Löschen'); await userEvent.click(confirmButtons[confirmButtons.length - 1]); await waitFor(() => { expect( screen.getByText('Der Server ist nicht erreichbar. Bitte erneut versuchen.'), ).toBeInTheDocument(); }); }); it('schliesst den Dialog und laedt die Liste neu, wenn das Loeschen gelingt (Erfolgsfall unveraendert)', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'DELETE') { return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const deleteButtons = screen.getAllByText('Löschen'); await userEvent.click(deleteButtons[deleteButtons.length - 1]); await waitFor(() => { expect(screen.getByText('Möchten Sie diesen Benutzer wirklich löschen?')).toBeInTheDocument(); }); const confirmButtons = screen.getAllByText('Löschen'); await userEvent.click(confirmButtons[confirmButtons.length - 1]); await waitFor(() => { expect( screen.queryByText('Möchten Sie diesen Benutzer wirklich löschen?'), ).not.toBeInTheDocument(); }); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); const usersCalls = fetchMock.mock.calls.filter( (call) => typeof call[0] === 'string' && call[0].endsWith('/users') && (!call[1] || (call[1] as RequestInit).method === undefined), ); // Erst-Laden + Neu-Laden nach erfolgreichem Loeschen. expect(usersCalls.length).toBeGreaterThanOrEqual(2); }); }); describe('AdminUsersPage — Formularweg und Listenladen (WINDOWS #36, Aufgabe 2)', () => { it('zeigt den Servertext im offenen Formular, wenn das Speichern abgewiesen wird', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'PATCH') { return Promise.resolve({ ok: false, status: 403, json: () => Promise.resolve({ message: 'Cannot modify a SUPER_ADMIN user' }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const editButtons = screen.getAllByText('Bearbeiten'); await userEvent.click(editButtons[0]); const usernameInput = await screen.findByDisplayValue('admin.eins'); const saveButton = screen.getByText('Speichern'); await userEvent.click(saveButton); await waitFor(() => { expect( screen.getByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'), ).toBeInTheDocument(); }); // Formular bleibt offen. expect(usernameInput).toBeInTheDocument(); }); it('verbindet Pruefmeldungen aus einem Feld mit Komma im Rahmensatz', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'PATCH') { return Promise.resolve({ ok: false, status: 400, json: () => Promise.resolve({ message: ['username must be longer', 'email must be an email'] }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const editButtons = screen.getAllByText('Bearbeiten'); await userEvent.click(editButtons[0]); await screen.findByDisplayValue('admin.eins'); await userEvent.click(screen.getByText('Speichern')); await waitFor(() => { expect( screen.getByText( 'Der Server hat die Aktion abgelehnt: username must be longer, email must be an email', ), ).toBeInTheDocument(); }); }); it('zeigt die Netzmeldung im offenen Formular, wenn die Verbindung beim Speichern scheitert', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'PATCH') { return Promise.reject(new Error('network down')); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const editButtons = screen.getAllByText('Bearbeiten'); await userEvent.click(editButtons[0]); const usernameInput = await screen.findByDisplayValue('admin.eins'); await userEvent.click(screen.getByText('Speichern')); await waitFor(() => { expect( screen.getByText('Der Server ist nicht erreichbar. Bitte erneut versuchen.'), ).toBeInTheDocument(); }); expect(usernameInput).toBeInTheDocument(); }); it('meldet ein gescheitertes Laden im Listenkopf statt faelschlich "Keine Benutzer gefunden" zu zeigen', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: false, status: 500, json: () => Promise.reject(new Error('not json')), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect( screen.getByText('Die Benutzerliste konnte nicht geladen werden. Bitte laden Sie die Seite neu.'), ).toBeInTheDocument(); }); expect(screen.queryByText('Keine Benutzer gefunden')).not.toBeInTheDocument(); }); it('laesst eine Meldung nicht das Schliessen und erneute Oeffnen des Formulars ueberdauern', async () => { stubAdmin(); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) }); } if (typeof url === 'string' && init?.method === 'PATCH') { return Promise.resolve({ ok: false, status: 403, json: () => Promise.resolve({ message: 'Cannot modify a SUPER_ADMIN user' }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); render(); await waitFor(() => { expect(screen.getByText('user.zwei')).toBeInTheDocument(); }); const editButtons = screen.getAllByText('Bearbeiten'); await userEvent.click(editButtons[0]); await screen.findByDisplayValue('admin.eins'); await userEvent.click(screen.getByText('Speichern')); await waitFor(() => { expect( screen.getByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'), ).toBeInTheDocument(); }); await userEvent.click(screen.getByText('Abbrechen')); await waitFor(() => { expect(screen.queryByDisplayValue('admin.eins')).not.toBeInTheDocument(); }); const editButtonsAgain = screen.getAllByText('Bearbeiten'); await userEvent.click(editButtonsAgain[0]); await screen.findByDisplayValue('admin.eins'); expect( screen.queryByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'), ).not.toBeInTheDocument(); }); }); describe('AdminUsersPage — Aktionsknoepfe der SUPER_ADMIN-Zeile (WINDOWS #36, Aufgabe 3)', () => { function stubList() { vi.stubGlobal( 'fetch', vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsersWithSuperAdmin) })), ); } it('bietet einem ADMIN in der SUPER_ADMIN-Zeile weder Bearbeiten noch Loeschen an, in einer normalen Zeile beide', async () => { stubAdmin('u1', 'ADMIN'); stubList(); render(); await waitFor(() => { expect(screen.getByText('super.null')).toBeInTheDocument(); }); const superAdminRow = screen.getByText('super.null').closest('tr') as HTMLElement; expect(within(superAdminRow).getByRole('button', { name: 'Details' })).toBeInTheDocument(); expect(within(superAdminRow).queryByText('Bearbeiten')).not.toBeInTheDocument(); expect(within(superAdminRow).queryByText('Löschen')).not.toBeInTheDocument(); const normalRow = screen.getByText('user.zwei').closest('tr') as HTMLElement; expect(within(normalRow).getByRole('button', { name: 'Bearbeiten' })).toBeInTheDocument(); expect(within(normalRow).getByRole('button', { name: 'Löschen' })).toBeInTheDocument(); }); it('bietet einem SUPER_ADMIN in der SUPER_ADMIN-Zeile beide Aktionsknoepfe an', async () => { stubAdmin('u1', 'SUPER_ADMIN'); stubList(); render(); await waitFor(() => { expect(screen.getByText('super.null')).toBeInTheDocument(); }); const superAdminRow = screen.getByText('super.null').closest('tr') as HTMLElement; expect(within(superAdminRow).getByRole('button', { name: 'Bearbeiten' })).toBeInTheDocument(); expect(within(superAdminRow).getByRole('button', { name: 'Löschen' })).toBeInTheDocument(); }); it('haelt die bestehende Sperre gegen Selbstloeschung unveraendert (ADMIN u1)', async () => { stubAdmin('u1', 'ADMIN'); stubList(); render(); await waitFor(() => { expect(screen.getByText('admin.eins')).toBeInTheDocument(); }); const ownRow = screen.getByText('admin.eins').closest('tr') as HTMLElement; const ownDeleteButton = within(ownRow).getByRole('button', { name: 'Löschen' }); expect(ownDeleteButton).toBeInTheDocument(); expect(ownDeleteButton).toBeDisabled(); }); }); describe('AdminUsersPage — Willkommensmail', () => { const base = { role: 'USER' as const, isActive: true, tenantId: 't1', createdAt: '2026-01-01T00:00:00.000Z', welcomeMailSentAt: null, }; const welcomeUsers: User[] = [ { ...base, id: 'u1', username: 'admin.eins', email: 'admin.eins@ctl.de', displayName: 'Admin Eins', role: 'ADMIN', lastLoginAt: '2026-09-01T08:00:00.000Z' }, { ...base, id: 'u2', username: 'neu.zwei', email: 'neu.zwei@ctl.de', displayName: 'Neu Zwei', lastLoginAt: null }, { ...base, id: 'u3', username: 'ohne.adresse', email: null, displayName: null, lastLoginAt: null }, { ...base, id: 'u4', username: 'schon.da', email: 'schon.da@ctl.de', displayName: null, lastLoginAt: '2026-09-02T08:00:00.000Z' }, { ...base, id: 'u5', username: 'super.fuenf', email: 'super@ctl.de', displayName: null, role: 'SUPER_ADMIN', lastLoginAt: '2026-09-02T08:00:00.000Z' }, ]; function stubFetch(opts: { available?: boolean; post?: () => Promise } = {}) { const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (url.endsWith('/users/welcome-mail/status')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ available: opts.available ?? true }) }); } if (url.endsWith('/users') && (!init || init.method === undefined)) { return Promise.resolve({ ok: true, json: () => Promise.resolve(welcomeUsers) }); } if (url.endsWith('/welcome-mail') && init?.method === 'POST') { return (opts.post ?? (() => Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) })))(); } return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }); vi.stubGlobal('fetch', fetchMock); return fetchMock; } it('Knopf bei jedem verwaltbaren Benutzer, auch nach einer Anmeldung; SUPER_ADMIN-Konto fuer einen ADMIN nie; ohne Adresse deaktiviert', async () => { stubAdmin(); stubFetch(); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); const rowAdmin = screen.getByText('admin.eins').closest('tr') as HTMLElement; expect(within(rowAdmin).getByRole('button', { name: 'Willkommensmail senden' })).toBeEnabled(); const rowDa = screen.getByText('schon.da').closest('tr') as HTMLElement; expect(within(rowDa).getByRole('button', { name: 'Willkommensmail senden' })).toBeEnabled(); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; expect(within(rowNeu).getByRole('button', { name: 'Willkommensmail senden' })).toBeEnabled(); // SUPER_ADMIN-Konto: fuer einen ADMIN gesperrt wie beim Bearbeiten const rowSuper = screen.getByText('super.fuenf').closest('tr') as HTMLElement; expect(within(rowSuper).queryByText('Willkommensmail senden')).not.toBeInTheDocument(); const rowOhne = screen.getByText('ohne.adresse').closest('tr') as HTMLElement; const btn = within(rowOhne).getByRole('button', { name: 'Willkommensmail senden' }); expect(btn).toBeDisabled(); expect(btn).toHaveAttribute('title', 'Keine E-Mail-Adresse hinterlegt – bitte zuerst eine Adresse eintragen.'); }); it('Spalte "Letzte Anmeldung": Datum/Uhrzeit lokal formatiert, sonst "Noch nie"', async () => { stubAdmin(); stubFetch(); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); expect(screen.getByText('Letzte Anmeldung')).toBeInTheDocument(); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; expect(within(rowNeu).getByText('Noch nie')).toBeInTheDocument(); const expected = new Intl.DateTimeFormat('de', { dateStyle: 'medium', timeStyle: 'short' }).format( new Date('2026-09-01T08:00:00.000Z'), ); const rowAdmin = screen.getByText('admin.eins').closest('tr') as HTMLElement; expect(within(rowAdmin).getByText(expected)).toBeInTheDocument(); }); it('ohne eingerichtetes SMTP ist der Knopf deaktiviert und nennt den Grund', async () => { stubAdmin(); stubFetch({ available: false }); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; await waitFor(() => expect(within(rowNeu).getByRole('button', { name: 'Willkommensmail senden' })).toBeDisabled()); expect(within(rowNeu).getByRole('button', { name: 'Willkommensmail senden' })).toHaveAttribute( 'title', 'Es ist noch kein SMTP-Server eingerichtet (Administrator → SMTP).', ); }); it('Dialog nennt den Empfaenger; Senden ruft POST /users/:id/welcome-mail und zeigt die Erfolgsmeldung', async () => { stubAdmin(); const fetchMock = stubFetch(); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; await userEvent.click(within(rowNeu).getByRole('button', { name: 'Willkommensmail senden' })); const dialog = await screen.findByRole('dialog'); expect(within(dialog).getByText(/neu\.zwei@ctl\.de/)).toBeInTheDocument(); await userEvent.click(within(dialog).getByText('Senden')); await waitFor(() => { expect(screen.getByText('Die Willkommensmail an neu.zwei@ctl.de wurde gesendet.')).toBeInTheDocument(); }); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); const post = fetchMock.mock.calls.find((c) => (c[1] as RequestInit | undefined)?.method === 'POST'); expect(post?.[0]).toMatch(/\/users\/u2\/welcome-mail$/); }); it('bei einer Ablehnung (409) bleibt der Dialog offen und zeigt den Servertext', async () => { stubAdmin(); stubFetch({ post: () => Promise.resolve({ ok: false, status: 409, json: () => Promise.resolve({ message: 'Dieser Benutzer hat sich bereits angemeldet.' }), }), }); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; await userEvent.click(within(rowNeu).getByRole('button', { name: 'Willkommensmail senden' })); const dialog = await screen.findByRole('dialog'); await userEvent.click(within(dialog).getByText('Senden')); await waitFor(() => { expect( screen.getByText('Der Server hat die Aktion abgelehnt: Dieser Benutzer hat sich bereits angemeldet.'), ).toBeInTheDocument(); }); expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('nach einem Versand heisst der Knopf "Erneut senden" und die Zeile nennt das Datum', async () => { stubAdmin(); welcomeUsers[1] = { ...welcomeUsers[1], welcomeMailSentAt: '2026-09-29T10:15:00.000Z' }; stubFetch(); render(); await waitFor(() => expect(screen.getByText('neu.zwei')).toBeInTheDocument()); const rowNeu = screen.getByText('neu.zwei').closest('tr') as HTMLElement; expect(within(rowNeu).getByRole('button', { name: 'Erneut senden' })).toBeInTheDocument(); expect(within(rowNeu).getByText(/Willkommensmail gesendet am/)).toBeInTheDocument(); welcomeUsers[1] = { ...welcomeUsers[1], welcomeMailSentAt: null }; }); it('SUPER_ADMIN sieht den Knopf auch bei einem angemeldeten SUPER_ADMIN-Konto', async () => { stubAdmin('u9', 'SUPER_ADMIN'); stubFetch(); render(); await waitFor(() => expect(screen.getByText('super.fuenf')).toBeInTheDocument()); const rowSuper = screen.getByText('super.fuenf').closest('tr') as HTMLElement; expect(within(rowSuper).getByRole('button', { name: 'Willkommensmail senden' })).toBeInTheDocument(); }); });