diff --git a/apps/web/src/app/(portal)/settings/general/smtp/page.tsx b/apps/web/src/app/(portal)/admin/smtp/page.tsx similarity index 57% rename from apps/web/src/app/(portal)/settings/general/smtp/page.tsx rename to apps/web/src/app/(portal)/admin/smtp/page.tsx index 67d8eb4..5bc30c5 100644 --- a/apps/web/src/app/(portal)/settings/general/smtp/page.tsx +++ b/apps/web/src/app/(portal)/admin/smtp/page.tsx @@ -3,12 +3,7 @@ import { useTranslations } from 'next-intl'; import { SmtpSettingsForm } from '@/components/settings/smtp-settings-form'; -/** - * SMTP settings page — /settings/general/smtp (Surface C, DKV-05). - * Sits inside the existing settings layout (SettingsSidebar wraps it). - * Heading follows the settings/dashboard/page.tsx pattern exactly. - */ -export default function SmtpSettingsPage() { +export default function AdminSmtpPage() { const t = useTranslations('settings'); return ( diff --git a/apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx b/apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx deleted file mode 100644 index 7309f82..0000000 --- a/apps/web/src/app/(portal)/settings/general/smtp/smtp-settings.test.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -// Mock next-intl — returns a short readable string for each key -vi.mock('next-intl', () => ({ - useTranslations: (ns: string) => (key: string) => { - const map: Record> = { - settings: { - 'smtp.title': 'SMTP-Konfiguration', - 'smtp.host': 'Host', - 'smtp.port': 'Port', - 'smtp.encryption': 'Verschluesselung', - 'smtp.username': 'Benutzername', - 'smtp.password': 'Passwort', - 'smtp.fromAddress': 'Absenderadresse (Von)', - 'smtp.fromAddressHelp': 'Die E-Mail-Adresse, die als Absender angezeigt wird.', - 'smtp.save': 'Einstellungen speichern', - 'smtp.test': 'Verbindung testen', - 'smtp.showPassword': 'Passwort anzeigen', - 'smtp.hidePassword': 'Passwort verbergen', - 'smtp.testTesting': 'Verbindung wird getestet...', - 'smtp.testSuccess': 'Verbindung erfolgreich', - 'smtp.testFailed': 'Verbindung fehlgeschlagen', - }, - }; - return map[ns]?.[key] ?? key; - }, -})); - -// Mock @/lib/settings-api -const mockFetchSmtp = vi.fn(); -const mockSaveSmtp = vi.fn(); -const mockTestSmtp = vi.fn(); - -vi.mock('@/lib/settings-api', () => ({ - fetchSmtp: (...args: unknown[]) => mockFetchSmtp(...args), - saveSmtp: (...args: unknown[]) => mockSaveSmtp(...args), - testSmtp: (...args: unknown[]) => mockTestSmtp(...args), -})); - -afterEach(() => { - cleanup(); - vi.restoreAllMocks(); -}); - -describe('SmtpSettingsForm', () => { - it('loads existing config and populates fields (password stays blank)', async () => { - mockFetchSmtp.mockResolvedValue({ - host: 'smtp.example.com', - port: 587, - encryption: 'starttls', - username: 'user@example.com', - fromAddress: 'tessera@example.com', - hasPassword: true, - }); - - const { SmtpSettingsForm } = await import( - '@/components/settings/smtp-settings-form' - ); - render(); - - await waitFor(() => { - expect( - (screen.getByLabelText('Host') as HTMLInputElement).value, - ).toBe('smtp.example.com'); - }); - - expect((screen.getByLabelText('Port') as HTMLInputElement).value).toBe( - '587', - ); - expect( - (screen.getByLabelText('Benutzername') as HTMLInputElement).value, - ).toBe('user@example.com'); - expect( - (screen.getByLabelText('Absenderadresse (Von)') as HTMLInputElement) - .value, - ).toBe('tessera@example.com'); - // T-07-17: password must never be pre-filled from server data - expect( - (screen.getByLabelText('Passwort') as HTMLInputElement).value, - ).toBe(''); - }); - - it('calls saveSmtp with form values on submit', async () => { - mockFetchSmtp.mockResolvedValue({ - host: 'smtp.example.com', - port: 587, - encryption: 'starttls', - username: 'user@example.com', - fromAddress: 'tessera@example.com', - hasPassword: false, - }); - mockSaveSmtp.mockResolvedValue({ - host: 'smtp.example.com', - port: 587, - encryption: 'starttls', - username: 'user@example.com', - fromAddress: 'tessera@example.com', - hasPassword: false, - }); - - const { SmtpSettingsForm } = await import( - '@/components/settings/smtp-settings-form' - ); - render(); - - await waitFor(() => { - expect( - (screen.getByLabelText('Host') as HTMLInputElement).value, - ).toBe('smtp.example.com'); - }); - - fireEvent.click(screen.getByText('Einstellungen speichern')); - - await waitFor(() => { - expect(mockSaveSmtp).toHaveBeenCalledWith( - expect.objectContaining({ - host: 'smtp.example.com', - port: 587, - encryption: 'starttls', - }), - ); - }); - }); - - it('shows success message when testSmtp resolves { success: true }', async () => { - mockFetchSmtp.mockResolvedValue(null); - mockTestSmtp.mockResolvedValue({ success: true }); - - const { SmtpSettingsForm } = await import( - '@/components/settings/smtp-settings-form' - ); - render(); - - await waitFor(() => - expect(screen.getByText('Verbindung testen')).toBeInTheDocument(), - ); - - fireEvent.click(screen.getByText('Verbindung testen')); - - await waitFor(() => { - expect( - screen.getByText('Verbindung erfolgreich'), - ).toBeInTheDocument(); - }); - }); - - it('shows error message when testSmtp resolves { success: false }', async () => { - mockFetchSmtp.mockResolvedValue(null); - mockTestSmtp.mockResolvedValue({ success: false }); - - const { SmtpSettingsForm } = await import( - '@/components/settings/smtp-settings-form' - ); - render(); - - await waitFor(() => - expect(screen.getByText('Verbindung testen')).toBeInTheDocument(), - ); - - fireEvent.click(screen.getByText('Verbindung testen')); - - await waitFor(() => { - expect( - screen.getByText('Verbindung fehlgeschlagen'), - ).toBeInTheDocument(); - }); - }); - - it('toggles password input type between password and text', async () => { - mockFetchSmtp.mockResolvedValue(null); - - const { SmtpSettingsForm } = await import( - '@/components/settings/smtp-settings-form' - ); - render(); - - await waitFor(() => - expect(screen.getByLabelText('Passwort')).toBeInTheDocument(), - ); - - const passwordInput = screen.getByLabelText('Passwort') as HTMLInputElement; - expect(passwordInput.type).toBe('password'); - - const toggleButton = screen.getByRole('button', { - name: 'Passwort anzeigen', - }); - fireEvent.click(toggleButton); - - expect(passwordInput.type).toBe('text'); - }); -}); diff --git a/apps/web/src/components/admin/admin-sidebar.tsx b/apps/web/src/components/admin/admin-sidebar.tsx index 0f4cbf5..e471b71 100644 --- a/apps/web/src/components/admin/admin-sidebar.tsx +++ b/apps/web/src/components/admin/admin-sidebar.tsx @@ -60,6 +60,17 @@ export function AdminSidebar() { ), }, + { + label: t('admin.smtp'), + href: '/admin/smtp', + show: true, + icon: ( + + + + + ), + }, ]; return ( diff --git a/apps/web/src/components/settings/settings-sidebar.tsx b/apps/web/src/components/settings/settings-sidebar.tsx index 5ebb07e..4dd7223 100644 --- a/apps/web/src/components/settings/settings-sidebar.tsx +++ b/apps/web/src/components/settings/settings-sidebar.tsx @@ -58,19 +58,6 @@ export function SettingsSidebar() { > {t('categoryAccount')} - - {t('categorySmtp')} - {/* Dashboard category — existing items unchanged */} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index efa4897..034cefe 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -74,7 +74,8 @@ "users": "Benutzer", "tenants": "Mandanten", "modules": "Module", - "ldap": "LDAP" + "ldap": "LDAP", + "smtp": "SMTP" }, "role": { "SUPER_ADMIN": "Super-Admin", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 3d63e72..f653fa6 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -74,7 +74,8 @@ "users": "Users", "tenants": "Tenants", "modules": "Modules", - "ldap": "LDAP" + "ldap": "LDAP", + "smtp": "SMTP" }, "role": { "SUPER_ADMIN": "Super Admin",