diff --git a/frontend/src/api/__tests__/security.test.ts b/frontend/src/api/__tests__/security.test.ts index 12434c58..f548eb21 100644 --- a/frontend/src/api/__tests__/security.test.ts +++ b/frontend/src/api/__tests__/security.test.ts @@ -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 }) diff --git a/frontend/src/api/security.ts b/frontend/src/api/security.ts index 9378a669..9f2945d7 100644 --- a/frontend/src/api/security.ts +++ b/frontend/src/api/security.ts @@ -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) => { + 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) => { + 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 } diff --git a/frontend/src/hooks/__tests__/useSecurity.test.tsx b/frontend/src/hooks/__tests__/useSecurity.test.tsx index 269aa480..297b9540 100644 --- a/frontend/src/hooks/__tests__/useSecurity.test.tsx +++ b/frontend/src/hooks/__tests__/useSecurity.test.tsx @@ -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 }) diff --git a/frontend/src/hooks/useSecurity.ts b/frontend/src/hooks/useSecurity.ts index 36756c09..462600ff 100644 --- a/frontend/src/hooks/useSecurity.ts +++ b/frontend/src/hooks/useSecurity.ts @@ -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) => 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) => disableCerberus(payload), onSuccess: () => { qc.invalidateQueries({ queryKey: ['securityConfig'] }) qc.invalidateQueries({ queryKey: ['securityStatus'] }) diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 8c26d964..e48130c5 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -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: () => { diff --git a/frontend/src/pages/__tests__/CrowdSecConfig.spec.tsx b/frontend/src/pages/__tests__/CrowdSecConfig.spec.tsx index 6e5f9b97..bfef1290 100644 --- a/frontend/src/pages/__tests__/CrowdSecConfig.spec.tsx +++ b/frontend/src/pages/__tests__/CrowdSecConfig.spec.tsx @@ -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() 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() 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() 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() diff --git a/frontend/src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx b/frontend/src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx index 1d8042d2..578e6252 100644 --- a/frontend/src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx +++ b/frontend/src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx @@ -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 () => { diff --git a/frontend/src/pages/__tests__/ProxyHosts-extra.test.tsx b/frontend/src/pages/__tests__/ProxyHosts-extra.test.tsx index 22aaec6c..9319199d 100644 --- a/frontend/src/pages/__tests__/ProxyHosts-extra.test.tsx +++ b/frontend/src/pages/__tests__/ProxyHosts-extra.test.tsx @@ -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() diff --git a/frontend/src/pages/__tests__/Security.audit.test.tsx b/frontend/src/pages/__tests__/Security.audit.test.tsx index eebb1a98..d816f75f 100644 --- a/frontend/src/pages/__tests__/Security.audit.test.tsx +++ b/frontend/src/pages/__tests__/Security.audit.test.tsx @@ -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(, { wrapper }) diff --git a/frontend/src/pages/__tests__/Security.spec.tsx b/frontend/src/pages/__tests__/Security.spec.tsx index 48dd3c1c..aba8b842 100644 --- a/frontend/src/pages/__tests__/Security.spec.tsx +++ b/frontend/src/pages/__tests__/Security.spec.tsx @@ -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() await waitFor(() => expect(screen.getByText('Security Dashboard')).toBeInTheDocument()) const exportBtn = screen.getByText('Export') diff --git a/frontend/src/pages/__tests__/Security.test.tsx b/frontend/src/pages/__tests__/Security.test.tsx index 2aaf391d..24c8b8b6 100644 --- a/frontend/src/pages/__tests__/Security.test.tsx +++ b/frontend/src/pages/__tests__/Security.test.tsx @@ -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) vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus) render(, { 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) vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus) render(, { 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) vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus) render(, { wrapper }) diff --git a/frontend/src/types/test-shims.d.ts b/frontend/src/types/test-shims.d.ts index 6a3899a2..8a2cc62c 100644 --- a/frontend/src/types/test-shims.d.ts +++ b/frontend/src/types/test-shims.d.ts @@ -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 } }