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 { useTranslations } from 'next-intl';
|
||||||
import { SmtpSettingsForm } from '@/components/settings/smtp-settings-form';
|
import { SmtpSettingsForm } from '@/components/settings/smtp-settings-form';
|
||||||
|
|
||||||
/**
|
export default function AdminSmtpPage() {
|
||||||
* 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() {
|
|
||||||
const t = useTranslations('settings');
|
const t = useTranslations('settings');
|
||||||
|
|
||||||
return (
|
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>
|
</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 (
|
return (
|
||||||
|
|||||||
@@ -58,19 +58,6 @@ export function SettingsSidebar() {
|
|||||||
>
|
>
|
||||||
{t('categoryAccount')}
|
{t('categoryAccount')}
|
||||||
</Link>
|
</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>
|
</nav>
|
||||||
|
|
||||||
{/* Dashboard category — existing items unchanged */}
|
{/* Dashboard category — existing items unchanged */}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@
|
|||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"tenants": "Mandanten",
|
"tenants": "Mandanten",
|
||||||
"modules": "Module",
|
"modules": "Module",
|
||||||
"ldap": "LDAP"
|
"ldap": "LDAP",
|
||||||
|
"smtp": "SMTP"
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"SUPER_ADMIN": "Super-Admin",
|
"SUPER_ADMIN": "Super-Admin",
|
||||||
|
|||||||
@@ -74,7 +74,8 @@
|
|||||||
"users": "Users",
|
"users": "Users",
|
||||||
"tenants": "Tenants",
|
"tenants": "Tenants",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"ldap": "LDAP"
|
"ldap": "LDAP",
|
||||||
|
"smtp": "SMTP"
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"SUPER_ADMIN": "Super Admin",
|
"SUPER_ADMIN": "Super Admin",
|
||||||
|
|||||||
Reference in New Issue
Block a user