refactor(admin): move SMTP settings from user settings to admin area
SMTP configuration is an admin concern, not a per-user setting. Removed it from the settings sidebar and relocated to /admin/smtp with a link in the admin sidebar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-6
@@ -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 (
|
||||
@@ -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<string, Record<string, string>> = {
|
||||
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(<SmtpSettingsForm />);
|
||||
|
||||
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(<SmtpSettingsForm />);
|
||||
|
||||
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(<SmtpSettingsForm />);
|
||||
|
||||
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(<SmtpSettingsForm />);
|
||||
|
||||
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(<SmtpSettingsForm />);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -60,6 +60,17 @@ export function AdminSidebar() {
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t('admin.smtp'),
|
||||
href: '/admin/smtp',
|
||||
show: true,
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -58,19 +58,6 @@ export function SettingsSidebar() {
|
||||
>
|
||||
{t('categoryAccount')}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings/general/smtp"
|
||||
className={`flex items-center rounded-md px-2 py-1.5 text-sm transition-colors ${
|
||||
isActive('/settings/general/smtp')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
||||
: 'text-sidebar-foreground hover:bg-muted'
|
||||
}`}
|
||||
aria-current={
|
||||
isActive('/settings/general/smtp') ? 'page' : undefined
|
||||
}
|
||||
>
|
||||
{t('categorySmtp')}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Dashboard category — existing items unchanged */}
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
"users": "Benutzer",
|
||||
"tenants": "Mandanten",
|
||||
"modules": "Module",
|
||||
"ldap": "LDAP"
|
||||
"ldap": "LDAP",
|
||||
"smtp": "SMTP"
|
||||
},
|
||||
"role": {
|
||||
"SUPER_ADMIN": "Super-Admin",
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
"users": "Users",
|
||||
"tenants": "Tenants",
|
||||
"modules": "Modules",
|
||||
"ldap": "LDAP"
|
||||
"ldap": "LDAP",
|
||||
"smtp": "SMTP"
|
||||
},
|
||||
"role": {
|
||||
"SUPER_ADMIN": "Super Admin",
|
||||
|
||||
Reference in New Issue
Block a user