diff --git a/docs/issues/manual_test_notifications_single_source_of_truth.md b/docs/issues/manual_test_notifications_single_source_of_truth.md new file mode 100644 index 00000000..bf5cac1d --- /dev/null +++ b/docs/issues/manual_test_notifications_single_source_of_truth.md @@ -0,0 +1,56 @@ +--- +title: Manual Test Plan - Notifications Single Source of Truth +status: Open +priority: High +assignee: QA +labels: testing, frontend, i18n, accessibility +--- + +# Test Goal +Manually verify the notifications single-source-of-truth behavior for provider security events and confirm related UI, persistence, localization, and accessibility smoke behavior. + +# Scope +- Notifications settings page visibility changes +- Provider Add/Edit security event checkbox behavior +- CRUD persistence for security event selections +- Updated locale string rendering +- Provider form accessibility smoke checks + +# Preconditions +- Charon is running and reachable in a browser. +- Tester can access Settings and notification provider Add/Edit flows. +- At least one language switch option is available in the UI. + +## 1) Notifications settings page: no standalone security section +- [ ] Open Settings → Notifications. +- [ ] Confirm no standalone security section is shown on this page. +- [ ] Confirm page layout remains clean (no large empty gaps where removed content would be). +- [ ] Confirm no untranslated raw keys are visible. + +## 2) Provider Add/Edit security event checkboxes as single source of truth +- [ ] Open Add Provider form and locate security event checkboxes. +- [ ] Confirm security event options are configurable in provider form. +- [ ] Save a provider with a specific mixed selection (some checked, some unchecked). +- [ ] Re-open Edit for that provider and confirm checkbox states match saved values. +- [ ] Confirm checkbox labels are clear and not duplicated. + +## 3) CRUD flows preserve security event selections +- [ ] Create provider with custom security event selection and save. +- [ ] Refresh and verify saved selections persist. +- [ ] Edit provider, change only one security event selection, save, refresh, verify exact change persisted. +- [ ] Duplicate CRUD sanity: create second provider with different security event selections and confirm values stay isolated per provider. +- [ ] Delete one provider and confirm remaining provider keeps its own security event selections unchanged. + +## 4) i18n rendering for modified locale strings +- [ ] Switch to each supported language in turn. +- [ ] Open Notifications settings and provider Add/Edit screens. +- [ ] Confirm modified strings render as user-facing text (no raw translation keys). +- [ ] Confirm labels are not truncated or overlapping in provider forms. +- [ ] Confirm returning to default language restores expected text. + +## 5) Accessibility smoke checks for provider form interactions +- [ ] Navigate provider form controls using keyboard only (`Tab`, `Shift+Tab`, `Space`, `Enter`). +- [ ] Confirm each security event checkbox is reachable and operable by keyboard. +- [ ] Confirm visible focus indicator is present on each interactive control. +- [ ] Confirm checkbox label text clearly describes each option. +- [ ] Run a quick screen reader smoke pass and confirm checkbox name + checked state are announced. diff --git a/frontend/src/components/__tests__/SecurityNotificationSettingsModal.test.tsx b/frontend/src/components/__tests__/SecurityNotificationSettingsModal.test.tsx index 2e528a34..03cb26c9 100644 --- a/frontend/src/components/__tests__/SecurityNotificationSettingsModal.test.tsx +++ b/frontend/src/components/__tests__/SecurityNotificationSettingsModal.test.tsx @@ -22,8 +22,6 @@ vi.mock('../../api/notifications', async () => { getProviders: vi.fn(), getTemplates: vi.fn(), getExternalTemplates: vi.fn(), - getSecurityNotificationSettings: vi.fn(), - updateSecurityNotificationSettings: vi.fn(), }; }); @@ -31,16 +29,6 @@ vi.mock('../../utils/toast', () => ({ toast: { success: vi.fn(), error: vi.fn() }, })); -const mockSettings: notificationsApi.SecurityNotificationSettings = { - enabled: true, - min_log_level: 'warn', - security_waf_enabled: true, - security_acl_enabled: true, - security_rate_limit_enabled: false, - destination_ambiguous: false, - webhook_url: 'https://example.com/webhook', -}; - describe('Security Notification Settings on Notifications page', () => { let queryClient: ReturnType; @@ -50,8 +38,6 @@ describe('Security Notification Settings on Notifications page', () => { vi.mocked(notificationsApi.getProviders).mockResolvedValue([]); vi.mocked(notificationsApi.getTemplates).mockResolvedValue([]); vi.mocked(notificationsApi.getExternalTemplates).mockResolvedValue([]); - vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings); - vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(mockSettings); }); const renderPage = () => @@ -63,40 +49,10 @@ describe('Security Notification Settings on Notifications page', () => { ); - it('renders the security notifications section', async () => { + it('does not render a standalone security notifications section', async () => { renderPage(); - expect(await screen.findByTestId('security-notifications-section')).toBeInTheDocument(); - }); - - it('loads and displays existing compatibility security settings', async () => { - renderPage(); - - await waitFor(() => { - const enableSwitch = screen.getByTestId('security-notifications-enabled') as HTMLInputElement; - expect(enableSwitch.checked).toBe(true); - }); - - const webhookInput = screen.getByTestId('security-webhook-url') as HTMLInputElement; - expect(webhookInput.value).toBe('https://example.com/webhook'); - expect(screen.getByTestId('security-compatibility-banner')).toBeInTheDocument(); - }); - - it('shows compatibility controls as read-only', async () => { - vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue({ - ...mockSettings, - enabled: false, - }); - - renderPage(); - - await waitFor(() => { - const enableSwitch = screen.getByTestId('security-notifications-enabled') as HTMLInputElement; - expect(enableSwitch.checked).toBe(false); - }); - - expect((screen.getByTestId('security-min-log-level') as HTMLSelectElement).disabled).toBe(true); - expect((screen.getByTestId('security-webhook-url') as HTMLInputElement).disabled).toBe(true); - expect(screen.queryByTestId('security-notifications-save-btn')).toBeNull(); + await screen.findByTestId('add-provider-btn'); + expect(screen.queryByTestId('security-notifications-section')).toBeNull(); }); it('shows provider security event checkboxes in add-provider flow', async () => { @@ -116,9 +72,7 @@ describe('Security Notification Settings on Notifications page', () => { it('does not render a modal overlay for security settings', async () => { renderPage(); - await waitFor(() => { - expect(screen.getByTestId('security-notifications-section')).toBeInTheDocument(); - }); + await screen.findByTestId('add-provider-btn'); // Security settings are inline on the page, not inside a modal overlay expect(document.querySelector('.fixed.inset-0')).toBeNull(); diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json index 5b109502..0564d4b2 100644 --- a/frontend/src/locales/de/translation.json +++ b/frontend/src/locales/de/translation.json @@ -502,15 +502,6 @@ "testFailed": "Test konnte nicht gesendet werden", "deleteConfirm": "Sind Sie sicher?", "noProviders": "Keine Benachrichtigungsanbieter konfiguriert.", - "securityNotifications": "Security Event Notifications", - "securityNotificationsDescription": "Compatibility view for legacy security notification settings.", - "securityCompatibilityDeprecated": "This compatibility endpoint is deprecated. Configure security events per provider in Notification Events.", - "securityCompatibilityNotifyOnly": "Notify-only behavior is preserved: only enabled providers subscribed to these events receive notifications.", - "enableAlerts": "Enable Security Alerts", - "alertsDescription": "Receive notifications when security events occur.", - "minLogLevel": "Minimum Log Level", - "minLogLevelHelp": "Only events at this level or higher will trigger notifications.", - "notifyOn": "Notify On:", "securityEventSubscriptions": "Security Event Subscriptions", "securityEventSubscriptionsHelp": "Select security events for each provider in this form.", "wafBlocks": "WAF Blocks", @@ -519,10 +510,6 @@ "aclDenialsHelp": "When an IP is denied by Access Control Lists.", "rateLimitHits": "Rate Limit Hits", "rateLimitHitsHelp": "When a client exceeds rate limiting thresholds.", - "compatibilityAggregateHelp": "Read-only aggregate across enabled providers.", - "compatibilityDestination": "Compatibility Destination", - "compatibilityDestinationHelp": "Read-only metadata from compatibility aggregation.", - "destinationAmbiguous": "Compatibility destination is ambiguous because zero or multiple managed legacy providers are present.", "webhookUrl": "Webhook URL (Optional)", "webhookUrlHelp": "POST requests will be sent to this URL when security events occur.", "emailRecipients": "Email Recipients (Optional)", diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index c88d31f4..d8643276 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -577,15 +577,6 @@ "testFailed": "Failed to send test", "deleteConfirm": "Are you sure?", "noProviders": "No notification providers configured.", - "securityNotifications": "Security Event Notifications", - "securityNotificationsDescription": "Compatibility view for legacy security notification settings.", - "securityCompatibilityDeprecated": "This compatibility endpoint is deprecated. Configure security events per provider in Notification Events.", - "securityCompatibilityNotifyOnly": "Notify-only behavior is preserved: only enabled providers subscribed to these events receive notifications.", - "enableAlerts": "Enable Security Alerts", - "alertsDescription": "Receive notifications when security events occur.", - "minLogLevel": "Minimum Log Level", - "minLogLevelHelp": "Only events at this level or higher will trigger notifications.", - "notifyOn": "Notify On:", "securityEventSubscriptions": "Security Event Subscriptions", "securityEventSubscriptionsHelp": "Select security events for each provider in this form.", "wafBlocks": "WAF Blocks", @@ -594,10 +585,6 @@ "aclDenialsHelp": "When an IP is denied by Access Control Lists.", "rateLimitHits": "Rate Limit Hits", "rateLimitHitsHelp": "When a client exceeds rate limiting thresholds.", - "compatibilityAggregateHelp": "Read-only aggregate across enabled providers.", - "compatibilityDestination": "Compatibility Destination", - "compatibilityDestinationHelp": "Read-only metadata from compatibility aggregation.", - "destinationAmbiguous": "Compatibility destination is ambiguous because zero or multiple managed legacy providers are present.", "webhookUrl": "Webhook URL (Optional)", "webhookUrlHelp": "POST requests will be sent to this URL when security events occur.", "emailRecipients": "Email Recipients (Optional)", diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json index 8e3048f5..77b0b9be 100644 --- a/frontend/src/locales/es/translation.json +++ b/frontend/src/locales/es/translation.json @@ -502,15 +502,6 @@ "testFailed": "Error al enviar prueba", "deleteConfirm": "¿Estás seguro?", "noProviders": "No hay proveedores de notificaciones configurados.", - "securityNotifications": "Security Event Notifications", - "securityNotificationsDescription": "Compatibility view for legacy security notification settings.", - "securityCompatibilityDeprecated": "This compatibility endpoint is deprecated. Configure security events per provider in Notification Events.", - "securityCompatibilityNotifyOnly": "Notify-only behavior is preserved: only enabled providers subscribed to these events receive notifications.", - "enableAlerts": "Enable Security Alerts", - "alertsDescription": "Receive notifications when security events occur.", - "minLogLevel": "Minimum Log Level", - "minLogLevelHelp": "Only events at this level or higher will trigger notifications.", - "notifyOn": "Notify On:", "securityEventSubscriptions": "Security Event Subscriptions", "securityEventSubscriptionsHelp": "Select security events for each provider in this form.", "wafBlocks": "WAF Blocks", @@ -519,10 +510,6 @@ "aclDenialsHelp": "When an IP is denied by Access Control Lists.", "rateLimitHits": "Rate Limit Hits", "rateLimitHitsHelp": "When a client exceeds rate limiting thresholds.", - "compatibilityAggregateHelp": "Read-only aggregate across enabled providers.", - "compatibilityDestination": "Compatibility Destination", - "compatibilityDestinationHelp": "Read-only metadata from compatibility aggregation.", - "destinationAmbiguous": "Compatibility destination is ambiguous because zero or multiple managed legacy providers are present.", "webhookUrl": "Webhook URL (Optional)", "webhookUrlHelp": "POST requests will be sent to this URL when security events occur.", "emailRecipients": "Email Recipients (Optional)", diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json index 32e5d306..80fa90f5 100644 --- a/frontend/src/locales/fr/translation.json +++ b/frontend/src/locales/fr/translation.json @@ -502,15 +502,6 @@ "testFailed": "Échec de l'envoi du test", "deleteConfirm": "Êtes-vous sûr?", "noProviders": "Aucun fournisseur de notifications configuré.", - "securityNotifications": "Security Event Notifications", - "securityNotificationsDescription": "Compatibility view for legacy security notification settings.", - "securityCompatibilityDeprecated": "This compatibility endpoint is deprecated. Configure security events per provider in Notification Events.", - "securityCompatibilityNotifyOnly": "Notify-only behavior is preserved: only enabled providers subscribed to these events receive notifications.", - "enableAlerts": "Enable Security Alerts", - "alertsDescription": "Receive notifications when security events occur.", - "minLogLevel": "Minimum Log Level", - "minLogLevelHelp": "Only events at this level or higher will trigger notifications.", - "notifyOn": "Notify On:", "securityEventSubscriptions": "Security Event Subscriptions", "securityEventSubscriptionsHelp": "Select security events for each provider in this form.", "wafBlocks": "WAF Blocks", @@ -519,10 +510,6 @@ "aclDenialsHelp": "When an IP is denied by Access Control Lists.", "rateLimitHits": "Rate Limit Hits", "rateLimitHitsHelp": "When a client exceeds rate limiting thresholds.", - "compatibilityAggregateHelp": "Read-only aggregate across enabled providers.", - "compatibilityDestination": "Compatibility Destination", - "compatibilityDestinationHelp": "Read-only metadata from compatibility aggregation.", - "destinationAmbiguous": "Compatibility destination is ambiguous because zero or multiple managed legacy providers are present.", "webhookUrl": "Webhook URL (Optional)", "webhookUrlHelp": "POST requests will be sent to this URL when security events occur.", "emailRecipients": "Email Recipients (Optional)", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 518c5156..34a3e260 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -502,15 +502,6 @@ "testFailed": "发送测试失败", "deleteConfirm": "您确定吗?", "noProviders": "未配置通知提供商。", - "securityNotifications": "Security Event Notifications", - "securityNotificationsDescription": "Compatibility view for legacy security notification settings.", - "securityCompatibilityDeprecated": "This compatibility endpoint is deprecated. Configure security events per provider in Notification Events.", - "securityCompatibilityNotifyOnly": "Notify-only behavior is preserved: only enabled providers subscribed to these events receive notifications.", - "enableAlerts": "Enable Security Alerts", - "alertsDescription": "Receive notifications when security events occur.", - "minLogLevel": "Minimum Log Level", - "minLogLevelHelp": "Only events at this level or higher will trigger notifications.", - "notifyOn": "Notify On:", "securityEventSubscriptions": "Security Event Subscriptions", "securityEventSubscriptionsHelp": "Select security events for each provider in this form.", "wafBlocks": "WAF Blocks", @@ -519,10 +510,6 @@ "aclDenialsHelp": "When an IP is denied by Access Control Lists.", "rateLimitHits": "Rate Limit Hits", "rateLimitHitsHelp": "When a client exceeds rate limiting thresholds.", - "compatibilityAggregateHelp": "Read-only aggregate across enabled providers.", - "compatibilityDestination": "Compatibility Destination", - "compatibilityDestinationHelp": "Read-only metadata from compatibility aggregation.", - "destinationAmbiguous": "Compatibility destination is ambiguous because zero or multiple managed legacy providers are present.", "webhookUrl": "Webhook URL (Optional)", "webhookUrlHelp": "POST requests will be sent to this URL when security events occur.", "emailRecipients": "Email Recipients (Optional)", diff --git a/frontend/src/pages/Notifications.tsx b/frontend/src/pages/Notifications.tsx index 454d8738..fd2a707c 100644 --- a/frontend/src/pages/Notifications.tsx +++ b/frontend/src/pages/Notifications.tsx @@ -2,11 +2,9 @@ import { useEffect, useState, type FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getProviders, createProvider, updateProvider, deleteProvider, testProvider, getTemplates, previewProvider, NotificationProvider, getExternalTemplates, previewExternalTemplate, ExternalTemplate, createExternalTemplate, updateExternalTemplate, deleteExternalTemplate, NotificationTemplate } from '../api/notifications'; -import { useSecurityNotificationSettings } from '../hooks/useNotifications'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { Switch } from '../components/ui/Switch'; -import { Bell, Plus, Trash2, Edit2, Send, Check, X, Loader2, Shield, AlertTriangle } from 'lucide-react'; +import { Bell, Plus, Trash2, Edit2, Send, Check, X, Loader2 } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { toast } from '../utils/toast'; @@ -373,160 +371,6 @@ const TemplateForm: FC<{ ); }; -const SecurityNotificationsSection: FC = () => { - const { t } = useTranslation(); - const { data: settings, isLoading } = useSecurityNotificationSettings(); - - const compatibilityDestination = - settings?.webhook_url || - settings?.discord_webhook_url || - settings?.slack_webhook_url || - settings?.gotify_url || - ''; - - return ( - -
-
- -

- {t('notificationProviders.securityNotifications')} -

-
-

- {t('notificationProviders.securityNotificationsDescription')} -

- -
-
-
-

- {t('notificationProviders.securityCompatibilityNotifyOnly')} -

-
- - {isLoading ? ( -
{t('common.loading')}
- ) : ( -
-
-
- -

- {t('notificationProviders.alertsDescription')} -

-
- -
- -
- - -

{t('notificationProviders.minLogLevelHelp')}

-
- -
-

{t('notificationProviders.notifyOn')}

- -
-
- -

{t('notificationProviders.compatibilityAggregateHelp')}

-
- -
- -
-
- -

{t('notificationProviders.compatibilityAggregateHelp')}

-
- -
- -
-
- -

{t('notificationProviders.compatibilityAggregateHelp')}

-
- -
-
- -
- - -

{t('notificationProviders.compatibilityDestinationHelp')}

-
- - {settings?.destination_ambiguous ? ( -

- {t('notificationProviders.destinationAmbiguous')} -

- ) : null} -
- )} -
-
- ); -}; - const Notifications: FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -611,9 +455,6 @@ const Notifications: FC = () => { - {/* Security Event Notifications */} - - {/* External Templates Management */}

{t('notificationProviders.externalTemplates')}

diff --git a/frontend/src/pages/__tests__/Notifications.test.tsx b/frontend/src/pages/__tests__/Notifications.test.tsx index 72c626af..a645601e 100644 --- a/frontend/src/pages/__tests__/Notifications.test.tsx +++ b/frontend/src/pages/__tests__/Notifications.test.tsx @@ -5,7 +5,7 @@ import Notifications from '../Notifications' import { renderWithQueryClient } from '../../test-utils/renderWithQueryClient' import * as notificationsApi from '../../api/notifications' import { toast } from '../../utils/toast' -import type { NotificationProvider, SecurityNotificationSettings } from '../../api/notifications' +import type { NotificationProvider } from '../../api/notifications' vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -26,8 +26,6 @@ vi.mock('../../api/notifications', () => ({ createExternalTemplate: vi.fn(), updateExternalTemplate: vi.fn(), deleteExternalTemplate: vi.fn(), - getSecurityNotificationSettings: vi.fn(), - updateSecurityNotificationSettings: vi.fn(), })) vi.mock('../../utils/toast', () => ({ @@ -56,24 +54,12 @@ const baseProvider: NotificationProvider = { created_at: '2024-01-01T00:00:00Z', } -const mockSecuritySettings: SecurityNotificationSettings = { - enabled: false, - min_log_level: 'warn', - security_waf_enabled: true, - security_acl_enabled: true, - security_rate_limit_enabled: true, - destination_ambiguous: false, - webhook_url: '', -} - const setupMocks = (providers: NotificationProvider[] = []) => { vi.mocked(notificationsApi.getProviders).mockResolvedValue(providers) vi.mocked(notificationsApi.getTemplates).mockResolvedValue([]) vi.mocked(notificationsApi.getExternalTemplates).mockResolvedValue([]) vi.mocked(notificationsApi.createProvider).mockResolvedValue(baseProvider) vi.mocked(notificationsApi.updateProvider).mockResolvedValue(baseProvider) - vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSecuritySettings) - vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(mockSecuritySettings) } describe('Notifications', () => { @@ -277,10 +263,11 @@ describe('Notifications', () => { confirmSpy.mockRestore() }) - it('renders the security notifications section', async () => { + it('does not render a standalone security notifications section', async () => { renderWithQueryClient() - expect(await screen.findByTestId('security-notifications-section')).toBeInTheDocument() - expect(screen.getByTestId('security-compatibility-banner')).toBeInTheDocument() + await screen.findByTestId('add-provider-btn') + expect(screen.queryByTestId('security-notifications-section')).toBeNull() + expect(screen.queryByTestId('security-compatibility-banner')).toBeNull() }) it('shows security event subscription controls in provider form', async () => { @@ -294,23 +281,11 @@ describe('Notifications', () => { expect(screen.getByTestId('notify-security-rate-limit-hits')).toBeInTheDocument() }) - it('renders compatibility security settings as read-only', async () => { + it('does not show compatibility section hints anywhere on the page', async () => { renderWithQueryClient() - - const enableToggle = await screen.findByTestId('security-notifications-enabled') - const minLogLevel = screen.getByTestId('security-min-log-level') as HTMLSelectElement - const destination = screen.getByTestId('security-webhook-url') as HTMLInputElement - - expect(enableToggle).toBeDisabled() - expect(minLogLevel.disabled).toBe(true) - expect(destination.disabled).toBe(true) - expect(screen.queryByTestId('security-notifications-save-btn')).toBeNull() - }) - - it('does not show Shoutrrr help link anywhere on the page', async () => { - renderWithQueryClient() - await screen.findByTestId('security-notifications-section') + await screen.findByTestId('add-provider-btn') expect(screen.queryByText(/shoutrrr/i)).toBeNull() + expect(screen.queryByText(/compatibility endpoint/i)).toBeNull() expect(document.querySelector('a[href*="containrrr.dev"]')).toBeNull() }) diff --git a/frontend/src/pages/__tests__/ProxyHosts-coverage.test.tsx b/frontend/src/pages/__tests__/ProxyHosts-coverage.test.tsx index b7a7b273..343f73b0 100644 --- a/frontend/src/pages/__tests__/ProxyHosts-coverage.test.tsx +++ b/frontend/src/pages/__tests__/ProxyHosts-coverage.test.tsx @@ -115,7 +115,7 @@ describe('ProxyHosts - Coverage enhancements', () => { // Save await user.click(await screen.findByRole('button', { name: 'Save' })) await waitFor(() => expect(proxyHostsApi.createProxyHost).toHaveBeenCalled()) - }) + }, 15000) it('handles equal sort values gracefully', async () => { const host1 = baseHost({ uuid: 'e1', name: 'Same', domain_names: 'a.example.com' }) diff --git a/tests/settings/notifications.spec.ts b/tests/settings/notifications.spec.ts index f4e69f86..b24e952b 100644 --- a/tests/settings/notifications.spec.ts +++ b/tests/settings/notifications.spec.ts @@ -92,6 +92,10 @@ test.describe('Notification Providers', () => { const errorAlert = page.getByRole('alert').filter({ hasText: /error|failed/i }); await expect(errorAlert).toHaveCount(0); }); + + await test.step('Verify standalone security compatibility section is not rendered', async () => { + await expect(page.getByTestId('security-notifications-section')).toHaveCount(0); + }); }); /** @@ -1281,6 +1285,13 @@ test.describe('Notification Providers', () => { await expect(page.getByTestId('notify-domains')).toBeVisible(); await expect(page.getByTestId('notify-certs')).toBeVisible(); await expect(page.getByTestId('notify-uptime')).toBeVisible(); + await expect(page.getByTestId('notify-security-waf-blocks')).toBeVisible(); + await expect(page.getByTestId('notify-security-acl-denies')).toBeVisible(); + await expect(page.getByTestId('notify-security-rate-limit-hits')).toBeVisible(); + }); + + await test.step('Verify no standalone compatibility section exists', async () => { + await expect(page.getByTestId('security-notifications-section')).toHaveCount(0); }); await test.step('Toggle each event type', async () => {