feat: enhance type safety in security API and related tests

This commit is contained in:
GitHub Actions
2025-12-06 02:57:51 +00:00
parent 8e2ba14ae5
commit 8ef1e7cda0
12 changed files with 67 additions and 50 deletions
+18 -14
View File
@@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState, useEffect } from 'react'
import { useNavigate, Outlet } from 'react-router-dom'
import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink } from 'lucide-react'
import { getSecurityStatus } from '../api/security'
import { getSecurityStatus, type SecurityStatus } from '../api/security'
import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken, useRuleSets } from '../hooks/useSecurity'
import { exportCrowdsecConfig, startCrowdsec, stopCrowdsec, statusCrowdsec } from '../api/crowdsec'
import { updateSetting } from '../api/settings'
@@ -38,21 +38,23 @@ export default function Security() {
onMutate: async ({ key, enabled }: { key: string; enabled: boolean }) => {
await queryClient.cancelQueries({ queryKey: ['security-status'] })
const previous = queryClient.getQueryData(['security-status'])
queryClient.setQueryData(['security-status'], (old: any) => {
if (!old) return old
queryClient.setQueryData(['security-status'], (old: unknown) => {
if (!old || typeof old !== 'object') return old
const parts = key.split('.')
const section = parts[1]
const section = parts[1] as keyof SecurityStatus
const field = parts[2]
const copy = { ...old }
if (copy[section]) {
copy[section] = { ...copy[section], [field]: enabled }
const copy = { ...(old as SecurityStatus) }
if (copy[section] && typeof copy[section] === 'object') {
copy[section] = { ...copy[section], [field]: enabled } as never
}
return copy
})
return { previous }
},
onError: (_err, _vars, context: any) => {
if (context?.previous) queryClient.setQueryData(['security-status'], context.previous)
onError: (_err, _vars, context: unknown) => {
if (context && typeof context === 'object' && 'previous' in context) {
queryClient.setQueryData(['security-status'], context.previous)
}
const msg = _err instanceof Error ? _err.message : String(_err)
toast.error(`Failed to update setting: ${msg}`)
},
@@ -71,17 +73,19 @@ export default function Security() {
await queryClient.cancelQueries({ queryKey: ['security-status'] })
const previous = queryClient.getQueryData(['security-status'])
if (previous) {
queryClient.setQueryData(['security-status'], (old: any) => {
const copy = JSON.parse(JSON.stringify(old))
if (!copy.cerberus) copy.cerberus = {}
queryClient.setQueryData(['security-status'], (old: unknown) => {
const copy = JSON.parse(JSON.stringify(old)) as SecurityStatus
if (!copy.cerberus) copy.cerberus = { enabled: false }
copy.cerberus.enabled = enabled
return copy
})
}
return { previous }
},
onError: (_err, _vars, context: any) => {
if (context?.previous) queryClient.setQueryData(['security-status'], context.previous)
onError: (_err, _vars, context: unknown) => {
if (context && typeof context === 'object' && 'previous' in context) {
queryClient.setQueryData(['security-status'], context.previous)
}
},
// onSuccess: already set below
onSuccess: () => {
@@ -30,10 +30,10 @@ describe('CrowdSecConfig', () => {
beforeEach(() => vi.clearAllMocks())
it('exports config when clicking Export', async () => {
vi.mocked(api.getSecurityStatus).mockResolvedValue({ crowdsec: { enabled: true, mode: 'local', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } } as any)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] } as any)
vi.mocked(api.getSecurityStatus).mockResolvedValue({ crowdsec: { enabled: true, mode: 'local', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } })
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] })
const blob = new Blob(['dummy'])
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(blob as any)
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(blob)
renderWithProviders(<CrowdSecConfig />)
await waitFor(() => expect(screen.getByText('CrowdSec Configuration')).toBeInTheDocument())
const exportBtn = screen.getByText('Export')
@@ -42,10 +42,10 @@ describe('CrowdSecConfig', () => {
})
it('uploads a file and calls import on Import (backup before save)', async () => {
vi.mocked(api.getSecurityStatus).mockResolvedValue({ crowdsec: { enabled: true, mode: 'local', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } } as any)
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' } as any)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] } as any)
vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue({ status: 'imported' } as any)
vi.mocked(api.getSecurityStatus).mockResolvedValue({ crowdsec: { enabled: true, mode: 'local', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } })
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] })
vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue({ status: 'imported' })
renderWithProviders(<CrowdSecConfig />)
await waitFor(() => expect(screen.getByText('CrowdSec Configuration')).toBeInTheDocument())
const input = screen.getByTestId('import-file') as HTMLInputElement
@@ -58,12 +58,12 @@ describe('CrowdSecConfig', () => {
})
it('lists files, reads file content and can save edits (backup before save)', async () => {
const status = { crowdsec: { enabled: true, mode: 'local', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } } as any
const status = { crowdsec: { enabled: true, mode: 'local' as const, api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' as const }, rate_limit: { enabled: false }, acl: { enabled: false } }
vi.mocked(api.getSecurityStatus).mockResolvedValue(status)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['conf.d/a.conf', 'b.conf'] } as any)
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: 'rule1' } as any)
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' } as any)
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({ status: 'written' } as any)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['conf.d/a.conf', 'b.conf'] })
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: 'rule1' })
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({ status: 'written' })
renderWithProviders(<CrowdSecConfig />)
await waitFor(() => expect(screen.getByText('CrowdSec Configuration')).toBeInTheDocument())
@@ -86,9 +86,9 @@ describe('CrowdSecConfig', () => {
})
it('persists crowdsec.mode via settings when changed', async () => {
const status = { crowdsec: { enabled: true, mode: 'disabled', api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' }, rate_limit: { enabled: false }, acl: { enabled: false } } as any
const status = { crowdsec: { enabled: true, mode: 'disabled' as const, api_url: '' }, cerberus: { enabled: true }, waf: { enabled: false, mode: 'disabled' as const }, rate_limit: { enabled: false }, acl: { enabled: false } }
vi.mocked(api.getSecurityStatus).mockResolvedValue(status)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] } as any)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: [] })
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
renderWithProviders(<CrowdSecConfig />)
@@ -64,7 +64,7 @@ describe('ProxyHosts - Certificate Cleanup Prompts', () => {
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([])
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.db' } as any)
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.db' })
})
it('prompts to delete certificate when deleting proxy host with unique custom cert', async () => {
@@ -184,7 +184,7 @@ describe('ProxyHosts page extra tests', () => {
vi.doMock('../../hooks/useAccessLists', () => ({ useAccessLists: vi.fn(() => ({ data: [] })) }))
vi.doMock('../../api/settings', () => ({ getSettings: vi.fn(() => Promise.resolve({ 'ui.domain_link_behavior': 'new_window' })) }))
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null as any)
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const { default: ProxyHosts } = await import('../ProxyHosts')
renderWithProviders(<ProxyHosts />)
@@ -391,7 +391,7 @@ describe('Security Page - QA Security Audit', () => {
it('handles undefined crowdsec status gracefully', async () => {
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue(null as any)
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue(null as never)
render(<Security />, { wrapper })
@@ -119,7 +119,7 @@ describe('Security page', () => {
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
const blob = new Blob(['dummy'])
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(blob as any)
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(blob)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Security Dashboard')).toBeInTheDocument())
const exportBtn = screen.getByText('Export')
@@ -189,7 +189,7 @@ describe('Security', () => {
const user = userEvent.setup()
const mockMutate = vi.fn()
const { useUpdateSecurityConfig } = await import('../../hooks/useSecurity')
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as any)
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as unknown as ReturnType<typeof useUpdateSecurityConfig>)
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
render(<Security />, { wrapper })
@@ -239,7 +239,7 @@ describe('Security', () => {
it('should export CrowdSec config', async () => {
const user = userEvent.setup()
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue('config data' as any)
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob(['config data']))
window.URL.createObjectURL = vi.fn(() => 'blob:url')
window.URL.revokeObjectURL = vi.fn()
@@ -261,7 +261,7 @@ describe('Security', () => {
const user = userEvent.setup()
const { useUpdateSecurityConfig } = await import('../../hooks/useSecurity')
const mockMutate = vi.fn()
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as any)
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as unknown as ReturnType<typeof useUpdateSecurityConfig>)
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
render(<Security />, { wrapper })
@@ -277,7 +277,7 @@ describe('Security', () => {
const user = userEvent.setup()
const { useUpdateSecurityConfig } = await import('../../hooks/useSecurity')
const mockMutate = vi.fn()
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as any)
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as unknown as ReturnType<typeof useUpdateSecurityConfig>)
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
render(<Security />, { wrapper })