- Added references to existing test files in the UI/UX testing plan. - Updated CI failure remediation plan with improved file paths and clarity. - Expanded CrowdSec full implementation documentation with detailed configuration steps and scripts. - Improved CrowdSec testing plan with clearer objectives and expected results. - Updated current specification documentation with additional context on CVE remediation. - Enhanced docs-to-issues workflow documentation for better issue tracking. - Corrected numbering in UI/UX bugfixes specification for clarity. - Improved WAF testing plan with detailed curl commands and expected results. - Updated QA reports for CrowdSec implementation and UI/UX testing with detailed results and coverage metrics. - Fixed rate limit integration test summary with clear identification of issues and resolutions. - Enhanced rate limit test status report with detailed root causes and next steps for follow-up.
20 KiB
Cerberus Security Dashboard - UI/UX Test Plan
Issue: #319 - Cerberus Integration Testing (Final Phase) Created: December 12, 2025 Status: ✅ Complete
Executive Summary
This test plan covers comprehensive UI/UX testing for the Cerberus Security Dashboard, including all security feature cards, loading states, error handling, and mobile responsiveness. The plan leverages existing Vitest unit test patterns and recommends expanding Playwright E2E coverage.
1. Components Under Test
1.1 Primary Security Pages
| Component | File Path | Description |
|---|---|---|
| Security Dashboard | frontend/src/pages/Security.tsx | Main Cerberus dashboard with 4 security cards |
| WAF Configuration | frontend/src/pages/WafConfig.tsx | Coraza rule set management |
| Rate Limiting | frontend/src/pages/RateLimiting.tsx | Rate limit configuration |
| CrowdSec Config | frontend/src/pages/CrowdSecConfig.tsx | CrowdSec mode, presets, bans |
| Access Lists | frontend/src/pages/AccessLists.tsx | ACL management |
1.2 Shared Components
| Component | File Path | Description |
|---|---|---|
| ConfigReloadOverlay | frontend/src/components/LoadingStates.tsx | Cerberus-themed loading overlay |
| CerberusLoader | frontend/src/components/LoadingStates.tsx | Three-headed guardian animation |
| Card | frontend/src/components/ui/Card.tsx | Security card wrapper |
| Switch | frontend/src/components/ui/Switch.tsx | Toggle control |
| SecurityNotificationSettingsModal | frontend/src/components/SecurityNotificationSettingsModal.tsx | Notification settings |
1.3 React Query Hooks
| Hook | File Path | Purpose |
|---|---|---|
| useSecurityStatus | frontend/src/hooks/useSecurity.ts | Fetch security status |
| useSecurityConfig | frontend/src/hooks/useSecurity.ts | Fetch security config |
| useUpdateSecurityConfig | frontend/src/hooks/useSecurity.ts | Update config mutation |
| useRuleSets | frontend/src/hooks/useSecurity.ts | WAF rule sets |
| useEnableCerberus | frontend/src/hooks/useSecurity.ts | Enable Cerberus mutation |
| useDisableCerberus | frontend/src/hooks/useSecurity.ts | Disable Cerberus mutation |
1.4 API Layer
| API Module | File Path |
|---|---|
| Security API | frontend/src/api/security.ts |
| CrowdSec API | frontend/src/api/crowdsec.ts |
| Settings API | frontend/src/api/settings.ts |
2. Test Cases by Requirement
2.1 Security Dashboard Cards - Correct Status Display
Test File: frontend/src/pages/__tests__/Security.dashboard.test.tsx (new)
Framework: Vitest + React Testing Library
Test Cases
| ID | Test Case | Expected Behavior | Priority |
|---|---|---|---|
| SD-01 | Dashboard shows "Cerberus Disabled" banner when cerberus.enabled=false |
Banner renders with documentation link | High |
| SD-02 | CrowdSec card shows "Active" when crowdsec.enabled=true |
Green icon, "Active" text, running PID | High |
| SD-03 | CrowdSec card shows "Disabled" when crowdsec.enabled=false |
Gray icon, "Disabled" text | High |
| SD-04 | WAF (Coraza) card shows correct status | "Active"/"Disabled" based on waf.enabled |
High |
| SD-05 | Rate Limiting card shows correct status | Badge and text reflect rate_limit.enabled |
High |
| SD-06 | ACL card shows correct status | Status matches acl.enabled |
High |
| SD-07 | All cards display correct layer indicators | Layer 1-4 labels present in order | Medium |
| SD-08 | Threat protection summaries display correctly | Each card shows threat types | Medium |
| SD-09 | Cards maintain order after toggle | CrowdSec → ACL → WAF → Rate Limiting | Medium |
| SD-10 | Toggle switches are disabled when Cerberus is off | All service toggles disabled | High |
Assertions
// SD-01: Cerberus disabled banner
expect(screen.getByText(/Cerberus Disabled/i)).toBeInTheDocument()
expect(screen.getByRole('link', { name: /Documentation/i })).toHaveAttribute('href', expect.stringContaining('wikid82.github.io'))
// SD-02: CrowdSec active status
expect(screen.getByText('Active')).toBeInTheDocument()
expect(screen.getByTestId('toggle-crowdsec')).toBeChecked()
// SD-07: Layer indicators
expect(screen.getByText(/Layer 1: IP Reputation/i)).toBeInTheDocument()
expect(screen.getByText(/Layer 2: Access Control/i)).toBeInTheDocument()
expect(screen.getByText(/Layer 3: Request Inspection/i)).toBeInTheDocument()
expect(screen.getByText(/Layer 4: Volume Control/i)).toBeInTheDocument()
Mock Strategy
vi.mock('../../api/security')
vi.mock('../../api/crowdsec')
vi.mock('../../api/settings')
const mockSecurityStatus = {
cerberus: { enabled: true },
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: true },
waf: { mode: 'enabled', enabled: true },
rate_limit: { enabled: true },
acl: { enabled: true },
}
2.2 Loading States
Test File: frontend/src/pages/__tests__/Security.loading.test.tsx (new)
Framework: Vitest + React Testing Library
Test Cases
| ID | Test Case | Expected Behavior | Priority |
|---|---|---|---|
| LS-01 | Initial page load shows loading text | "Loading security status..." appears | High |
| LS-02 | Toggling service shows CerberusLoader overlay | ConfigReloadOverlay with type="cerberus" |
High |
| LS-03 | Starting CrowdSec shows "Summoning the guardian..." | Specific message for start operation | High |
| LS-04 | Stopping CrowdSec shows "Guardian rests..." | Specific message for stop operation | High |
| LS-05 | WAF config operations show overlay | Create/update/delete show loader | High |
| LS-06 | Rate Limiting save shows overlay | "Adjusting the gates..." message | High |
| LS-07 | CrowdSec preset pull shows overlay | "Fetching preset..." message | Medium |
| LS-08 | CrowdSec preset apply shows overlay | "Loading preset..." message | Medium |
| LS-09 | Overlay blocks interactions | Pointer events disabled during load | High |
| LS-10 | Overlay disappears on mutation success | No overlay after operation completes | High |
Assertions
// LS-02: Toggle shows overlay
const toggle = screen.getByTestId('toggle-waf')
await user.click(toggle)
await waitFor(() => {
expect(screen.getByText(/Three heads turn/i)).toBeInTheDocument()
expect(screen.getByText(/Cerberus configuration updating/i)).toBeInTheDocument()
})
// LS-03: CrowdSec start message
await waitFor(() => {
expect(screen.getByText(/Summoning the guardian/i)).toBeInTheDocument()
expect(screen.getByText(/CrowdSec is starting/i)).toBeInTheDocument()
})
// LS-09: Overlay blocks interactions
expect(screen.getByRole('status', { name: /Security Loading/i })).toBeInTheDocument()
Mock Strategy
// Use never-resolving promises to test loading states
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(() => new Promise(() => {}))
2.3 Error Handling
Test File: frontend/src/pages/__tests__/Security.errors.test.tsx (new)
Framework: Vitest + React Testing Library
Test Cases
| ID | Test Case | Expected Behavior | Priority |
|---|---|---|---|
| EH-01 | Failed security status fetch shows error | "Failed to load security status" message | High |
| EH-02 | Toggle mutation failure shows toast | toast.error() called with message |
High |
| EH-03 | CrowdSec start failure shows specific toast | "Failed to start CrowdSec: [message]" | High |
| EH-04 | CrowdSec stop failure shows specific toast | "Failed to stop CrowdSec: [message]" | High |
| EH-05 | WAF fetch failure shows error state | "Failed to load WAF configuration" | High |
| EH-06 | Rate Limiting update failure shows toast | Error toast with message | High |
| EH-07 | Network error shows generic message | Graceful degradation | Medium |
| EH-08 | Validation error shows inline message | Form validation errors | Medium |
| EH-09 | API returns 401 redirects to login | Auth check on protected routes | High |
| EH-10 | Optimistic update reverts on error | Previous state restored | High |
Assertions
// EH-01: Failed fetch
vi.mocked(securityApi.getSecurityStatus).mockRejectedValue(new Error('Network error'))
await renderSecurityPage()
await waitFor(() => {
expect(screen.getByText(/Failed to load security status/i)).toBeInTheDocument()
})
// EH-02: Toggle failure toast
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Permission denied'))
await user.click(toggle)
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
})
// EH-10: Optimistic update rollback
const previousState = queryClient.getQueryData(['security-status'])
// After error, state should be restored
await waitFor(() => {
expect(queryClient.getQueryData(['security-status'])).toEqual(previousState)
})
Mock Strategy
vi.mock('../../utils/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}))
// Reject API calls
vi.mocked(securityApi.getSecurityStatus).mockRejectedValue(new Error('API Error'))
2.4 Mobile Responsiveness
Test File: frontend/e2e/tests/security-mobile.spec.ts (new)
Framework: Playwright
Test Cases
| ID | Test Case | Viewport | Expected Behavior | Priority |
|---|---|---|---|---|
| MR-01 | Dashboard cards stack on mobile | 375×667 | Single column layout | High |
| MR-02 | Dashboard cards 2-col on tablet | 768×1024 | md:grid-cols-2 applies |
Medium |
| MR-03 | Dashboard cards 4-col on desktop | 1920×1080 | lg:grid-cols-4 applies |
Medium |
| MR-04 | Toggle switches remain accessible | 375×667 | Touch target ≥44px | High |
| MR-05 | Config buttons are tappable | 375×667 | Full-width on mobile | High |
| MR-06 | Modal/overlay renders correctly | 375×667 | Scrollable, no overflow | High |
| MR-07 | WAF table scrolls horizontally | 375×667 | Horizontal scroll enabled | Medium |
| MR-08 | Rate limiting form inputs fit | 375×667 | Inputs stack vertically | Medium |
| MR-09 | CrowdSec preset list is scrollable | 375×667 | Max-height with overflow | Medium |
| MR-10 | Navigation sidebar collapses | 375×667 | Mobile menu toggle works | High |
Playwright Test Example
import { test, expect } from '@playwright/test'
const base = process.env.CHARON_BASE_URL || 'http://localhost:8080'
test.describe('Security Dashboard Mobile', () => {
test.use({ viewport: { width: 375, height: 667 } })
test('cards stack vertically on mobile', async ({ page }) => {
await page.goto(`${base}/security`)
await page.waitForSelector('[data-testid="toggle-crowdsec"]')
// Check grid is single column
const cardsContainer = page.locator('.grid')
await expect(cardsContainer).toHaveCSS('grid-template-columns', /^[0-9.]+px$/)
})
test('toggle switches are accessible touch targets', async ({ page }) => {
await page.goto(`${base}/security`)
const toggle = page.getByTestId('toggle-crowdsec')
const box = await toggle.boundingBox()
expect(box?.height).toBeGreaterThanOrEqual(44)
})
test('config reload overlay is scrollable', async ({ page }) => {
await page.goto(`${base}/security`)
// Trigger loading state
await page.getByTestId('toggle-waf').click()
const overlay = page.locator('.fixed.inset-0')
await expect(overlay).toBeVisible()
// Verify no content overflow
const isScrollable = await overlay.evaluate((el) => el.scrollHeight > el.clientHeight)
// Overlay should fit or be scrollable
expect(true).toBe(true) // Overlay renders
})
})
test.describe('Security Dashboard Tablet', () => {
test.use({ viewport: { width: 768, height: 1024 } })
test('cards show 2 columns', async ({ page }) => {
await page.goto(`${base}/security`)
await page.waitForSelector('[data-testid="toggle-crowdsec"]')
const cardsContainer = page.locator('.grid.grid-cols-1.md\\:grid-cols-2')
await expect(cardsContainer).toBeVisible()
})
})
3. Test Implementation Strategy
3.1 Unit Tests (Vitest)
Location: frontend/src/pages/__tests__/
| Test File | Status | Coverage Target |
|---|---|---|
| Security.test.tsx | ✅ Exists | Expand for full card status |
| Security.audit.test.tsx | ✅ Exists | Edge cases covered |
| WafConfig.spec.tsx | ✅ Exists | Loading/error states |
| RateLimiting.spec.tsx | ✅ Exists | Toggle and save |
| CrowdSecConfig.test.tsx | ✅ Exists | Mode toggle, presets |
| Security.dashboard.test.tsx | ❌ Create | Card status verification |
| Security.loading.test.tsx | ❌ Create | Loading overlay tests |
| Security.errors.test.tsx | ❌ Create | Error handling tests |
3.2 E2E Tests (Playwright)
Location: frontend/e2e/tests/
| Test File | Status | Coverage Target |
|---|---|---|
| waf.spec.ts | ✅ Exists | WAF blocking |
| security-mobile.spec.ts | ❌ Create | Mobile responsive |
| security-e2e.spec.ts | ❌ Create | Full user flow |
3.3 Mocking Strategies
API Mocking (Vitest)
// Complete mock setup for security API
vi.mock('../../api/security', () => ({
getSecurityStatus: vi.fn(),
getSecurityConfig: vi.fn(),
updateSecurityConfig: vi.fn(),
enableCerberus: vi.fn(),
disableCerberus: vi.fn(),
getRuleSets: vi.fn(),
upsertRuleSet: vi.fn(),
deleteRuleSet: vi.fn(),
}))
vi.mock('../../api/crowdsec', () => ({
startCrowdsec: vi.fn(),
stopCrowdsec: vi.fn(),
statusCrowdsec: vi.fn(),
listCrowdsecDecisions: vi.fn(),
banIP: vi.fn(),
unbanIP: vi.fn(),
}))
vi.mock('../../api/settings', () => ({
updateSetting: vi.fn(),
}))
Toast Mocking
vi.mock('../../utils/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
loading: vi.fn(),
},
}))
React Query Test Wrapper
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 0 },
mutations: { retry: false },
},
})
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={createTestQueryClient()}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
)
4. Test Data Fixtures
4.1 Security Status Fixtures
// All features enabled
export const mockSecurityStatusAllEnabled = {
cerberus: { enabled: true },
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: true },
waf: { mode: 'enabled', enabled: true },
rate_limit: { enabled: true },
acl: { enabled: true },
}
// Cerberus disabled (all features should be disabled)
export const mockSecurityStatusCerberusDisabled = {
cerberus: { enabled: false },
crowdsec: { mode: 'disabled', api_url: '', enabled: false },
waf: { mode: 'disabled', enabled: false },
rate_limit: { enabled: false },
acl: { enabled: false },
}
// Mixed states
export const mockSecurityStatusMixed = {
cerberus: { enabled: true },
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: true },
waf: { mode: 'disabled', enabled: false },
rate_limit: { enabled: true },
acl: { enabled: false },
}
4.2 WAF Rule Set Fixtures
export const mockRuleSets = {
rulesets: [
{
id: 1,
uuid: 'uuid-1',
name: 'OWASP CRS',
source_url: 'https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.5.tar.gz',
mode: 'blocking',
last_updated: '2025-12-04T10:00:00Z',
content: '',
},
{
id: 2,
uuid: 'uuid-2',
name: 'Custom SQLi Rules',
source_url: '',
mode: 'detection',
last_updated: '2025-12-10T15:30:00Z',
content: 'SecRule ARGS "@detectSQLi" "id:1001,phase:1,deny"',
},
],
}
5. Test Execution Plan
5.1 Pre-Merge Checklist
- All existing tests pass (
npm run test) - New unit tests added for uncovered scenarios
- E2E mobile tests added
- Coverage meets threshold (80% lines)
- No console errors in tests
5.2 CI Integration
# Add to existing test workflow
- name: Run Security UI Tests
run: |
cd frontend
npm run test -- --coverage --reporter=json --outputFile=coverage/security-ui.json
- name: Run E2E Mobile Tests
run: |
cd frontend
npx playwright test e2e/tests/security-mobile.spec.ts
5.3 Manual QA Verification
| Scenario | Browser | Viewport | Verified |
|---|---|---|---|
| Dashboard loads | Chrome | Desktop | [ ] |
| Dashboard loads | Safari | Mobile | [ ] |
| Toggle CrowdSec | Chrome | Desktop | [ ] |
| Toggle WAF | Firefox | Desktop | [ ] |
| Error toast appears | Chrome | Desktop | [ ] |
| Mobile navigation | Safari | iPhone 14 | [ ] |
| Tablet layout | Chrome | iPad | [ ] |
6. Coverage Gaps & Recommendations
6.1 Current Coverage
- ✅ Security.tsx: Good coverage (toggle, loading, errors)
- ✅ WafConfig.tsx: Full CRUD coverage
- ✅ RateLimiting.tsx: Toggle and config save
- ✅ CrowdSecConfig.tsx: Mode toggle, presets
- ⚠️ Mobile responsiveness: No E2E tests
- ⚠️ ConfigReloadOverlay: Unit tests missing
6.2 Recommended New Tests
- Create
Security.dashboard.test.tsx: Focus on card status display - Create
Security.loading.test.tsx: Comprehensive loading overlay tests - Create
security-mobile.spec.ts: Playwright mobile viewport tests - Add LoadingStates.test.tsx: Unit test ConfigReloadOverlay component
6.3 Test Data-Testid Inventory
Existing test IDs for automation:
toggle-crowdsec
toggle-waf
toggle-acl
toggle-rate-limit
waf-loading
waf-error
waf-empty-state
rulesets-table
create-ruleset-btn
ruleset-name-input
ruleset-content-input
rate-limit-toggle
rate-limit-rps
rate-limit-burst
rate-limit-window
save-rate-limit-btn
crowdsec-mode-toggle
import-btn
apply-preset-btn
7. Appendix
A. Existing Test Patterns (Reference)
See existing test files for patterns:
B. Component Hierarchy
Security.tsx
├── ConfigReloadOverlay (when isApplyingConfig)
├── Header Banner (when Cerberus disabled)
├── Admin Whitelist Input
├── Outlet (nested routes)
└── Grid (4 columns)
├── CrowdSec Card
│ ├── Switch (toggle-crowdsec)
│ └── Config Button
├── ACL Card
│ ├── Switch (toggle-acl)
│ └── Manage Lists Button
├── Coraza Card
│ ├── Switch (toggle-waf)
│ └── Configure Button
└── Rate Limiting Card
├── Switch (toggle-rate-limit)
└── Configure Limits Button
C. API Response Types
interface SecurityStatus {
cerberus?: { enabled: boolean }
crowdsec: { mode: 'disabled' | 'local'; api_url: string; enabled: boolean }
waf: { mode: 'disabled' | 'enabled'; enabled: boolean }
rate_limit: { mode?: 'disabled' | 'enabled'; enabled: boolean }
acl: { enabled: boolean }
}
Author: Copilot Planning Agent Review Required: QA Team Implementation Owner: Frontend Team