mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
refactor(admin): share the notification settings saves and tests between desktop and phone
useAdminNotificationSettings now holds the SMTP save and test, the trip reminder switch and the admin webhook and ntfy save, test and token clear. The desktop tab drops its inline handlers and both views keep their markup.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { MASK, useAdminNotificationSettings } from './useAdminNotificationSettings';
|
||||
|
||||
// FE-HOOK-ADMNOTIF-001 to FE-HOOK-ADMNOTIF-013
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
updateAppSettings: vi.fn(),
|
||||
getAppConfig: vi.fn(),
|
||||
testSmtp: vi.fn(),
|
||||
testWebhook: vi.fn(),
|
||||
testNtfy: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../api/client', () => ({
|
||||
authApi: { updateAppSettings: api.updateAppSettings, getAppConfig: api.getAppConfig },
|
||||
notificationsApi: { testSmtp: api.testSmtp, testWebhook: api.testWebhook, testNtfy: api.testNtfy },
|
||||
}));
|
||||
|
||||
const t = (key: string) => key;
|
||||
|
||||
function mount(initial: Record<string, string>) {
|
||||
const toast = { success: vi.fn(), error: vi.fn() };
|
||||
const setTripRemindersEnabled = vi.fn();
|
||||
const hook = renderHook(() => {
|
||||
const [smtpValues, setSmtpValues] = useState(initial);
|
||||
return {
|
||||
smtpValues,
|
||||
settings: useAdminNotificationSettings({ smtpValues, setSmtpValues, setTripRemindersEnabled, toast }, t),
|
||||
};
|
||||
});
|
||||
return { ...hook, toast, setTripRemindersEnabled };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(api)) fn.mockReset();
|
||||
api.updateAppSettings.mockResolvedValue({});
|
||||
api.getAppConfig.mockResolvedValue({ trip_reminders_enabled: true });
|
||||
api.testSmtp.mockResolvedValue({ success: true });
|
||||
api.testWebhook.mockResolvedValue({ success: true });
|
||||
api.testNtfy.mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
describe('useAdminNotificationSettings', () => {
|
||||
it('FE-HOOK-ADMNOTIF-001: derives the reminder switch and whether SMTP is configured', () => {
|
||||
expect(mount({}).result.current.settings.tripRemindersActive).toBe(true);
|
||||
expect(mount({ notify_trip_reminder: 'false' }).result.current.settings.tripRemindersActive).toBe(false);
|
||||
expect(mount({ smtp_host: ' ' }).result.current.settings.smtpConfigured).toBe(false);
|
||||
expect(mount({ smtp_host: 'mail.example.com' }).result.current.settings.smtpConfigured).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-002: saving SMTP sends only the loaded credential keys and refreshes the reminders', async () => {
|
||||
const { result, toast, setTripRemindersEnabled } = mount({
|
||||
smtp_host: 'h',
|
||||
smtp_pass: MASK,
|
||||
admin_webhook_url: 'x',
|
||||
});
|
||||
await act(() => result.current.settings.saveSmtp());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ smtp_host: 'h', smtp_pass: MASK });
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.notifications.saved');
|
||||
await waitFor(() => expect(setTripRemindersEnabled).toHaveBeenCalledWith(true));
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-003: a failed SMTP save toasts the error; a config without the flag changes nothing', async () => {
|
||||
api.getAppConfig.mockResolvedValue({});
|
||||
const { result, toast, setTripRemindersEnabled } = mount({ smtp_host: 'h' });
|
||||
await act(() => result.current.settings.saveSmtp());
|
||||
await waitFor(() => expect(api.getAppConfig).toHaveBeenCalled());
|
||||
expect(setTripRemindersEnabled).not.toHaveBeenCalled();
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
await act(() => result.current.settings.saveSmtp());
|
||||
expect(toast.error).toHaveBeenCalledWith('common.error');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-004: the SMTP test saves first, even when that fails, then reports the result', async () => {
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
api.testSmtp.mockResolvedValue({ success: false, error: 'Auth failed' });
|
||||
const { result, toast } = mount({ smtp_host: 'h' });
|
||||
await act(() => result.current.settings.testSmtp());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ smtp_host: 'h' });
|
||||
expect(toast.error).toHaveBeenCalledWith('Auth failed');
|
||||
api.testSmtp.mockResolvedValue({ success: false });
|
||||
await act(() => result.current.settings.testSmtp());
|
||||
expect(toast.error).toHaveBeenLastCalledWith('admin.smtp.testFailed');
|
||||
api.testSmtp.mockResolvedValue({ success: true });
|
||||
await act(() => result.current.settings.testSmtp());
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.smtp.testSuccess');
|
||||
api.testSmtp.mockRejectedValue(new Error('x'));
|
||||
await act(() => result.current.settings.testSmtp());
|
||||
expect(toast.error).toHaveBeenLastCalledWith('admin.smtp.testFailed');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-005: toggling reminders off saves, toasts and refreshes the flag', async () => {
|
||||
const { result, toast, setTripRemindersEnabled } = mount({});
|
||||
await act(() => result.current.settings.toggleTripReminders());
|
||||
expect(result.current.smtpValues.notify_trip_reminder).toBe('false');
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ notify_trip_reminder: 'false' });
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.notifications.tripReminders.disabled');
|
||||
await waitFor(() => expect(setTripRemindersEnabled).toHaveBeenCalledWith(true));
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-006: a failed reminder toggle puts the old value back and toasts', async () => {
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
const { result, toast } = mount({ notify_trip_reminder: 'false' });
|
||||
await act(() => result.current.settings.toggleTripReminders());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ notify_trip_reminder: 'true' });
|
||||
expect(result.current.smtpValues.notify_trip_reminder).toBe('false');
|
||||
expect(toast.error).toHaveBeenCalledWith('common.error');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-007: saving the admin webhook sends the url or an empty string', async () => {
|
||||
const { result, toast } = mount({});
|
||||
await act(() => result.current.settings.saveAdminWebhook());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ admin_webhook_url: '' });
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.notifications.adminWebhookPanel.saved');
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
await act(() => result.current.settings.saveAdminWebhook());
|
||||
expect(toast.error).toHaveBeenCalledWith('common.error');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-008: testing a typed webhook saves it first and passes it on', async () => {
|
||||
const { result, toast } = mount({ admin_webhook_url: 'https://hook' });
|
||||
await act(() => result.current.settings.testAdminWebhook());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ admin_webhook_url: 'https://hook' });
|
||||
expect(api.testWebhook).toHaveBeenCalledWith('https://hook');
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.notifications.adminWebhookPanel.testSuccess');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-009: a masked webhook is tested as stored, without a save', async () => {
|
||||
api.testWebhook.mockResolvedValue({ success: false, error: '404' });
|
||||
const { result, toast } = mount({ admin_webhook_url: MASK });
|
||||
await act(() => result.current.settings.testAdminWebhook());
|
||||
expect(api.updateAppSettings).not.toHaveBeenCalled();
|
||||
expect(api.testWebhook).toHaveBeenCalledWith(undefined);
|
||||
expect(toast.error).toHaveBeenCalledWith('404');
|
||||
api.testWebhook.mockRejectedValue(new Error('x'));
|
||||
await act(() => result.current.settings.testAdminWebhook());
|
||||
expect(toast.error).toHaveBeenLastCalledWith('admin.notifications.adminWebhookPanel.testFailed');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-010: clearing the ntfy token saves an empty token and empties the field', async () => {
|
||||
const { result, toast } = mount({ admin_ntfy_token: MASK });
|
||||
await act(() => result.current.settings.clearNtfyToken());
|
||||
expect(api.updateAppSettings).toHaveBeenCalledWith({ admin_ntfy_token: '' });
|
||||
expect(result.current.smtpValues.admin_ntfy_token).toBe('');
|
||||
expect(toast.success).toHaveBeenCalledWith('admin.notifications.adminNtfyPanel.tokenCleared');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-011: a failed token clear keeps the mask and toasts', async () => {
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
const { result, toast } = mount({ admin_ntfy_token: MASK });
|
||||
await act(() => result.current.settings.clearNtfyToken());
|
||||
expect(result.current.smtpValues.admin_ntfy_token).toBe(MASK);
|
||||
expect(toast.error).toHaveBeenCalledWith('common.error');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-012: saving ntfy sends a typed token but never the mask', async () => {
|
||||
const typed = mount({ admin_ntfy_server: 'https://ntfy', admin_ntfy_topic: 'trek', admin_ntfy_token: 'tk' });
|
||||
await act(() => typed.result.current.settings.saveAdminNtfy());
|
||||
expect(api.updateAppSettings).toHaveBeenLastCalledWith({
|
||||
admin_ntfy_server: 'https://ntfy',
|
||||
admin_ntfy_topic: 'trek',
|
||||
admin_ntfy_token: 'tk',
|
||||
});
|
||||
expect(typed.toast.success).toHaveBeenCalledWith('admin.notifications.adminNtfyPanel.saved');
|
||||
const masked = mount({ admin_ntfy_token: MASK });
|
||||
await act(() => masked.result.current.settings.saveAdminNtfy());
|
||||
expect(api.updateAppSettings).toHaveBeenLastCalledWith({ admin_ntfy_server: '', admin_ntfy_topic: '' });
|
||||
api.updateAppSettings.mockRejectedValue(new Error('x'));
|
||||
await act(() => masked.result.current.settings.saveAdminNtfy());
|
||||
expect(masked.toast.error).toHaveBeenCalledWith('common.error');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ADMNOTIF-013: the ntfy test needs a topic and sends the token only when typed', async () => {
|
||||
const empty = mount({ admin_ntfy_topic: ' ' });
|
||||
await act(() => empty.result.current.settings.testAdminNtfy());
|
||||
expect(api.testNtfy).not.toHaveBeenCalled();
|
||||
const masked = mount({ admin_ntfy_topic: ' trek ', admin_ntfy_token: MASK });
|
||||
await act(() => masked.result.current.settings.testAdminNtfy());
|
||||
expect(api.testNtfy).toHaveBeenLastCalledWith({ topic: 'trek', server: null, token: null });
|
||||
expect(masked.toast.success).toHaveBeenCalledWith('admin.notifications.adminNtfyPanel.testSuccess');
|
||||
api.testNtfy.mockResolvedValue({ success: false });
|
||||
const typed = mount({ admin_ntfy_topic: 'trek', admin_ntfy_server: 'https://n', admin_ntfy_token: 'tk' });
|
||||
await act(() => typed.result.current.settings.testAdminNtfy());
|
||||
expect(api.testNtfy).toHaveBeenLastCalledWith({ topic: 'trek', server: 'https://n', token: 'tk' });
|
||||
expect(typed.toast.error).toHaveBeenCalledWith('admin.notifications.adminNtfyPanel.testFailed');
|
||||
api.testNtfy.mockRejectedValue(new Error('x'));
|
||||
await act(() => typed.result.current.settings.testAdminNtfy());
|
||||
expect(typed.toast.error).toHaveBeenLastCalledWith('admin.notifications.adminNtfyPanel.testFailed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user