fix(tests): Enhance CrowdSecConfig with new input fields and improve accessibility
- Added IDs to input fields in CrowdSecConfig for better accessibility. - Updated labels to use <label> elements for checkboxes and inputs. - Improved error handling and user feedback in the CrowdSecConfig tests. - Enhanced test coverage for console enrollment and banned IP functionalities. fix: Update SecurityHeaders to include aria-label for delete button - Added aria-label to the delete button for better screen reader support. test: Add comprehensive tests for proxyHostsHelpers and validation utilities - Implemented tests for formatting and help text functions in proxyHostsHelpers. - Added validation tests for email and IP address formats. chore: Update vitest configuration for dynamic coverage thresholds - Adjusted coverage thresholds to be dynamic based on environment variables. - Included additional coverage reporters. chore: Update frontend-test-coverage script to reflect new coverage threshold - Increased minimum coverage requirement from 85% to 87.5%. fix: Ensure tests pass with consistent data in passwd file - Updated tests/etc/passwd to ensure consistent content.
This commit is contained in:
@@ -7,19 +7,20 @@ import CrowdSecConfig from '../CrowdSecConfig'
|
||||
import * as securityApi from '../../api/security'
|
||||
import * as crowdsecApi from '../../api/crowdsec'
|
||||
import * as backupsApi from '../../api/backups'
|
||||
import * as settingsApi from '../../api/settings'
|
||||
import * as presetsApi from '../../api/presets'
|
||||
import * as featureFlagsApi from '../../api/featureFlags'
|
||||
import { toast } from '../../utils/toast'
|
||||
|
||||
vi.mock('../../api/security')
|
||||
vi.mock('../../api/crowdsec')
|
||||
vi.mock('../../api/backups')
|
||||
vi.mock('../../api/settings')
|
||||
vi.mock('../../api/presets')
|
||||
vi.mock('../../api/featureFlags')
|
||||
vi.mock('../../utils/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -32,7 +33,7 @@ describe('CrowdSecConfig', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithProviders = () => {
|
||||
const renderComponent = () => {
|
||||
const queryClient = createClient()
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -45,6 +46,8 @@ describe('CrowdSecConfig', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Default mocks
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue({
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: true },
|
||||
@@ -52,13 +55,29 @@ describe('CrowdSecConfig', () => {
|
||||
rate_limit: { enabled: true },
|
||||
acl: { enabled: true },
|
||||
})
|
||||
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] })
|
||||
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: '' })
|
||||
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.crowdsec.console_enrollment': true
|
||||
})
|
||||
|
||||
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['config.yaml', 'profiles.yaml'] })
|
||||
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: 'yaml content' })
|
||||
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({})
|
||||
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({ decisions: [] })
|
||||
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({
|
||||
decisions: [
|
||||
{ id: '1', ip: '1.2.3.4', reason: 'ssh-bf', duration: '23h', created_at: '2023-01-01', source: 'local' }
|
||||
]
|
||||
})
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob(['data']))
|
||||
vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue({})
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.banIP).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.unbanIP).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({
|
||||
running: true,
|
||||
pid: 123,
|
||||
lapi_ready: true,
|
||||
})
|
||||
|
||||
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
|
||||
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({ presets: [] })
|
||||
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
|
||||
@@ -67,40 +86,163 @@ describe('CrowdSecConfig', () => {
|
||||
preview: 'configs: {}',
|
||||
cache_key: 'cache-123',
|
||||
})
|
||||
vi.mocked(presetsApi.applyCrowdsecPreset).mockResolvedValue({ status: 'applied', backup: '/tmp/backup.tar.gz', cache_key: 'cache-123' })
|
||||
vi.mocked(presetsApi.getCrowdsecPresetCache).mockResolvedValue({ preview: 'configs: {}', cache_key: 'cache-123' })
|
||||
|
||||
// Window Prompt Mock
|
||||
vi.spyOn(window, 'prompt').mockReturnValue('crowdsec-export.tar.gz')
|
||||
window.URL.createObjectURL = vi.fn(() => 'blob:url')
|
||||
window.URL.revokeObjectURL = vi.fn()
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('shows info banner directing to Security Dashboard', async () => {
|
||||
renderWithProviders()
|
||||
|
||||
await waitFor(() => screen.getByText(/CrowdSec is controlled via the toggle on the/i))
|
||||
expect(screen.getByRole('link', { name: /Security/i })).toHaveAttribute('href', '/security')
|
||||
})
|
||||
|
||||
it('exports configuration packages with prompted filename', async () => {
|
||||
renderWithProviders()
|
||||
|
||||
await waitFor(() => screen.getByRole('button', { name: /Export/i }))
|
||||
const exportButton = screen.getByRole('button', { name: /Export/i })
|
||||
|
||||
await userEvent.click(exportButton)
|
||||
// 1. Rendering basic elements
|
||||
it('renders page configuration elements', async () => {
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(crowdsecApi.exportCrowdsecConfig).toHaveBeenCalled()
|
||||
expect(toast.success).toHaveBeenCalledWith('CrowdSec configuration exported')
|
||||
expect(screen.getByText('CrowdSec Configuration')).toBeInTheDocument()
|
||||
expect(screen.getByText('Configuration Packages')).toBeInTheDocument()
|
||||
// Updated text to match translation file
|
||||
expect(screen.getByText('Edit Configuration Files')).toBeInTheDocument()
|
||||
expect(screen.getByText('Banned IPs')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows Configuration Packages heading', async () => {
|
||||
renderWithProviders()
|
||||
// 2. File Editor
|
||||
it('allows reading and saving config files', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByText('Configuration Packages'))
|
||||
await waitFor(() => screen.getByTestId('crowdsec-file-select'))
|
||||
|
||||
expect(screen.getByText('Configuration Packages')).toBeInTheDocument()
|
||||
// Select file
|
||||
const select = screen.getByTestId('crowdsec-file-select')
|
||||
await user.selectOptions(select, 'config.yaml')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(crowdsecApi.readCrowdsecFile).toHaveBeenCalledWith('config.yaml')
|
||||
expect(screen.getByDisplayValue('yaml content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Edit content
|
||||
const textarea = screen.getByDisplayValue('yaml content')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'new content')
|
||||
|
||||
// Save
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalledWith('config.yaml', 'new content')
|
||||
expect(backupsApi.createBackup).toHaveBeenCalled() // Should backup first
|
||||
})
|
||||
})
|
||||
|
||||
// 3. Banned IPs Table
|
||||
it('renders banned IPs table', async () => {
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1.2.3.4')).toBeInTheDocument()
|
||||
expect(screen.getByText('ssh-bf')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// 4. Ban IP Action
|
||||
it('allows banning an IP', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByText('Ban IP'))
|
||||
|
||||
// Click Ban IP trigger (using ID we added)
|
||||
await user.click(screen.getByTestId('ban-ip-trigger'))
|
||||
|
||||
// Modal opens
|
||||
await waitFor(() => screen.getByText('Ban IP Address'))
|
||||
|
||||
// Fill form
|
||||
await user.type(screen.getByLabelText(/IP Address/i), '5.6.7.8')
|
||||
await user.type(screen.getByPlaceholderText(/Reason for ban/i), 'manual ban')
|
||||
|
||||
// Submit - Target the last button with name "Ban IP" (modal button)
|
||||
const buttons = screen.getAllByRole('button', { name: 'Ban IP' })
|
||||
const submitBtn = buttons[buttons.length - 1]
|
||||
await user.click(submitBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(crowdsecApi.banIP).toHaveBeenCalledWith('5.6.7.8', '24h', 'manual ban')
|
||||
expect(toast.success).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// 5. Unban IP Action
|
||||
it('allows unbanning an IP', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByText('1.2.3.4'))
|
||||
|
||||
const unbanBtns = screen.getAllByRole('button', { name: 'Unban' })
|
||||
expect(unbanBtns.length).toBeGreaterThan(0)
|
||||
|
||||
// Click the unban button in the table (first one)
|
||||
await user.click(unbanBtns[0])
|
||||
|
||||
// Confirm modal
|
||||
await waitFor(() => screen.getByText('Confirm Unban'))
|
||||
|
||||
// Click confirm in modal. Use getAllByRole to get the modal one (last one)
|
||||
const modalButtons = screen.getAllByRole('button', { name: 'Unban' })
|
||||
const confirmBtn = modalButtons[modalButtons.length - 1]
|
||||
await user.click(confirmBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(crowdsecApi.unbanIP).toHaveBeenCalledWith('1.2.3.4')
|
||||
expect(toast.success).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// 6. Console Enrollment fields (if enabled)
|
||||
it('handles console enrollment form', async () => {
|
||||
// const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByText('Console Enrollment'))
|
||||
|
||||
// Check inputs exist
|
||||
expect(screen.getByTestId('console-enrollment-token')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('console-agent-name')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// 7. Presets logic
|
||||
it('handles preset searching', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Mock presets with data
|
||||
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({
|
||||
presets: [
|
||||
{
|
||||
slug: 'ssh-bf',
|
||||
title: 'SSH Bruteforce',
|
||||
summary: 'Block SSH attacks',
|
||||
source: 'crowdsec',
|
||||
tags: ['linux'],
|
||||
requires_hub: false,
|
||||
available: true,
|
||||
cached: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(presetsApi.listCrowdsecPresets).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search presets...')
|
||||
expect(searchInput).toBeInTheDocument()
|
||||
|
||||
await user.type(searchInput, 'SSH')
|
||||
expect(searchInput).toHaveValue('SSH')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user