feat: enhance type safety in security API and related tests
This commit is contained in:
@@ -162,7 +162,7 @@ describe('security API', () => {
|
||||
|
||||
describe('createDecision', () => {
|
||||
it('should call POST /security/decisions with payload', async () => {
|
||||
const payload = { ip: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
|
||||
@@ -55,13 +55,13 @@ export const generateBreakGlassToken = async () => {
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const enableCerberus = async (payload?: any) => {
|
||||
const response = await client.post('/security/enable', payload || {} as unknown) // Specify a more accurate type
|
||||
export const enableCerberus = async (payload?: Record<string, unknown>) => {
|
||||
const response = await client.post('/security/enable', payload || {})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const disableCerberus = async (payload?: any) => {
|
||||
const response = await client.post('/security/disable', payload || {} as unknown) // Specify a more accurate type
|
||||
export const disableCerberus = async (payload?: Record<string, unknown>) => {
|
||||
const response = await client.post('/security/disable', payload || {})
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -70,7 +70,14 @@ export const getDecisions = async (limit = 50) => {
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const createDecision = async (payload: any) => {
|
||||
export interface CreateDecisionPayload {
|
||||
type: string
|
||||
value: string
|
||||
duration: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export const createDecision = async (payload: CreateDecisionPayload) => {
|
||||
const response = await client.post('/security/decisions', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('useSecurity hooks', () => {
|
||||
|
||||
describe('useCreateDecision', () => {
|
||||
it('should create decision and invalidate queries', async () => {
|
||||
const payload = { ip: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
vi.mocked(securityApi.createDecision).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useCreateDecision(), { wrapper })
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
upsertRuleSet,
|
||||
deleteRuleSet,
|
||||
type UpsertRuleSetPayload,
|
||||
type SecurityConfigPayload,
|
||||
type CreateDecisionPayload,
|
||||
} from '../api/security'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
@@ -26,8 +28,8 @@ export function useSecurityConfig() {
|
||||
export function useUpdateSecurityConfig() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) => updateSecurityConfig(payload),
|
||||
onSuccess: () => { // Specify a more accurate type for payload
|
||||
mutationFn: (payload: SecurityConfigPayload) => updateSecurityConfig(payload),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['securityConfig'] })
|
||||
qc.invalidateQueries({ queryKey: ['securityStatus'] })
|
||||
toast.success('Security configuration updated')
|
||||
@@ -49,7 +51,7 @@ export function useDecisions(limit = 50) {
|
||||
export function useCreateDecision() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) => createDecision(payload),
|
||||
mutationFn: (payload: CreateDecisionPayload) => createDecision(payload),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['securityDecisions'] }),
|
||||
})
|
||||
}
|
||||
@@ -89,7 +91,7 @@ export function useDeleteRuleSet() {
|
||||
export function useEnableCerberus() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload?: any) => enableCerberus(payload),
|
||||
mutationFn: (payload?: Record<string, unknown>) => enableCerberus(payload),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['securityConfig'] })
|
||||
qc.invalidateQueries({ queryKey: ['securityStatus'] })
|
||||
@@ -104,7 +106,7 @@ export function useEnableCerberus() {
|
||||
export function useDisableCerberus() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload?: any) => disableCerberus(payload),
|
||||
mutationFn: (payload?: Record<string, unknown>) => disableCerberus(payload),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['securityConfig'] })
|
||||
qc.invalidateQueries({ queryKey: ['securityStatus'] })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
8
frontend/src/types/test-shims.d.ts
vendored
8
frontend/src/types/test-shims.d.ts
vendored
@@ -1,5 +1,9 @@
|
||||
// Test-only type shims to satisfy strict type-checking in CI
|
||||
// Properly type the default export from @testing-library/user-event
|
||||
declare module '@testing-library/user-event' {
|
||||
const userEvent: any;
|
||||
export default userEvent;
|
||||
import type { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'
|
||||
const userEvent: UserEvent
|
||||
export default userEvent
|
||||
export { userEvent }
|
||||
export type { UserEvent }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user