Add comprehensive tests for Security Dashboard functionality
- Implement tests for Security Dashboard card status verification (SD-01 to SD-10) to ensure correct display of security statuses and toggle functionality. - Create error handling tests (EH-01 to EH-10) to validate error messages on API failures, toast notifications on mutation errors, and optimistic update rollback. - Develop loading overlay tests (LS-01 to LS-10) to verify the appearance of loading indicators during operations and ensure interactions are blocked appropriately.
This commit is contained in:
353
frontend/src/pages/__tests__/Security.dashboard.test.tsx
Normal file
353
frontend/src/pages/__tests__/Security.dashboard.test.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* 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<typeof import('../../hooks/useSecurity')>()
|
||||
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 })
|
||||
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 }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
const renderSecurityPage = async () => {
|
||||
await act(async () => {
|
||||
render(<Security />, { wrapper })
|
||||
})
|
||||
}
|
||||
|
||||
describe('SD-01: Cerberus Disabled Banner', () => {
|
||||
it('should show "Cerberus Disabled" banner when cerberus.enabled=false', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCerberusDisabled)
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Cerberus Disabled/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show documentation link in disabled banner', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCerberusDisabled)
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => {
|
||||
// Multiple Documentation buttons exist (one in banner, one in header)
|
||||
const docButtons = screen.getAllByRole('button', { name: /Documentation/i })
|
||||
expect(docButtons.length).toBeGreaterThanOrEqual(1)
|
||||
// The primary one in the banner should have blue-600 (primary variant)
|
||||
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 "Active" when crowdsec.enabled=true', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => {
|
||||
const cards = screen.getAllByText('Active')
|
||||
expect(cards.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 })
|
||||
|
||||
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()
|
||||
expect(screen.getByText(/● Active/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
expect(screen.getByText(/○ Disabled/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
// Verify each layer indicator is present
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SD-08: Threat Protection Summaries', () => {
|
||||
it('should display threat protection descriptions for each card', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
|
||||
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
|
||||
const cards = screen.getAllByRole('heading', { level: 3 })
|
||||
const cardNames = cards.map((card: HTMLElement) => card.textContent)
|
||||
|
||||
// Verify pipeline order: CrowdSec (Layer 1) → ACL (Layer 2) → Coraza (Layer 3) → Rate Limiting (Layer 4) + Security Access Logs
|
||||
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'Coraza', '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(['CrowdSec', 'Access Control', 'Coraza', '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(/Cerberus Disabled/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()
|
||||
})
|
||||
})
|
||||
})
|
||||
362
frontend/src/pages/__tests__/Security.errors.test.tsx
Normal file
362
frontend/src/pages/__tests__/Security.errors.test.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Security Error Handling Tests
|
||||
* Test IDs: EH-01 through EH-10
|
||||
*
|
||||
* Tests error messages on API failures, toast notifications on mutation errors,
|
||||
* and optimistic update rollback.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } 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'
|
||||
import { toast } from '../../utils/toast'
|
||||
|
||||
vi.mock('../../api/security')
|
||||
vi.mock('../../api/crowdsec')
|
||||
vi.mock('../../api/settings')
|
||||
vi.mock('../../utils/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock('../../hooks/useSecurity', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks/useSecurity')>()
|
||||
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 mockSecurityStatusCrowdsecDisabled = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local' as const, api_url: 'http://localhost', enabled: false },
|
||||
waf: { mode: 'enabled' as const, enabled: true },
|
||||
rate_limit: { enabled: true },
|
||||
acl: { enabled: true },
|
||||
}
|
||||
|
||||
describe('Security Error Handling Tests', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
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')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
const renderSecurityPage = async () => {
|
||||
await act(async () => {
|
||||
render(<Security />, { wrapper })
|
||||
})
|
||||
}
|
||||
|
||||
describe('EH-01: Failed Security Status Fetch Shows Error', () => {
|
||||
it('should show "Failed to load security status" when API fails', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Failed to load security status/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-02: Toggle Mutation Failure Shows Toast', () => {
|
||||
it('should call toast.error() when toggle mutation fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Permission denied'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-03: CrowdSec Start Failure Shows Specific Toast', () => {
|
||||
it('should show "Failed to start CrowdSec: [message]" on start failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Service unavailable'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
await user.click(screen.getByTestId('toggle-crowdsec'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to start CrowdSec'))
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Service unavailable'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-04: CrowdSec Stop Failure Shows Specific Toast', () => {
|
||||
it('should show "Failed to stop CrowdSec: [message]" on stop failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Process locked'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
await user.click(screen.getByTestId('toggle-crowdsec'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to stop CrowdSec'))
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Process locked'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-05: WAF Toggle Failure Shows Error', () => {
|
||||
it('should show error toast when WAF toggle fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('WAF configuration error'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-06: Rate Limiting Update Failure Shows Toast', () => {
|
||||
it('should show error toast when rate limiting toggle fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Rate limit config error'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-rate-limit'))
|
||||
await user.click(screen.getByTestId('toggle-rate-limit'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-07: Network Error Shows Generic Message', () => {
|
||||
it('should handle network errors gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Network request failed'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-acl'))
|
||||
await user.click(screen.getByTestId('toggle-acl'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Network request failed'))
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle non-Error objects gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue('Unknown error string')
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-acl'))
|
||||
await user.click(screen.getByTestId('toggle-acl'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-08: ACL Toggle Failure Shows Error', () => {
|
||||
it('should show error when ACL toggle fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('ACL update failed'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-acl'))
|
||||
await user.click(screen.getByTestId('toggle-acl'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('ACL update failed'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-09: Multiple Consecutive Failures Show Multiple Toasts', () => {
|
||||
it('should show separate toast for each failed operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Server error'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
// First failure
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Second failure
|
||||
await user.click(screen.getByTestId('toggle-acl'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EH-10: Optimistic Update Reverts on Error', () => {
|
||||
it('should revert toggle state when mutation fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Update failed'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
|
||||
// WAF is initially enabled
|
||||
const toggle = screen.getByTestId('toggle-waf')
|
||||
expect(toggle).toBeChecked()
|
||||
|
||||
// Click to disable - optimistic update will uncheck it
|
||||
await user.click(toggle)
|
||||
|
||||
// Wait for error and rollback
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// After rollback, the toggle should be back to checked (enabled)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toggle-waf')).toBeChecked()
|
||||
})
|
||||
})
|
||||
|
||||
it('should revert CrowdSec state on start failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Start failed'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
|
||||
// CrowdSec is initially disabled
|
||||
const toggle = screen.getByTestId('toggle-crowdsec')
|
||||
expect(toggle).not.toBeChecked()
|
||||
|
||||
// Click to enable
|
||||
await user.click(toggle)
|
||||
|
||||
// Wait for error
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to start CrowdSec'))
|
||||
})
|
||||
|
||||
// After rollback, toggle should be back to unchecked (disabled)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toggle-crowdsec')).not.toBeChecked()
|
||||
})
|
||||
})
|
||||
|
||||
it('should revert CrowdSec state on stop failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Stop failed'))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
|
||||
// CrowdSec is initially enabled
|
||||
const toggle = screen.getByTestId('toggle-crowdsec')
|
||||
expect(toggle).toBeChecked()
|
||||
|
||||
// Click to disable
|
||||
await user.click(toggle)
|
||||
|
||||
// Wait for error
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to stop CrowdSec'))
|
||||
})
|
||||
|
||||
// After rollback, toggle should be back to checked (enabled)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toggle-crowdsec')).toBeChecked()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
302
frontend/src/pages/__tests__/Security.loading.test.tsx
Normal file
302
frontend/src/pages/__tests__/Security.loading.test.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Security Loading Overlay Tests
|
||||
* Test IDs: LS-01 through LS-10
|
||||
*
|
||||
* Tests ConfigReloadOverlay appears during operations, specific loading messages,
|
||||
* and overlay blocks interactions.
|
||||
*/
|
||||
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<typeof import('../../hooks/useSecurity')>()
|
||||
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 mockSecurityStatusCrowdsecDisabled = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local' as const, api_url: 'http://localhost', enabled: false },
|
||||
waf: { mode: 'enabled' as const, enabled: true },
|
||||
rate_limit: { enabled: true },
|
||||
acl: { enabled: true },
|
||||
}
|
||||
|
||||
describe('Security Loading Overlay Tests', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
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 }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
const renderSecurityPage = async () => {
|
||||
await act(async () => {
|
||||
render(<Security />, { wrapper })
|
||||
})
|
||||
}
|
||||
|
||||
describe('LS-01: Initial Page Load Shows Loading Text', () => {
|
||||
it('should show "Loading security status..." during initial load', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
expect(screen.getByText(/Loading security status/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-02: Toggling Service Shows CerberusLoader Overlay', () => {
|
||||
it('should show ConfigReloadOverlay with type="cerberus" when toggling', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
// Never-resolving promise to keep loading state
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-03: Starting CrowdSec Shows "Summoning the guardian..."', () => {
|
||||
it('should show specific message for CrowdSec start operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
// Never-resolving promise to keep loading state
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
const toggle = screen.getByTestId('toggle-crowdsec')
|
||||
await user.click(toggle)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Summoning the guardian/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/CrowdSec is starting/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-04: Stopping CrowdSec Shows "Guardian rests..."', () => {
|
||||
it('should show specific message for CrowdSec stop operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
// Never-resolving promise to keep loading state
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
const toggle = screen.getByTestId('toggle-crowdsec')
|
||||
await user.click(toggle)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Guardian rests/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/CrowdSec is stopping/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-05: WAF Config Operations Show Overlay', () => {
|
||||
it('should show overlay when toggling WAF', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Three heads turn/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-06: Rate Limiting Toggle Shows Overlay', () => {
|
||||
it('should show overlay when toggling rate limiting', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-rate-limit'))
|
||||
await user.click(screen.getByTestId('toggle-rate-limit'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Three heads turn/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Cerberus configuration updating/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-07: ACL Toggle Shows Overlay', () => {
|
||||
it('should show overlay when toggling ACL', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-acl'))
|
||||
await user.click(screen.getByTestId('toggle-acl'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Three heads turn/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-08: Overlay Contains CerberusLoader Component', () => {
|
||||
it('should render CerberusLoader animation within overlay', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
// The CerberusLoader has role="status" with aria-label="Security Loading"
|
||||
expect(screen.getByRole('status', { name: /Security Loading/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-09: Overlay Blocks Interactions', () => {
|
||||
it('should show overlay during toggle operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify the fixed overlay is present (it has class "fixed inset-0")
|
||||
const overlay = document.querySelector('.fixed.inset-0')
|
||||
expect(overlay).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should have z-50 overlay that covers content', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
await waitFor(() => {
|
||||
const overlay = document.querySelector('.z-50')
|
||||
expect(overlay).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LS-10: Overlay Disappears on Mutation Success', () => {
|
||||
it('should remove overlay after toggle completes successfully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
|
||||
// First call - resolves quickly to simulate successful toggle
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
|
||||
// The overlay might flash briefly and disappear, so we verify no overlay after completion
|
||||
await user.click(screen.getByTestId('toggle-waf'))
|
||||
|
||||
// Wait for mutation to complete and overlay to disappear
|
||||
await waitFor(() => {
|
||||
const overlay = document.querySelector('.fixed.inset-0.bg-slate-900\\/70')
|
||||
// After successful mutation, overlay should be gone
|
||||
expect(overlay).not.toBeInTheDocument()
|
||||
}, { timeout: 3000 })
|
||||
})
|
||||
|
||||
it('should not show overlay when mutation completes instantly', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Cerberus Dashboard/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// After successful load, no overlay should be present
|
||||
const overlay = document.querySelector('.fixed.inset-0.bg-slate-900\\/70')
|
||||
expect(overlay).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user