import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClientProvider } from '@tanstack/react-query'; import { SecurityNotificationSettingsModal } from '../SecurityNotificationSettingsModal'; import { createTestQueryClient } from '../../test/createTestQueryClient'; import * as notificationsApi from '../../api/notifications'; // Mock the API vi.mock('../../api/notifications', async () => { const actual = await vi.importActual('../../api/notifications'); return { ...actual, getSecurityNotificationSettings: vi.fn(), updateSecurityNotificationSettings: vi.fn(), }; }); // Mock toast vi.mock('../../utils/toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), }, })); describe('SecurityNotificationSettingsModal', () => { const mockSettings: notificationsApi.SecurityNotificationSettings = { enabled: true, min_log_level: 'warn', notify_waf_blocks: true, notify_acl_denials: true, notify_rate_limit_hits: false, webhook_url: 'https://example.com/webhook', email_recipients: 'admin@example.com', }; let queryClient: ReturnType; beforeEach(() => { queryClient = createTestQueryClient(); vi.clearAllMocks(); vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings); vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(mockSettings); }); const renderModal = (isOpen = true, onClose = vi.fn()) => { return render( ); }; it('does not render when isOpen is false', () => { renderModal(false); expect(screen.queryByText('Security Notification Settings')).toBeFalsy(); }); it('renders the modal when isOpen is true', async () => { renderModal(); await waitFor(() => { expect(screen.getByText('Security Notification Settings')).toBeTruthy(); }); }); it('loads and displays existing settings', async () => { renderModal(); await waitFor(() => { expect(screen.getByLabelText('Enable Notifications')).toBeTruthy(); }); // Check that settings are loaded const enableSwitch = screen.getByLabelText('Enable Notifications') as HTMLInputElement; expect(enableSwitch.checked).toBe(true); const levelSelect = screen.getByLabelText(/minimum log level/i) as HTMLSelectElement; expect(levelSelect.value).toBe('warn'); const webhookInput = screen.getByPlaceholderText(/your-webhook-endpoint/i) as HTMLInputElement; expect(webhookInput.value).toBe('https://example.com/webhook'); }); it('closes modal when close button is clicked', async () => { const user = userEvent.setup(); const mockOnClose = vi.fn(); renderModal(true, mockOnClose); await waitFor(() => { expect(screen.getByText('Security Notification Settings')).toBeTruthy(); }); const closeButton = screen.getByLabelText('Close'); await user.click(closeButton); expect(mockOnClose).toHaveBeenCalled(); }); it('closes modal when clicking outside', async () => { const user = userEvent.setup(); const mockOnClose = vi.fn(); const { container } = renderModal(true, mockOnClose); await waitFor(() => { expect(screen.getByText('Security Notification Settings')).toBeTruthy(); }); // Click on the backdrop const backdrop = container.querySelector('.fixed.inset-0'); if (backdrop) { await user.click(backdrop); expect(mockOnClose).toHaveBeenCalled(); } }); it('submits updated settings', async () => { const user = userEvent.setup(); const mockOnClose = vi.fn(); renderModal(true, mockOnClose); await waitFor(() => { expect(screen.getByLabelText('Enable Notifications')).toBeTruthy(); }); // Change minimum log level const levelSelect = screen.getByLabelText(/minimum log level/i); await user.selectOptions(levelSelect, 'error'); // Change webhook URL const webhookInput = screen.getByPlaceholderText(/your-webhook-endpoint/i); await user.clear(webhookInput); await user.type(webhookInput, 'https://new-webhook.com'); // Submit form const saveButton = screen.getByRole('button', { name: /save settings/i }); await user.click(saveButton); await waitFor(() => { expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith( expect.objectContaining({ min_log_level: 'error', webhook_url: 'https://new-webhook.com', }) ); }); // Modal should close on success await waitFor(() => { expect(mockOnClose).toHaveBeenCalled(); }); }); it('toggles notification enable/disable', async () => { const user = userEvent.setup(); renderModal(); await waitFor(() => { expect(screen.getByLabelText('Enable Notifications')).toBeTruthy(); }); const enableSwitch = screen.getByLabelText('Enable Notifications') as HTMLInputElement; expect(enableSwitch.checked).toBe(true); // Disable notifications await user.click(enableSwitch); await waitFor(() => { expect(enableSwitch.checked).toBe(false); }); }); it('disables controls when notifications are disabled', async () => { vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue({ ...mockSettings, enabled: false, }); renderModal(); // Wait for settings to be loaded and form to render await waitFor(() => { const enableSwitch = screen.getByLabelText('Enable Notifications') as HTMLInputElement; expect(enableSwitch.checked).toBe(false); }); const levelSelect = screen.getByLabelText(/minimum log level/i) as HTMLSelectElement; expect(levelSelect.disabled).toBe(true); const webhookInput = screen.getByPlaceholderText(/your-webhook-endpoint/i) as HTMLInputElement; expect(webhookInput.disabled).toBe(true); }); it('toggles event type filters', async () => { const user = userEvent.setup(); renderModal(); await waitFor(() => { expect(screen.getByText('WAF Blocks')).toBeTruthy(); }); // Find and toggle WAF blocks switch const wafSwitch = screen.getByLabelText('WAF Blocks') as HTMLInputElement; expect(wafSwitch.checked).toBe(true); await user.click(wafSwitch); // Submit form const saveButton = screen.getByRole('button', { name: /save settings/i }); await user.click(saveButton); await waitFor(() => { expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith( expect.objectContaining({ notify_waf_blocks: false, }) ); }); }); it('handles API errors gracefully', async () => { const user = userEvent.setup(); const mockOnClose = vi.fn(); vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockRejectedValue( new Error('API Error') ); renderModal(true, mockOnClose); await waitFor(() => { expect(screen.getByText('Security Notification Settings')).toBeTruthy(); }); // Submit form const saveButton = screen.getByRole('button', { name: /save settings/i }); await user.click(saveButton); await waitFor(() => { expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalled(); }); // Modal should NOT close on error expect(mockOnClose).not.toHaveBeenCalled(); }); it('shows loading state', () => { vi.mocked(notificationsApi.getSecurityNotificationSettings).mockReturnValue( new Promise(() => {}) // Never resolves ); renderModal(); expect(screen.getByText('Loading settings...')).toBeTruthy(); }); it('handles email recipients input', async () => { const user = userEvent.setup(); renderModal(); await waitFor(() => { expect(screen.getByPlaceholderText(/admin@example.com/i)).toBeTruthy(); }); const emailInput = screen.getByPlaceholderText(/admin@example.com/i); await user.clear(emailInput); await user.type(emailInput, 'user1@test.com, user2@test.com'); const saveButton = screen.getByRole('button', { name: /save settings/i }); await user.click(saveButton); await waitFor(() => { expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith( expect.objectContaining({ email_recipients: 'user1@test.com, user2@test.com', }) ); }); }); it('prevents modal content clicks from closing modal', async () => { const user = userEvent.setup(); const mockOnClose = vi.fn(); renderModal(true, mockOnClose); await waitFor(() => { expect(screen.getByText('Security Notification Settings')).toBeTruthy(); }); // Click inside the modal content const modalContent = screen.getByText('Security Notification Settings'); await user.click(modalContent); // Modal should not close expect(mockOnClose).not.toHaveBeenCalled(); }); });