/** * Security Dashboard Card Status Verification Tests * Test IDs: SD-01 through SD-10 * * Tests all 4 security cards display correct status, Cerberus disabled banner, * and toggle switches disabled when Cerberus is off. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { BrowserRouter } from 'react-router-dom' import Security from '../Security' import * as securityApi from '../../api/security' import * as crowdsecApi from '../../api/crowdsec' import * as settingsApi from '../../api/settings' vi.mock('../../api/security') vi.mock('../../api/crowdsec') vi.mock('../../api/settings') vi.mock('../../hooks/useSecurity', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useSecurityConfig: vi.fn(() => ({ data: { config: { admin_whitelist: '10.0.0.0/8' } } })), useUpdateSecurityConfig: vi.fn(() => ({ mutate: vi.fn(), isPending: false })), useGenerateBreakGlassToken: vi.fn(() => ({ mutate: vi.fn(), isPending: false })), useRuleSets: vi.fn(() => ({ data: { rulesets: [ { id: 1, uuid: 'abc', name: 'OWASP CRS', source_url: 'https://example.com', mode: 'blocking', last_updated: '2025-12-04', content: 'rules' } ] } })), } }) // Test Data Fixtures const mockSecurityStatusAllEnabled = { cerberus: { enabled: true }, crowdsec: { mode: 'local' as const, api_url: 'http://localhost', enabled: true }, waf: { mode: 'enabled' as const, enabled: true }, rate_limit: { enabled: true }, acl: { enabled: true }, } const mockSecurityStatusCerberusDisabled = { cerberus: { enabled: false }, crowdsec: { mode: 'disabled' as const, api_url: '', enabled: false }, waf: { mode: 'disabled' as const, enabled: false }, rate_limit: { enabled: false }, acl: { enabled: false }, } const mockSecurityStatusMixed = { cerberus: { enabled: true }, crowdsec: { mode: 'local' as const, api_url: 'http://localhost', enabled: true }, waf: { mode: 'disabled' as const, enabled: false }, rate_limit: { enabled: true }, acl: { enabled: false }, } describe('Security Dashboard - Card Status Tests', () => { let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }) vi.clearAllMocks() vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false }) vi.mocked(settingsApi.updateSetting).mockResolvedValue() vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob()) vi.spyOn(window, 'open').mockImplementation(() => null) vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) vi.spyOn(window, 'prompt').mockReturnValue('crowdsec-export.tar.gz') }) const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) const renderSecurityPage = async () => { await act(async () => { render(, { wrapper }) }) } describe('SD-01: Cerberus Disabled Banner', () => { it('should show "Security Features Unavailable" banner when cerberus.enabled=false', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCerberusDisabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Security Features Unavailable/i)).toBeInTheDocument() }) }) it('should show documentation link in disabled banner', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCerberusDisabled) await renderSecurityPage() await waitFor(() => { // Documentation link uses "Learn More" text in current UI const docButtons = screen.getAllByRole('button', { name: /Learn More/i }) expect(docButtons.length).toBeGreaterThanOrEqual(1) expect(docButtons[0]).toBeInTheDocument() }) }) it('should not show banner when Cerberus is enabled', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) expect(screen.queryByText(/^Cerberus Disabled$/)).not.toBeInTheDocument() }) }) describe('SD-02: CrowdSec Card Active Status', () => { it('should show "Enabled" when crowdsec.enabled=true', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true }) await renderSecurityPage() await waitFor(() => { // Status badges now show 'Enabled' text const enabledBadges = screen.getAllByText('Enabled') expect(enabledBadges.length).toBeGreaterThan(0) }) const toggle = screen.getByTestId('toggle-crowdsec') expect(toggle).toBeChecked() }) it('should show running PID when CrowdSec is running', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true }) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Running \(pid 1234\)/i)).toBeInTheDocument() }) }) }) describe('SD-03: CrowdSec Card Disabled Status', () => { it('should show "Disabled" when crowdsec.enabled=false', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue({ ...mockSecurityStatusAllEnabled, crowdsec: { mode: 'disabled', api_url: '', enabled: false }, }) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) const toggle = screen.getByTestId('toggle-crowdsec') expect(toggle).not.toBeChecked() }) }) describe('SD-04: WAF (Coraza) Card Status', () => { it('should show "Active" when waf.enabled=true', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-waf')).toBeChecked() }) }) it('should show "Disabled" when waf.enabled=false', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusMixed) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-waf')).not.toBeChecked() }) }) }) describe('SD-05: Rate Limiting Card Status', () => { it('should show badge and text when rate_limit.enabled=true', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-rate-limit')).toBeChecked() const enabledBadges = screen.getAllByText('Enabled') expect(enabledBadges.length).toBeGreaterThan(0) }) }) it('should show "Disabled" badge when rate_limit.enabled=false', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue({ ...mockSecurityStatusAllEnabled, rate_limit: { enabled: false }, }) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-rate-limit')).not.toBeChecked() const disabledBadges = screen.getAllByText('Disabled') expect(disabledBadges.length).toBeGreaterThan(0) }) }) }) describe('SD-06: ACL Card Status', () => { it('should show "Active" when acl.enabled=true', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-acl')).toBeChecked() }) }) it('should show "Disabled" when acl.enabled=false', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusMixed) await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-acl')).not.toBeChecked() }) }) }) describe('SD-07: Layer Indicators', () => { it('should display all layer indicators in correct order', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) // Layer indicators are now Badges with just the layer number expect(screen.getByText('Layer 1')).toBeInTheDocument() expect(screen.getByText('Layer 2')).toBeInTheDocument() expect(screen.getByText('Layer 3')).toBeInTheDocument() expect(screen.getByText('Layer 4')).toBeInTheDocument() }) }) describe('SD-08: Threat Protection Summaries', () => { it('should display threat protection descriptions for each card', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) // CrowdSec must be running to show threat protection descriptions vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true }) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) // Verify threat protection descriptions expect(screen.getByText(/Known attackers, botnets/i)).toBeInTheDocument() expect(screen.getByText(/Unauthorized IPs, geo-based attacks/i)).toBeInTheDocument() expect(screen.getByText(/SQL injection, XSS, RCE/i)).toBeInTheDocument() expect(screen.getByText(/DDoS attacks, credential stuffing/i)).toBeInTheDocument() }) }) describe('SD-09: Card Order (Pipeline Sequence)', () => { it('should maintain card order: CrowdSec → ACL → WAF → Rate Limiting', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) // Get all card headings (includes Admin Whitelist when Cerberus is enabled) const cards = screen.getAllByRole('heading', { level: 3 }) const cardNames = cards.map((card: HTMLElement) => card.textContent) // Verify pipeline order with Admin Whitelist first (when Cerberus enabled) expect(cardNames).toEqual(['Admin Whitelist', 'CrowdSec', 'Access Control', 'Coraza WAF', 'Rate Limiting', 'Security Access Logs']) }) it('should maintain card order even after toggle', async () => { const user = userEvent.setup() vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) vi.mocked(settingsApi.updateSetting).mockResolvedValue() await renderSecurityPage() await waitFor(() => { expect(screen.getByTestId('toggle-waf')).toBeInTheDocument() }) // Toggle WAF off await user.click(screen.getByTestId('toggle-waf')) // Cards should still be in order const cards = screen.getAllByRole('heading', { level: 3 }) const cardNames = cards.map((card: HTMLElement) => card.textContent) expect(cardNames).toEqual(['Admin Whitelist', 'CrowdSec', 'Access Control', 'Coraza WAF', 'Rate Limiting', 'Security Access Logs']) }) }) describe('SD-10: Toggle Switches Disabled When Cerberus Off', () => { it('should disable all service toggles when Cerberus is disabled', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCerberusDisabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Security Features Unavailable/i)).toBeInTheDocument() }) // All toggles should be disabled expect(screen.getByTestId('toggle-crowdsec')).toBeDisabled() expect(screen.getByTestId('toggle-waf')).toBeDisabled() expect(screen.getByTestId('toggle-acl')).toBeDisabled() expect(screen.getByTestId('toggle-rate-limit')).toBeDisabled() }) it('should enable toggles when Cerberus is enabled', async () => { vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled) await renderSecurityPage() await waitFor(() => { expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument() }) // All toggles should be enabled expect(screen.getByTestId('toggle-crowdsec')).not.toBeDisabled() expect(screen.getByTestId('toggle-waf')).not.toBeDisabled() expect(screen.getByTestId('toggle-acl')).not.toBeDisabled() expect(screen.getByTestId('toggle-rate-limit')).not.toBeDisabled() }) }) })