Files
tessera-ctl/apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx
T
schalli 2176f8ecce refactor(domains): Einstellung Standard-Nameserver entfernt, Doku angepasst (h3t)
- Spalte DomainsConfig.defaultNameServers per Migration 20261008160000 entfernt
- Feld in Einstellungen/Status (API, DTO, Typen) und Karte im Web entfernt
- Anleitungen und CHANGELOG: Nameserver kommen aus AutoDNS

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 12:37:21 +02:00

551 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
{ui}
</NextIntlClientProvider>,
);
}
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<typeof import('@/lib/domains-api')>();
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> = {}): DomainsStatus {
return {
environment: 'DEMO',
configured: true,
demoConfigured: true,
liveConfigured: false,
...over,
};
}
function settings(over: Partial<DomainsSettings> = {}): 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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
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(<DomainsPage />);
fireEvent.click(await screen.findByRole('button', { name: 'Registrieren' }));
expect(await screen.findByRole('button', { name: 'Verfügbarkeit prüfen' })).toBeTruthy();
});
});