import { cleanup, fireEvent, render as rtlRender, screen, waitFor, within, } from '@testing-library/react'; import { NextIntlClientProvider } from 'next-intl'; import type { ReactElement } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { DomainsContactList, DomainsCustomer, DomainsDomainList, DomainsSettings, DomainsStatus, } from '@/lib/domains-api'; import de from '@/messages/de.json'; import DomainsPage from './page'; function render(ui: ReactElement) { return rtlRender( {ui} , ); } const mockGetStatus = vi.fn(); const mockGetSettings = vi.fn(); const mockSaveSettings = vi.fn(); const mockTestConnection = vi.fn(); const mockListCustomers = vi.fn(); const mockListContacts = vi.fn(); const mockListDomains = vi.fn(); const mockAssignContacts = vi.fn(); const mockCreateCustomer = vi.fn(); const mockDeleteCustomer = vi.fn(); const mockListOrders = vi.fn(); const mockRefreshOrders = vi.fn(); vi.mock('@/lib/domains-api', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, getDomainsStatus: (...args: unknown[]) => mockGetStatus(...args), getDomainsSettings: (...args: unknown[]) => mockGetSettings(...args), saveDomainsSettings: (...args: unknown[]) => mockSaveSettings(...args), testDomainsConnection: (...args: unknown[]) => mockTestConnection(...args), listCustomers: (...args: unknown[]) => mockListCustomers(...args), listContacts: (...args: unknown[]) => mockListContacts(...args), listDomains: (...args: unknown[]) => mockListDomains(...args), assignContacts: (...args: unknown[]) => mockAssignContacts(...args), createCustomer: (...args: unknown[]) => mockCreateCustomer(...args), deleteCustomer: (...args: unknown[]) => mockDeleteCustomer(...args), listOrders: (...args: unknown[]) => mockListOrders(...args), refreshOrders: (...args: unknown[]) => mockRefreshOrders(...args), }; }); let mockCanManage: boolean | null = true; vi.mock('@/lib/use-module-capability', () => ({ useCanManageModule: () => mockCanManage, })); function status(over: Partial = {}): DomainsStatus { return { environment: 'DEMO', configured: true, demoConfigured: true, liveConfigured: false, ...over, }; } function settings(over: Partial = {}): DomainsSettings { return { environment: 'DEMO', demo: { user: 'api-user', hasPassword: true, context: 1 }, live: { user: null, hasPassword: false, context: null }, configured: { demo: true, live: false }, ...over, }; } const CUSTOMERS: DomainsCustomer[] = [ { id: 'c-own', name: 'Unsere Firma', isOwnCompany: true, contactCount: 1 }, { id: 'c-a', name: 'Alpha GmbH', isOwnCompany: false, contactCount: 2 }, ]; function domainList(): DomainsDomainList { return { environment: 'DEMO', truncated: false, fetchedAt: '2026-10-08T10:00:00.000Z', domains: [ { name: 'frei.de', expire: '2027-03-01T12:00:00.000Z', status: 'ACTIVE', cancelationPending: false, ownerContactId: 5, ownerName: 'Niemand', customerId: null, customerName: null, }, { name: 'alpha.de', expire: '2027-07-15T12:00:00.000Z', status: 'LOCK', cancelationPending: true, ownerContactId: 6, ownerName: 'Alpha GmbH', customerId: 'c-a', customerName: 'Alpha GmbH', }, { name: 'eigen.de', expire: null, status: 'SONDERWERT', cancelationPending: false, ownerContactId: 7, ownerName: 'Unsere Firma', customerId: 'c-own', customerName: 'Unsere Firma', }, ], }; } function contactList(): DomainsContactList { return { environment: 'DEMO', truncated: false, fetchedAt: '2026-10-08T10:00:00.000Z', contacts: [ { id: 6, type: 'ORG', displayName: 'Alpha GmbH', fname: 'Anna', lname: 'Alpha', organization: 'Alpha GmbH', address: ['Weg 1'], pcode: '10115', city: 'Berlin', country: 'DE', email: 'anna@alpha.example', phone: '+49 30 1', alias: null, customerId: 'c-a', customerName: 'Alpha GmbH', }, { id: 5, type: 'PERSON', displayName: 'Bert Beta', fname: 'Bert', lname: 'Beta', organization: null, address: [], pcode: null, city: 'Hamburg', country: 'DE', email: 'bert@beta.example', phone: null, alias: null, customerId: null, customerName: null, }, ], }; } /** Oeffnet den Reiter Einstellungen (Standardreiter ist Domains). */ async function renderSettings() { render(); fireEvent.click(await screen.findByRole('button', { name: 'Einstellungen' })); } beforeEach(() => { mockCanManage = true; mockListCustomers.mockReset().mockResolvedValue(CUSTOMERS); mockListDomains.mockReset().mockResolvedValue(domainList()); mockListContacts.mockReset().mockResolvedValue(contactList()); mockAssignContacts.mockReset().mockResolvedValue({ assigned: 1, removed: 0 }); mockCreateCustomer.mockReset().mockResolvedValue(CUSTOMERS[1]); mockDeleteCustomer.mockReset().mockResolvedValue({ deleted: true }); mockListOrders.mockReset().mockResolvedValue([]); mockRefreshOrders.mockReset().mockResolvedValue([]); mockGetStatus.mockReset().mockResolvedValue(status()); mockGetSettings.mockReset().mockResolvedValue(settings()); mockSaveSettings.mockReset().mockImplementation(async () => settings()); mockTestConnection .mockReset() .mockResolvedValue({ ok: true, message: 'Verbindung erfolgreich.' }); }); afterEach(() => { cleanup(); }); describe('Domains-Seite — Kopf und Reiter', () => { it('Verwalter sieht den Reiter Einstellungen und die Demo-Kennzeichnung', async () => { render(); expect(await screen.findByRole('button', { name: 'Einstellungen' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Domains' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Kontakte' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Kunden' })).toBeTruthy(); expect((await screen.findByTestId('domains-environment-badge')).textContent).toBe( 'Demo-System (Testbetrieb)', ); }); it('Live-System zeigt die Kostenwarnung', async () => { mockGetStatus.mockResolvedValue(status({ environment: 'LIVE', liveConfigured: true })); render(); await waitFor(() => expect(screen.getByTestId('domains-environment-badge').textContent).toBe( 'Live-System – Registrierungen kosten Geld', ), ); }); it('nicht eingerichtet: Kennzeichnung und Hinweis', async () => { mockGetStatus.mockResolvedValue(status({ configured: false, demoConfigured: false })); render(); await waitFor(() => expect(screen.getByTestId('domains-environment-badge').textContent).toBe( 'AutoDNS nicht eingerichtet', ), ); expect(screen.getByText('AutoDNS ist noch nicht eingerichtet.')).toBeTruthy(); expect(screen.getByRole('button', { name: 'Zu den Einstellungen' })).toBeTruthy(); }); it('nicht eingerichtet, ohne Verwalten: Hinweis auf einen Administrator, kein Einstellungen-Reiter', async () => { mockCanManage = false; mockGetStatus.mockResolvedValue(status({ configured: false, demoConfigured: false })); render(); expect(await screen.findByText(/Bitte wenden Sie sich an einen Administrator/)).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Zu den Einstellungen' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Einstellungen' })).toBeNull(); }); it('Benutzer ohne Verwalten sieht den Reiter Einstellungen nicht', async () => { mockCanManage = false; render(); await screen.findByTestId('domains-environment-badge'); expect(screen.queryByRole('button', { name: 'Einstellungen' })).toBeNull(); expect(mockGetSettings).not.toHaveBeenCalled(); }); }); describe('Einstellungen', () => { it('Passwortfelder beginnen leer, mit Hinweis auf das gespeicherte Passwort', async () => { await renderSettings(); const demoPassword = (await screen.findByLabelText('Passwort', { selector: '#domains-demo-password', })) as HTMLInputElement; expect(demoPassword.value).toBe(''); expect(demoPassword.placeholder).toBe('Gespeichert – leer lassen, um es beizubehalten'); const livePassword = document.getElementById('domains-live-password') as HTMLInputElement; expect(livePassword.placeholder).toBe(''); }); it('der Live-Kontext ist mit 4 vorbelegt, der Demo-Kontext zeigt den gespeicherten Wert', async () => { await renderSettings(); await screen.findByLabelText('Passwort', { selector: '#domains-demo-password' }); expect((document.getElementById('domains-live-context') as HTMLInputElement).value).toBe('4'); expect((document.getElementById('domains-demo-context') as HTMLInputElement).value).toBe('1'); }); it('Wechsel auf Live verlangt Bestaetigung; erst die bestaetigte Speicherung sendet confirmLive', async () => { mockSaveSettings.mockResolvedValue(settings({ environment: 'LIVE' })); await renderSettings(); const liveRadio = await screen.findByRole('radio', { name: /Live-System \(kostenpflichtig\)/ }); fireEvent.click(liveRadio); const dialog = await screen.findByRole('alertdialog'); expect(within(dialog).getByText(/kosten Geld/)).toBeTruthy(); expect(mockSaveSettings).not.toHaveBeenCalled(); // Abbrechen sendet nichts und stellt die Auswahl zurueck. fireEvent.click(within(dialog).getByRole('button', { name: 'Abbrechen' })); expect(mockSaveSettings).not.toHaveBeenCalled(); expect(screen.queryByRole('alertdialog')).toBeNull(); expect( (screen.getByRole('radio', { name: /Demo-System \(Testbetrieb\)/ }) as HTMLInputElement) .checked, ).toBe(true); fireEvent.click(screen.getByRole('radio', { name: /Live-System \(kostenpflichtig\)/ })); fireEvent.click( within(await screen.findByRole('alertdialog')).getByRole('button', { name: 'Live-System verwenden', }), ); await waitFor(() => expect(mockSaveSettings).toHaveBeenCalledTimes(1)); expect(mockSaveSettings).toHaveBeenCalledWith({ environment: 'LIVE', confirmLive: true }); }); it('Speichern des Zugangs sendet nur die eigenen Felder; ohne Eingabe kein Passwort', async () => { await renderSettings(); const user = (await screen.findByLabelText('Benutzername', { selector: '#domains-demo-user', })) as HTMLInputElement; fireEvent.change(user, { target: { value: 'neuer-user' } }); const demoCard = document.getElementById('domains-demo') as HTMLElement; fireEvent.click(within(demoCard).getByRole('button', { name: 'Speichern' })); await waitFor(() => expect(mockSaveSettings).toHaveBeenCalledTimes(1)); expect(mockSaveSettings).toHaveBeenCalledWith({ demoUser: 'neuer-user', demoContext: 1 }); }); it('Verbindung testen: ein Aufruf je Klick, gesperrt waehrend des Laufs, zeigt das Ergebnis', async () => { let resolveTest: (v: { ok: boolean; message: string }) => void = () => undefined; mockTestConnection.mockImplementation( () => new Promise((resolve) => { resolveTest = resolve; }), ); await renderSettings(); await screen.findByLabelText('Passwort', { selector: '#domains-demo-password' }); const demoCard = document.getElementById('domains-demo') as HTMLElement; const button = within(demoCard).getByRole('button', { name: 'Verbindung testen' }); fireEvent.click(button); await waitFor(() => expect(mockTestConnection).toHaveBeenCalledTimes(1)); expect(mockTestConnection).toHaveBeenCalledWith('DEMO'); const running = within(demoCard).getByRole('button', { name: 'Verbindung wird getestet …' }); expect((running as HTMLButtonElement).disabled).toBe(true); fireEvent.click(running); expect(mockTestConnection).toHaveBeenCalledTimes(1); resolveTest({ ok: false, message: 'Anmeldung bei AutoDNS fehlgeschlagen.' }); expect(await within(demoCard).findByText('Anmeldung bei AutoDNS fehlgeschlagen.')).toBeTruthy(); }); it('der Verbindungstest ist bei ungespeicherten Aenderungen gesperrt', async () => { await renderSettings(); const password = (await screen.findByLabelText('Passwort', { selector: '#domains-demo-password', })) as HTMLInputElement; fireEvent.change(password, { target: { value: 'neu' } }); const demoCard = document.getElementById('domains-demo') as HTMLElement; expect( (within(demoCard).getByRole('button', { name: 'Verbindung testen' }) as HTMLButtonElement) .disabled, ).toBe(true); expect(within(demoCard).getByText('Bitte zuerst speichern')).toBeTruthy(); }); it('Einstellungen zeigen keine Karte "Standard-Nameserver" mehr (h3t)', async () => { await renderSettings(); await screen.findByRole('radio', { name: /Live-System \(kostenpflichtig\)/ }); expect(screen.queryByText('Standard-Nameserver')).toBeNull(); expect(document.getElementById('domains-nameservers')).toBeNull(); }); }); describe('Reiter Domains, Kontakte und Kunden', () => { it('Benutzen-Benutzer sieht Domains, Kontakte, Kunden, aber weder Einstellungen noch Schreibknoepfe', async () => { mockCanManage = false; render(); expect(await screen.findByRole('button', { name: 'Domains' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Kontakte' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Kunden' })).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Einstellungen' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: 'Kontakte' })); await screen.findByText('Bert Beta'); expect(screen.queryByRole('button', { name: 'Neuer Kontakt' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Zuordnen' })).toBeNull(); expect(screen.queryByRole('checkbox', { name: /auswählen/ })).toBeNull(); // Der Neuabruf bei AutoDNS ist Verwaltern vorbehalten (der Server ignoriert ihn sonst). expect(screen.queryByRole('button', { name: 'Aus AutoDNS neu einlesen' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: 'Kunden' })); await screen.findByText('Alpha GmbH'); expect(screen.queryByRole('button', { name: 'Kunde anlegen' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Bearbeiten' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Löschen' })).toBeNull(); }); it('Verwalter sieht die Schreibknoepfe', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'Kontakte' })); expect(await screen.findByRole('button', { name: 'Neuer Kontakt' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'Zuordnen' })).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Kunden' })); expect(await screen.findByRole('button', { name: 'Kunde anlegen' })).toBeTruthy(); expect(screen.getAllByRole('button', { name: 'Bearbeiten' })).toHaveLength(2); }); it('Domainliste: Gruppen je Kunde, "Nicht zugeordnet" zuletzt, Datum und Statusbeschriftung', async () => { render(); await screen.findByText('alpha.de'); const titles = Array.from(document.querySelectorAll('.cc-section-title')).map( (h) => h.textContent, ); expect(titles).toEqual(['Unsere Firma', 'Alpha GmbH', 'Nicht zugeordnet']); const row = screen.getByText('alpha.de').closest('tr') as HTMLElement; expect(within(row).getByText('15.07.2027')).toBeTruthy(); expect(within(row).getByText(/Gesperrt/)).toBeTruthy(); expect(within(row).getByText('Kündigung vorgemerkt')).toBeTruthy(); const frei = screen.getByText('frei.de').closest('tr') as HTMLElement; expect(within(frei).getByText('01.03.2027')).toBeTruthy(); expect(within(frei).getByText('Aktiv')).toBeTruthy(); // Unbekannter Status: Rohwert als Rueckfall, fehlendes Datum als Strich. const eigen = screen.getByText('eigen.de').closest('tr') as HTMLElement; expect(within(eigen).getByText('SONDERWERT')).toBeTruthy(); }); it('Kundenfilter blendet die anderen Gruppen aus', async () => { render(); await screen.findByText('alpha.de'); fireEvent.change(document.getElementById('domains-filter') as HTMLSelectElement, { target: { value: 'c-a' }, }); expect(screen.getByText('alpha.de')).toBeTruthy(); expect(screen.queryByText('frei.de')).toBeNull(); expect(screen.queryByText('eigen.de')).toBeNull(); fireEvent.change(document.getElementById('domains-filter') as HTMLSelectElement, { target: { value: 'unassigned' }, }); expect(screen.getByText('frei.de')).toBeTruthy(); expect(screen.queryByText('alpha.de')).toBeNull(); }); it('Textsuche und "Aus AutoDNS neu laden"', async () => { render(); await screen.findByText('alpha.de'); fireEvent.change(document.getElementById('domains-search') as HTMLInputElement, { target: { value: 'FREI' }, }); expect(screen.queryByText('alpha.de')).toBeNull(); expect(screen.getByText('frei.de')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Aus AutoDNS neu laden' })); await waitFor(() => expect(mockListDomains).toHaveBeenLastCalledWith({ refresh: true })); }); it('zeigt den Hinweis bei abgeschnittener Liste und die Fehlermeldung der API', async () => { mockListDomains.mockResolvedValueOnce({ ...domainList(), truncated: true }); render(); expect(await screen.findByText('Es werden die ersten 2000 Einträge angezeigt.')).toBeTruthy(); }); it('Kontakte zuordnen: ausgewaehlte Kontakte gehen an den gewaehlten Kunden', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'Kontakte' })); await screen.findByText('Bert Beta'); fireEvent.click(screen.getByRole('checkbox', { name: 'Kontakt Bert Beta auswählen' })); fireEvent.change(document.getElementById('contacts-assign-target') as HTMLSelectElement, { target: { value: 'c-a' }, }); fireEvent.click(screen.getByRole('button', { name: 'Zuordnen' })); await waitFor(() => expect(mockAssignContacts).toHaveBeenCalledTimes(1)); expect(mockAssignContacts).toHaveBeenCalledWith([5], 'c-a'); }); it('Zuordnung entfernen sendet null', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'Kontakte' })); await screen.findByText('Bert Beta'); fireEvent.click(screen.getByRole('checkbox', { name: 'Kontakt Alpha GmbH auswählen' })); fireEvent.change(document.getElementById('contacts-assign-target') as HTMLSelectElement, { target: { value: '__remove__' }, }); fireEvent.click(screen.getByRole('button', { name: 'Zuordnen' })); await waitFor(() => expect(mockAssignContacts).toHaveBeenCalledWith([6], null)); }); it('Kunden: Loeschen verlangt eine Bestaetigung und zeigt den 409-Text der API', async () => { const { DomainsRequestError } = await import('@/lib/domains-api'); mockDeleteCustomer.mockRejectedValueOnce( new DomainsRequestError(409, 'customerInUse', 'Diesem Kunden sind noch Kontakte zugeordnet.'), ); render(); fireEvent.click(await screen.findByRole('button', { name: 'Kunden' })); const buttons = await screen.findAllByRole('button', { name: 'Löschen' }); fireEvent.click(buttons[1]); const dialog = await screen.findByRole('alertdialog'); expect(mockDeleteCustomer).not.toHaveBeenCalled(); fireEvent.click(within(dialog).getByRole('button', { name: 'Endgültig löschen' })); await waitFor(() => expect(mockDeleteCustomer).toHaveBeenCalledWith('c-a')); expect(await screen.findByText('Diesem Kunden sind noch Kontakte zugeordnet.')).toBeTruthy(); }); it('Kunden: neuer Kunde mit Markierung eigene Firma', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'Kunden' })); fireEvent.change(await screen.findByLabelText('Name', { selector: '#customer-new-name' }), { target: { value: ' Neu AG ' }, }); fireEvent.click(screen.getByRole('checkbox', { name: 'Das ist unsere eigene Firma' })); fireEvent.click(screen.getByRole('button', { name: 'Kunde anlegen' })); await waitFor(() => expect(mockCreateCustomer).toHaveBeenCalledTimes(1)); expect(mockCreateCustomer).toHaveBeenCalledWith({ name: 'Neu AG', isOwnCompany: true }); }); it('nicht eingerichtet: keine Liste, nur der Hinweis', async () => { mockGetStatus.mockResolvedValue(status({ configured: false, demoConfigured: false })); render(); await screen.findByText('AutoDNS ist noch nicht eingerichtet.'); expect(mockListDomains).not.toHaveBeenCalled(); }); }); describe('Reiter Registrieren und Aufträge', () => { it('Verwalter sieht beide Reiter in der festen Reihenfolge', async () => { render(); await screen.findByRole('button', { name: 'Einstellungen' }); const wanted = ['Domains', 'Kontakte', 'Kunden', 'Registrieren', 'Aufträge', 'Einstellungen']; const names = screen .getAllByRole('button') .map((b) => b.textContent ?? '') .filter((x) => wanted.includes(x)); expect(names).toEqual(wanted); }); it('Benutzer ohne Verwalten sieht Aufträge, aber weder Registrieren noch Einstellungen', async () => { mockCanManage = false; render(); await screen.findByTestId('domains-environment-badge'); expect(screen.getByRole('button', { name: 'Aufträge' })).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Registrieren' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Einstellungen' })).toBeNull(); }); it('der Reiter Aufträge gleicht beim Öffnen mit AutoDNS ab', async () => { mockCanManage = false; render(); fireEvent.click(await screen.findByRole('button', { name: 'Aufträge' })); await waitFor(() => expect(mockRefreshOrders).toHaveBeenCalledTimes(1)); }); it('der Reiter Registrieren zeigt die Domain-Eingabe', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'Registrieren' })); expect(await screen.findByRole('button', { name: 'Verfügbarkeit prüfen' })).toBeTruthy(); }); });