feat(domains): Modul Domains mit AutoDNS-Zugang, Verbindungstest und Einstellungen
- Migration mit vier Tabellen (Einstellungen, Kunden, Kontaktzuordnung, Bestellungen), Zeilenschutz je Organisation - AutoDNS-Client mit festen Demo-/Live-Adressen, Takt-Begrenzer, 20 s Zeitlimit, ohne Wiederholung - Zugang je System verschluesselt gespeichert, Live-Wechsel nur mit Bestaetigung, Verbindungstest - Modulseite mit Systemkennzeichnung und Reiter Einstellungen (nur Verwalten) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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 { 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();
|
||||
|
||||
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),
|
||||
};
|
||||
});
|
||||
|
||||
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,
|
||||
defaultNameServers: [],
|
||||
...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 },
|
||||
defaultNameServers: [],
|
||||
configured: { demo: true, live: false },
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCanManage = true;
|
||||
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((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 () => {
|
||||
render(<DomainsPage />);
|
||||
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 () => {
|
||||
render(<DomainsPage />);
|
||||
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' }));
|
||||
render(<DomainsPage />);
|
||||
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 () => {
|
||||
render(<DomainsPage />);
|
||||
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;
|
||||
}),
|
||||
);
|
||||
render(<DomainsPage />);
|
||||
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 () => {
|
||||
render(<DomainsPage />);
|
||||
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('Nameserver: speichert die bereinigte Liste', async () => {
|
||||
render(<DomainsPage />);
|
||||
const first = (await screen.findByLabelText('Nameserver 1')) as HTMLInputElement;
|
||||
fireEvent.change(first, { target: { value: ' NS1.Example.com ' } });
|
||||
fireEvent.change(screen.getByLabelText('Nameserver 2'), {
|
||||
target: { value: 'ns2.example.com' },
|
||||
});
|
||||
const card = document.getElementById('domains-nameservers') as HTMLElement;
|
||||
fireEvent.click(within(card).getByRole('button', { name: 'Speichern' }));
|
||||
await waitFor(() => expect(mockSaveSettings).toHaveBeenCalledTimes(1));
|
||||
expect(mockSaveSettings).toHaveBeenCalledWith({
|
||||
defaultNameServers: ['ns1.example.com', 'ns2.example.com'],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user