feat(tests): enhance test coverage and error handling across various components
- Added a test case in CrowdSecConfig to show improved error message when preset is not cached. - Introduced a new test suite for the Dashboard component, verifying counts and health status. - Updated SMTPSettings tests to utilize a shared render function and added tests for backend validation errors. - Modified Security.audit tests to improve input handling and removed redundant export failure test. - Refactored Security tests to remove export functionality and ensure correct rendering of components. - Enhanced UsersPage tests with new scenarios for updating user permissions and manual invite link flow. - Created a new utility for rendering components with a QueryClient and MemoryRouter for better test isolation. - Updated go-test-coverage script to improve error handling and coverage reporting.
This commit is contained in:
@@ -55,6 +55,8 @@ const renderWithProviders = (children: ReactNode) => {
|
||||
describe('Layout', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
localStorage.setItem('sidebarCollapsed', 'false')
|
||||
// Default: all features enabled
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.cerberus.enabled': true,
|
||||
@@ -148,6 +150,31 @@ describe('Layout', () => {
|
||||
expect(toggleButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('persists collapse state to localStorage', async () => {
|
||||
localStorage.clear()
|
||||
renderWithProviders(
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
const collapseBtn = await screen.findByTitle('Collapse sidebar')
|
||||
await userEvent.click(collapseBtn)
|
||||
expect(JSON.parse(localStorage.getItem('sidebarCollapsed') || 'false')).toBe(true)
|
||||
})
|
||||
|
||||
it('restores collapsed state from localStorage on load', async () => {
|
||||
localStorage.setItem('sidebarCollapsed', 'true')
|
||||
|
||||
renderWithProviders(
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
expect(await screen.findByTitle('Expand sidebar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('Feature Flags - Conditional Sidebar Items', () => {
|
||||
it('displays Cerberus nav item when Cerberus is enabled', async () => {
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
@@ -255,7 +282,7 @@ describe('Layout', () => {
|
||||
|
||||
it('defaults to showing Cerberus and Uptime when feature flags are loading', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue(undefined as any)
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({} as any)
|
||||
|
||||
renderWithProviders(
|
||||
<Layout>
|
||||
|
||||
315
frontend/src/components/__tests__/LiveLogViewer.test.tsx
Normal file
315
frontend/src/components/__tests__/LiveLogViewer.test.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { LiveLogViewer } from '../LiveLogViewer';
|
||||
import * as logsApi from '../../api/logs';
|
||||
|
||||
// Mock the connectLiveLogs function
|
||||
vi.mock('../../api/logs', async () => {
|
||||
const actual = await vi.importActual('../../api/logs');
|
||||
return {
|
||||
...actual,
|
||||
connectLiveLogs: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('LiveLogViewer', () => {
|
||||
let mockCloseConnection: ReturnType<typeof vi.fn>;
|
||||
let mockOnMessage: ((log: logsApi.LiveLogEntry) => void) | null;
|
||||
let mockOnClose: (() => void) | null;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCloseConnection = vi.fn();
|
||||
mockOnMessage = null;
|
||||
mockOnClose = null;
|
||||
|
||||
vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, onMessage, onOpen, _onError, onClose) => {
|
||||
mockOnMessage = onMessage;
|
||||
mockOnClose = onClose ?? null;
|
||||
// Simulate connection success
|
||||
if (onOpen) {
|
||||
setTimeout(() => onOpen(), 0);
|
||||
}
|
||||
return mockCloseConnection as () => void;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the component with initial state', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
expect(screen.getByText('Live Security Logs')).toBeTruthy();
|
||||
// Initially disconnected until WebSocket opens
|
||||
expect(screen.getByText('Disconnected')).toBeTruthy();
|
||||
|
||||
// Wait for onOpen callback to be called
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connected')).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('displays incoming log messages', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Simulate receiving a log
|
||||
const logEntry: logsApi.LiveLogEntry = {
|
||||
level: 'info',
|
||||
timestamp: '2025-12-09T10:30:00Z',
|
||||
message: 'Test log message',
|
||||
source: 'test',
|
||||
};
|
||||
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage(logEntry);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test log message')).toBeTruthy();
|
||||
expect(screen.getByText('INFO')).toBeTruthy();
|
||||
expect(screen.getByText('[test]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('filters logs by text', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Add multiple logs
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'First message' });
|
||||
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Second message' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('First message')).toBeTruthy();
|
||||
expect(screen.getByText('Second message')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Apply text filter
|
||||
const filterInput = screen.getByPlaceholderText('Filter by text...');
|
||||
await user.type(filterInput, 'First');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('First message')).toBeTruthy();
|
||||
expect(screen.queryByText('Second message')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('filters logs by level', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Add multiple logs
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Info message' });
|
||||
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Error message' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Info message')).toBeTruthy();
|
||||
expect(screen.getByText('Error message')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Apply level filter
|
||||
const levelSelect = screen.getByRole('combobox');
|
||||
await user.selectOptions(levelSelect, 'error');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Info message')).toBeFalsy();
|
||||
expect(screen.getByText('Error message')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('pauses and resumes log streaming', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Add initial log
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Before pause' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Before pause')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click pause button
|
||||
const pauseButton = screen.getByTitle('Pause');
|
||||
await user.click(pauseButton);
|
||||
|
||||
// Verify paused state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('⏸ Paused')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Try to add log while paused (should not appear)
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'During pause' });
|
||||
}
|
||||
|
||||
// Log should not appear
|
||||
expect(screen.queryByText('During pause')).toBeFalsy();
|
||||
|
||||
// Resume
|
||||
const resumeButton = screen.getByTitle('Resume');
|
||||
await user.click(resumeButton);
|
||||
|
||||
// Add log after resume
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'After resume' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('After resume')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears all logs', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Add logs
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log 1')).toBeTruthy();
|
||||
expect(screen.getByText('Log 2')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click clear button
|
||||
const clearButton = screen.getByTitle('Clear logs');
|
||||
await user.click(clearButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Log 1')).toBeFalsy();
|
||||
expect(screen.queryByText('Log 2')).toBeFalsy();
|
||||
expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('limits the number of stored logs', async () => {
|
||||
render(<LiveLogViewer maxLogs={2} />);
|
||||
|
||||
// Add 3 logs (exceeding maxLogs)
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'Log 3' });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
// First log should be removed, only last 2 should remain
|
||||
expect(screen.queryByText('Log 1')).toBeFalsy();
|
||||
expect(screen.getByText('Log 2')).toBeTruthy();
|
||||
expect(screen.getByText('Log 3')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays log data when available', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
const logWithData: logsApi.LiveLogEntry = {
|
||||
level: 'error',
|
||||
timestamp: '2025-12-09T10:30:00Z',
|
||||
message: 'Error occurred',
|
||||
data: { error_code: 500, details: 'Internal server error' },
|
||||
};
|
||||
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage(logWithData);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error occurred')).toBeTruthy();
|
||||
// Check that data is rendered as JSON
|
||||
expect(screen.getByText(/"error_code"/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes WebSocket connection on unmount', () => {
|
||||
const { unmount } = render(<LiveLogViewer />);
|
||||
|
||||
expect(logsApi.connectLiveLogs).toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockCloseConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<LiveLogViewer className="custom-class" />);
|
||||
|
||||
const element = container.querySelector('.custom-class');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows correct connection status', async () => {
|
||||
let mockOnOpen: (() => void) | undefined;
|
||||
let mockOnError: ((error: Event) => void) | undefined;
|
||||
|
||||
vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, _onMessage, onOpen, onError) => {
|
||||
mockOnOpen = onOpen;
|
||||
mockOnError = onError;
|
||||
return mockCloseConnection as () => void;
|
||||
});
|
||||
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
// Initially disconnected until onOpen is called
|
||||
expect(screen.getByText('Disconnected')).toBeTruthy();
|
||||
|
||||
// Simulate connection opened
|
||||
if (mockOnOpen) {
|
||||
mockOnOpen();
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connected')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Simulate connection error
|
||||
if (mockOnError) {
|
||||
mockOnError(new Event('error'));
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Disconnected')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows no-match message when filters exclude all logs', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
if (mockOnMessage) {
|
||||
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Visible' });
|
||||
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Hidden' });
|
||||
}
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Visible')).toBeTruthy());
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Filter by text...'), 'nomatch');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No logs match the current filters.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('marks connection as disconnected when WebSocket closes', async () => {
|
||||
render(<LiveLogViewer />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
||||
|
||||
mockOnClose?.();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Disconnected')).toBeTruthy());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
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<typeof createTestQueryClient>;
|
||||
|
||||
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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SecurityNotificationSettingsModal isOpen={isOpen} onClose={onClose} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user