Files
Charon/frontend/src/pages/__tests__/Security.spec.tsx
T
GitHub Actions 8f2f18edf7 feat: implement modern UI/UX design system (#409)
- Add comprehensive design token system (colors, typography, spacing)
- Create 12 new UI components with Radix UI primitives
- Add layout components (PageShell, StatsCard, EmptyState, DataTable)
- Polish all pages with new component library
- Improve accessibility with WCAG 2.1 compliance
- Add dark mode support with semantic color tokens
- Update 947 tests to match new UI patterns

Closes #409
2025-12-16 21:21:39 +00:00

208 lines
8.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import Security from '../Security'
import * as api from '../../api/security'
import type { SecurityStatus, RuleSetsResponse } from '../../api/security'
import * as settingsApi from '../../api/settings'
import * as crowdsecApi from '../../api/crowdsec'
import { createTestQueryClient } from '../../test/createTestQueryClient'
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
return { ...actual, useNavigate: () => mockNavigate }
})
vi.mock('../../api/security')
vi.mock('../../api/settings')
vi.mock('../../api/crowdsec')
const defaultFeatureFlags = {
'feature.cerberus.enabled': true,
'feature.uptime.enabled': true,
}
const baseStatus: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
const createQueryClient = (initialData = []) => createTestQueryClient([
{ key: ['securityConfig'], data: mockSecurityConfig },
{ key: ['securityRulesets'], data: mockRuleSets },
{ key: ['feature-flags'], data: defaultFeatureFlags },
...initialData,
])
const renderWithProviders = (ui: React.ReactNode, initialData = []) => {
const qc = createQueryClient(initialData)
return render(
<QueryClientProvider client={qc}>
<BrowserRouter>
{ui}
</BrowserRouter>
</QueryClientProvider>
)
}
const mockSecurityConfig = {
config: {
name: 'default',
waf_mode: 'block',
waf_rules_source: '',
admin_whitelist: '',
},
}
const mockRuleSets: RuleSetsResponse = {
rulesets: [
{ id: 1, uuid: 'uuid-1', name: 'OWASP CRS', source_url: '', mode: 'blocking', last_updated: '', content: '' },
{ id: 2, uuid: 'uuid-2', name: 'Custom Rules', source_url: '', mode: 'detection', last_updated: '', content: '' },
],
}
// Types already imported at top-level; avoid duplicate declarations
describe('Security page', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.mocked(api.getSecurityStatus).mockResolvedValue(baseStatus as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
vi.mocked(api.updateSecurityConfig).mockResolvedValue({})
})
it('shows banner when all services are disabled and links to docs', async () => {
const status: SecurityStatus = {
cerberus: { enabled: false },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValueOnce(status as SecurityStatus)
vi.mocked(api.getSecurityStatus).mockResolvedValueOnce({
...status,
crowdsec: { ...status.crowdsec, enabled: true }
} as SecurityStatus)
renderWithProviders(<Security />)
expect(await screen.findByText('Security Features Unavailable')).toBeInTheDocument()
const docBtns = screen.getAllByText('Learn More')
expect(docBtns.length).toBeGreaterThan(0)
})
it('renders per-service toggles and calls updateSetting on change', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Cerberus Dashboard')).toBeInTheDocument())
const crowdsecToggle = screen.getByTestId('toggle-crowdsec') as HTMLInputElement
expect(crowdsecToggle.disabled).toBe(false)
// Ensure enable-all controls were removed
expect(screen.queryByTestId('enable-all-btn')).toBeNull()
})
it('calls updateSetting when toggling ACL', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
const updateSpy = vi.mocked(settingsApi.updateSetting)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Cerberus Dashboard')).toBeInTheDocument())
const aclToggle = screen.getByTestId('toggle-acl')
await userEvent.click(aclToggle)
await waitFor(() => expect(updateSpy).toHaveBeenCalledWith('security.acl.enabled', 'true', 'security', 'bool'))
})
// Export button is in CrowdSecConfig component, not Security page
it('calls start/stop endpoints for CrowdSec via toggle', async () => {
const user = userEvent.setup()
const baseStatus: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(baseStatus as SecurityStatus)
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
vi.mocked(crowdsecApi.startCrowdsec).mockResolvedValue({ status: 'started', pid: 123, lapi_ready: true })
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Cerberus Dashboard')).toBeInTheDocument())
const toggle = screen.getByTestId('toggle-crowdsec')
await user.click(toggle)
await waitFor(() => expect(crowdsecApi.startCrowdsec).toHaveBeenCalled())
cleanup()
const enabledStatus: SecurityStatus = { ...baseStatus, crowdsec: { enabled: true, mode: 'local' as const, api_url: '' } }
vi.mocked(api.getSecurityStatus).mockResolvedValue(enabledStatus as SecurityStatus)
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 123, lapi_ready: true })
vi.mocked(crowdsecApi.stopCrowdsec).mockResolvedValue(undefined)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Cerberus Dashboard')).toBeInTheDocument())
const stopToggle = screen.getByTestId('toggle-crowdsec')
await user.click(stopToggle)
await waitFor(() => expect(crowdsecApi.stopCrowdsec).toHaveBeenCalled())
})
it('disables service toggles when cerberus is off', async () => {
const status: SecurityStatus = {
cerberus: { enabled: false },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Security Features Unavailable')).toBeInTheDocument())
const crowdsecToggle = screen.getByTestId('toggle-crowdsec')
expect(crowdsecToggle).toBeDisabled()
})
it('displays correct WAF threat protection summary when enabled', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue({
config: { ...mockSecurityConfig.config, waf_mode: 'monitor' },
})
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
renderWithProviders(<Security />)
// WAF now shows threat protection summary instead of mode text
await waitFor(() => expect(screen.getByText(/SQL injection, XSS, RCE/i)).toBeInTheDocument())
})
})