Files
Charon/frontend/src/pages/__tests__/CrowdSecConfig.test.tsx
GitHub Actions 10582872f9 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.
2026-02-06 17:38:08 +00:00

249 lines
8.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import CrowdSecConfig from '../CrowdSecConfig'
import * as securityApi from '../../api/security'
import * as crowdsecApi from '../../api/crowdsec'
import * as backupsApi from '../../api/backups'
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/presets')
vi.mock('../../api/featureFlags')
vi.mock('../../utils/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
info: vi.fn(),
},
}))
describe('CrowdSecConfig', () => {
const createClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
const renderComponent = () => {
const queryClient = createClient()
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<CrowdSecConfig />
</MemoryRouter>
</QueryClientProvider>
)
}
beforeEach(() => {
vi.clearAllMocks()
// Default mocks
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue({
cerberus: { enabled: true },
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: true },
waf: { mode: 'enabled', enabled: true },
rate_limit: { enabled: true },
acl: { enabled: true },
})
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: [
{ 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(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({
status: 'pulled',
slug: 'bot-mitigation-essentials',
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()
})
// 1. Rendering basic elements
it('renders page configuration elements', async () => {
renderComponent()
await waitFor(() => {
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()
})
})
// 2. File Editor
it('allows reading and saving config files', async () => {
const user = userEvent.setup()
renderComponent()
await waitFor(() => screen.getByTestId('crowdsec-file-select'))
// 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')
})
})