chore: clean git cache
This commit is contained in:
179
frontend/src/hooks/__tests__/useAccessLists.test.tsx
Normal file
179
frontend/src/hooks/__tests__/useAccessLists.test.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useAccessLists, useAccessList, useCreateAccessList, useUpdateAccessList, useDeleteAccessList, useTestIP } from '../useAccessLists';
|
||||
import { accessListsApi } from '../../api/accessLists';
|
||||
import type { AccessList } from '../../api/accessLists';
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('../../api/accessLists');
|
||||
|
||||
// Create a wrapper with QueryClient
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useAccessLists hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useAccessLists', () => {
|
||||
it('should fetch all access lists', async () => {
|
||||
const mockLists: AccessList[] = [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'test-uuid',
|
||||
name: 'Test ACL',
|
||||
description: 'Test',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(accessListsApi.list).mockResolvedValueOnce(mockLists);
|
||||
|
||||
const { result } = renderHook(() => useAccessLists(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual(mockLists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAccessList', () => {
|
||||
it('should fetch a single access list', async () => {
|
||||
const mockList: AccessList = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid',
|
||||
name: 'Test ACL',
|
||||
description: 'Test',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
};
|
||||
|
||||
vi.mocked(accessListsApi.get).mockResolvedValueOnce(mockList);
|
||||
|
||||
const { result } = renderHook(() => useAccessList(1), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual(mockList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateAccessList', () => {
|
||||
it('should create a new access list', async () => {
|
||||
const newList = {
|
||||
name: 'New ACL',
|
||||
description: 'New',
|
||||
type: 'whitelist' as const,
|
||||
ip_rules: '[]',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const mockResponse: AccessList = {
|
||||
id: 1,
|
||||
uuid: 'new-uuid',
|
||||
...newList,
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
};
|
||||
|
||||
vi.mocked(accessListsApi.create).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useCreateAccessList(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(newList);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateAccessList', () => {
|
||||
it('should update an access list', async () => {
|
||||
const updates = { name: 'Updated ACL' };
|
||||
const mockResponse: AccessList = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid',
|
||||
name: 'Updated ACL',
|
||||
description: 'Test',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
};
|
||||
|
||||
vi.mocked(accessListsApi.update).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useUpdateAccessList(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ id: 1, data: updates });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteAccessList', () => {
|
||||
it('should delete an access list', async () => {
|
||||
vi.mocked(accessListsApi.delete).mockResolvedValueOnce(undefined);
|
||||
|
||||
const { result } = renderHook(() => useDeleteAccessList(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(1);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(accessListsApi.delete).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useTestIP', () => {
|
||||
it('should test an IP against an access list', async () => {
|
||||
const mockResponse = { allowed: true, reason: 'Test' };
|
||||
|
||||
vi.mocked(accessListsApi.testIP).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useTestIP(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ id: 1, ipAddress: '192.168.1.1' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
frontend/src/hooks/__tests__/useAuth.test.tsx
Normal file
26
frontend/src/hooks/__tests__/useAuth.test.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { AuthContext } from '../../context/AuthContextValue'
|
||||
import { useAuth } from '../useAuth'
|
||||
|
||||
const TestComponent = () => {
|
||||
const auth = useAuth()
|
||||
return <div>{auth.isAuthenticated ? 'auth' : 'no-auth'}</div>
|
||||
}
|
||||
|
||||
describe('useAuth hook', () => {
|
||||
it('throws if used outside provider', () => {
|
||||
const renderOutside = () => render(<TestComponent />)
|
||||
expect(renderOutside).toThrowError('useAuth must be used within an AuthProvider')
|
||||
})
|
||||
|
||||
it('returns context inside provider', () => {
|
||||
const fakeCtx = { user: { user_id: 1, role: 'admin', name: 'Test', email: 't@example.com' }, login: async () => {}, logout: () => {}, changePassword: async () => {}, isAuthenticated: true, isLoading: false }
|
||||
render(
|
||||
<AuthContext.Provider value={fakeCtx}>
|
||||
<TestComponent />
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
expect(screen.getByText('auth')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
529
frontend/src/hooks/__tests__/useConsoleEnrollment.test.tsx
Normal file
529
frontend/src/hooks/__tests__/useConsoleEnrollment.test.tsx
Normal file
@@ -0,0 +1,529 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useConsoleStatus, useEnrollConsole } from '../useConsoleEnrollment'
|
||||
import * as consoleEnrollmentApi from '../../api/consoleEnrollment'
|
||||
import type { ConsoleEnrollmentStatus, ConsoleEnrollPayload } from '../../api/consoleEnrollment'
|
||||
|
||||
vi.mock('../../api/consoleEnrollment')
|
||||
|
||||
describe('useConsoleEnrollment hooks', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
|
||||
describe('useConsoleStatus', () => {
|
||||
it('should fetch console enrollment status when enabled', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-1',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-14T10:00:00Z',
|
||||
last_heartbeat_at: '2025-12-15T09:00:00Z',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(mockStatus)
|
||||
expect(consoleEnrollmentApi.getConsoleStatus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should NOT fetch when enabled=false', async () => {
|
||||
const { result } = renderHook(() => useConsoleStatus(false), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||
expect(consoleEnrollmentApi.getConsoleStatus).not.toHaveBeenCalled()
|
||||
expect(result.current.data).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should use correct query key for invalidation', () => {
|
||||
renderHook(() => useConsoleStatus(), { wrapper })
|
||||
const queries = queryClient.getQueryCache().getAll()
|
||||
const consoleQuery = queries.find((q) =>
|
||||
JSON.stringify(q.queryKey) === JSON.stringify(['crowdsec-console-status'])
|
||||
)
|
||||
expect(consoleQuery).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle pending enrollment status', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'pending',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
last_attempt_at: '2025-12-15T09:00:00Z',
|
||||
correlation_id: 'req-abc123',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('pending')
|
||||
expect(result.current.data?.correlation_id).toBe('req-abc123')
|
||||
})
|
||||
|
||||
it('should handle failed enrollment status with error details', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'failed',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: false,
|
||||
last_error: 'Invalid enrollment key',
|
||||
last_attempt_at: '2025-12-15T09:00:00Z',
|
||||
correlation_id: 'err-xyz789',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('failed')
|
||||
expect(result.current.data?.last_error).toBe('Invalid enrollment key')
|
||||
expect(result.current.data?.key_present).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle none status (not enrolled)', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'none',
|
||||
key_present: false,
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('none')
|
||||
expect(result.current.data?.key_present).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const error = new Error('Network failure')
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(result.current.error).toEqual(error)
|
||||
})
|
||||
|
||||
it('should NOT expose enrollment key in status response', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).not.toHaveProperty('enrollment_key')
|
||||
expect(result.current.data).not.toHaveProperty('encrypted_enroll_key')
|
||||
expect(result.current.data).toHaveProperty('key_present')
|
||||
})
|
||||
|
||||
it('should be configured with refetchOnWindowFocus disabled by default', async () => {
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue({
|
||||
status: 'pending',
|
||||
key_present: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
// Clear mock call count
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Simulate window focus
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
|
||||
// Wait a bit to see if refetch would happen
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Should NOT trigger refetch by default (refetchOnWindowFocus is not enabled in our config)
|
||||
expect(consoleEnrollmentApi.getConsoleStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle status with heartbeat timestamp', async () => {
|
||||
const mockStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'production',
|
||||
agent_name: 'charon-main',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-14T10:00:00Z',
|
||||
last_heartbeat_at: '2025-12-15T09:55:00Z',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.last_heartbeat_at).toBe('2025-12-15T09:55:00Z')
|
||||
expect(result.current.data?.enrolled_at).toBe('2025-12-14T10:00:00Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useEnrollConsole', () => {
|
||||
it('should enroll console and invalidate status query', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
enrolled_at: new Date().toISOString(),
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
const payload: ConsoleEnrollPayload = {
|
||||
enrollment_key: 'cs-enroll-key-123',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
}
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(consoleEnrollmentApi.enrollConsole).toHaveBeenCalledWith(payload)
|
||||
expect(result.current.data).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('should invalidate console status query on success', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
key_present: true,
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
// Set up initial status query
|
||||
queryClient.setQueryData(['crowdsec-console-status'], { status: 'pending', key_present: true })
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'key',
|
||||
agent_name: 'agent',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
// Verify invalidation happened
|
||||
const state = queryClient.getQueryState(['crowdsec-console-status'])
|
||||
expect(state?.isInvalidated).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle enrollment errors', async () => {
|
||||
const error = new Error('Invalid enrollment key')
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'invalid',
|
||||
agent_name: 'test',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(result.current.error).toEqual(error)
|
||||
})
|
||||
|
||||
it('should enroll with force flag', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'new-tenant',
|
||||
agent_name: 'charon-updated',
|
||||
key_present: true,
|
||||
enrolled_at: new Date().toISOString(),
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
const payload: ConsoleEnrollPayload = {
|
||||
enrollment_key: 'cs-enroll-new-key',
|
||||
agent_name: 'charon-updated',
|
||||
force: true,
|
||||
}
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(consoleEnrollmentApi.enrollConsole).toHaveBeenCalledWith(payload)
|
||||
expect(result.current.data?.agent_name).toBe('charon-updated')
|
||||
})
|
||||
|
||||
it('should enroll with optional tenant parameter', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'custom-org',
|
||||
agent_name: 'charon-1',
|
||||
key_present: true,
|
||||
enrolled_at: new Date().toISOString(),
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
const payload: ConsoleEnrollPayload = {
|
||||
enrollment_key: 'cs-enroll-abc123',
|
||||
tenant: 'custom-org',
|
||||
agent_name: 'charon-1',
|
||||
}
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.tenant).toBe('custom-org')
|
||||
})
|
||||
|
||||
it('should handle network errors during enrollment', async () => {
|
||||
const error = new Error('Network timeout')
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'valid-key',
|
||||
agent_name: 'agent',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(result.current.error?.message).toBe('Network timeout')
|
||||
})
|
||||
|
||||
it('should handle enrollment returning pending status', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'pending',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-1',
|
||||
key_present: true,
|
||||
last_attempt_at: new Date().toISOString(),
|
||||
correlation_id: 'req-123',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'cs-enroll-key',
|
||||
agent_name: 'charon-1',
|
||||
tenant: 'test-org',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('pending')
|
||||
expect(result.current.data?.correlation_id).toBe('req-123')
|
||||
})
|
||||
|
||||
it('should handle enrollment returning failed status', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'failed',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-1',
|
||||
key_present: false,
|
||||
last_error: 'Enrollment key expired',
|
||||
last_attempt_at: new Date().toISOString(),
|
||||
correlation_id: 'err-456',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'expired-key',
|
||||
agent_name: 'charon-1',
|
||||
tenant: 'test-org',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('failed')
|
||||
expect(result.current.data?.last_error).toBe('Enrollment key expired')
|
||||
})
|
||||
|
||||
it('should allow retry after transient enrollment failure', async () => {
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
// First attempt fails with network error
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockRejectedValueOnce(
|
||||
new Error('Network timeout')
|
||||
)
|
||||
|
||||
const payload: ConsoleEnrollPayload = {
|
||||
enrollment_key: 'cs-enroll-key',
|
||||
agent_name: 'agent',
|
||||
}
|
||||
|
||||
result.current.mutate(payload)
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
|
||||
// Second attempt succeeds
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValueOnce({
|
||||
status: 'enrolled',
|
||||
key_present: true,
|
||||
})
|
||||
|
||||
result.current.mutate(payload)
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.status).toBe('enrolled')
|
||||
})
|
||||
|
||||
it('should handle multiple enrollment mutations gracefully', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
key_present: true,
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
// Trigger first mutation
|
||||
result.current.mutate({ enrollment_key: 'key1', agent_name: 'agent1' })
|
||||
|
||||
// Trigger second mutation immediately
|
||||
result.current.mutate({ enrollment_key: 'key2', agent_name: 'agent2' })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
// Last mutation should be the one recorded
|
||||
expect(consoleEnrollmentApi.enrollConsole).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ enrollment_key: 'key2', agent_name: 'agent2' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle enrollment with correlation ID tracking', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'prod',
|
||||
agent_name: 'charon-main',
|
||||
key_present: true,
|
||||
enrolled_at: new Date().toISOString(),
|
||||
correlation_id: 'success-req-789',
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'cs-enroll-key',
|
||||
agent_name: 'charon-main',
|
||||
tenant: 'prod',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.correlation_id).toBe('success-req-789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('query key consistency', () => {
|
||||
it('should use consistent query key between status and enrollment', async () => {
|
||||
// Setup status query
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue({
|
||||
status: 'none',
|
||||
key_present: false,
|
||||
})
|
||||
|
||||
renderHook(() => useConsoleStatus(), { wrapper })
|
||||
await waitFor(() => {
|
||||
const queries = queryClient.getQueryCache().getAll()
|
||||
expect(queries.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// Verify the query exists with the correct key
|
||||
const statusQuery = queryClient.getQueryCache().find({
|
||||
queryKey: ['crowdsec-console-status'],
|
||||
})
|
||||
expect(statusQuery).toBeDefined()
|
||||
|
||||
// Setup enrollment mutation
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue({
|
||||
status: 'enrolled',
|
||||
key_present: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'key',
|
||||
agent_name: 'agent',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
|
||||
// Verify that the query was invalidated (refetch will be triggered if there's an observer)
|
||||
// The mutation's onSuccess should have called invalidateQueries
|
||||
const state = queryClient.getQueryState(['crowdsec-console-status'])
|
||||
expect(state).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty agent_name gracefully', async () => {
|
||||
const error = new Error('Agent name is required')
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'key',
|
||||
agent_name: '',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
})
|
||||
|
||||
it('should handle special characters in agent name', async () => {
|
||||
const mockResponse: ConsoleEnrollmentStatus = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'charon-prod-01',
|
||||
key_present: true,
|
||||
enrolled_at: new Date().toISOString(),
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.enrollConsole).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useEnrollConsole(), { wrapper })
|
||||
|
||||
result.current.mutate({
|
||||
enrollment_key: 'key',
|
||||
agent_name: 'charon-prod-01',
|
||||
})
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.agent_name).toBe('charon-prod-01')
|
||||
})
|
||||
|
||||
it('should handle missing optional fields in status response', async () => {
|
||||
const minimalStatus: ConsoleEnrollmentStatus = {
|
||||
status: 'none',
|
||||
key_present: false,
|
||||
}
|
||||
vi.mocked(consoleEnrollmentApi.getConsoleStatus).mockResolvedValue(minimalStatus)
|
||||
|
||||
const { result } = renderHook(() => useConsoleStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(minimalStatus)
|
||||
expect(result.current.data?.tenant).toBeUndefined()
|
||||
expect(result.current.data?.agent_name).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
144
frontend/src/hooks/__tests__/useDocker.test.tsx
Normal file
144
frontend/src/hooks/__tests__/useDocker.test.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useDocker } from '../useDocker';
|
||||
import { dockerApi } from '../../api/docker';
|
||||
import React from 'react';
|
||||
|
||||
vi.mock('../../api/docker', () => ({
|
||||
dockerApi: {
|
||||
listContainers: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
describe('useDocker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockContainers = [
|
||||
{
|
||||
id: 'abc123',
|
||||
names: ['/nginx'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
network: 'bridge',
|
||||
ip: '172.17.0.2',
|
||||
ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }],
|
||||
},
|
||||
];
|
||||
|
||||
it('fetches containers when host is provided', async () => {
|
||||
vi.mocked(dockerApi.listContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const { result } = renderHook(() => useDocker('192.168.1.100'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(dockerApi.listContainers).toHaveBeenCalledWith('192.168.1.100', undefined);
|
||||
expect(result.current.containers).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('fetches containers when serverId is provided', async () => {
|
||||
vi.mocked(dockerApi.listContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const { result } = renderHook(() => useDocker(undefined, 'server-123'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(dockerApi.listContainers).toHaveBeenCalledWith(undefined, 'server-123');
|
||||
expect(result.current.containers).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('does not fetch when both host and serverId are null', async () => {
|
||||
const { result } = renderHook(() => useDocker(null, null), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(dockerApi.listContainers).not.toHaveBeenCalled();
|
||||
expect(result.current.containers).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not fetch when both host and serverId are undefined', async () => {
|
||||
const { result } = renderHook(() => useDocker(undefined, undefined), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(dockerApi.listContainers).not.toHaveBeenCalled();
|
||||
expect(result.current.containers).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array as default when no data', async () => {
|
||||
vi.mocked(dockerApi.listContainers).mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useDocker('localhost'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.containers).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles API errors', async () => {
|
||||
vi.mocked(dockerApi.listContainers).mockRejectedValue(new Error('Docker not available'));
|
||||
|
||||
const { result } = renderHook(() => useDocker('localhost'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Wait for the query to complete (with retry)
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
// After retries, containers should still be empty array
|
||||
expect(result.current.containers).toEqual([]);
|
||||
});
|
||||
|
||||
it('provides refetch function', async () => {
|
||||
vi.mocked(dockerApi.listContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const { result } = renderHook(() => useDocker('localhost'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(typeof result.current.refetch).toBe('function');
|
||||
|
||||
// Call refetch
|
||||
await result.current.refetch();
|
||||
|
||||
expect(dockerApi.listContainers).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
143
frontend/src/hooks/__tests__/useDomains.test.tsx
Normal file
143
frontend/src/hooks/__tests__/useDomains.test.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useDomains } from '../useDomains';
|
||||
import * as api from '../../api/domains';
|
||||
import React from 'react';
|
||||
|
||||
vi.mock('../../api/domains', () => ({
|
||||
getDomains: vi.fn(),
|
||||
createDomain: vi.fn(),
|
||||
deleteDomain: vi.fn(),
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
describe('useDomains', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockDomains = [
|
||||
{ id: 1, uuid: 'uuid-1', name: 'example.com', created_at: '2024-01-01T00:00:00Z' },
|
||||
{ id: 2, uuid: 'uuid-2', name: 'test.com', created_at: '2024-01-02T00:00:00Z' },
|
||||
];
|
||||
|
||||
it('fetches domains on mount', async () => {
|
||||
vi.mocked(api.getDomains).mockResolvedValue(mockDomains);
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(api.getDomains).toHaveBeenCalled();
|
||||
expect(result.current.domains).toEqual(mockDomains);
|
||||
});
|
||||
|
||||
it('returns empty array as default', async () => {
|
||||
vi.mocked(api.getDomains).mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.domains).toEqual([]);
|
||||
});
|
||||
|
||||
it('creates a new domain', async () => {
|
||||
vi.mocked(api.getDomains).mockResolvedValue(mockDomains);
|
||||
vi.mocked(api.createDomain).mockResolvedValue({
|
||||
id: 3,
|
||||
uuid: 'uuid-3',
|
||||
name: 'new.com',
|
||||
created_at: '2024-01-03T00:00:00Z',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createDomain('new.com');
|
||||
});
|
||||
|
||||
// Check that createDomain was called with the correct first argument
|
||||
expect(api.createDomain).toHaveBeenCalled();
|
||||
expect(vi.mocked(api.createDomain).mock.calls[0][0]).toBe('new.com');
|
||||
});
|
||||
|
||||
it('deletes a domain', async () => {
|
||||
vi.mocked(api.getDomains).mockResolvedValue(mockDomains);
|
||||
vi.mocked(api.deleteDomain).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteDomain('uuid-1');
|
||||
});
|
||||
|
||||
// Check that deleteDomain was called with the correct first argument
|
||||
expect(api.deleteDomain).toHaveBeenCalled();
|
||||
expect(vi.mocked(api.deleteDomain).mock.calls[0][0]).toBe('uuid-1');
|
||||
});
|
||||
|
||||
it('handles API errors', async () => {
|
||||
vi.mocked(api.getDomains).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(result.current.domains).toEqual([]);
|
||||
});
|
||||
|
||||
it('provides isFetching state', async () => {
|
||||
vi.mocked(api.getDomains).mockResolvedValue(mockDomains);
|
||||
|
||||
const { result } = renderHook(() => useDomains(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Initially fetching
|
||||
expect(result.current.isFetching).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
348
frontend/src/hooks/__tests__/useImport.test.tsx
Normal file
348
frontend/src/hooks/__tests__/useImport.test.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
import { useImport } from '../useImport'
|
||||
import * as api from '../../api/import'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../api/import', () => ({
|
||||
uploadCaddyfile: vi.fn(),
|
||||
getImportPreview: vi.fn(),
|
||||
commitImport: vi.fn(),
|
||||
cancelImport: vi.fn(),
|
||||
getImportStatus: vi.fn(),
|
||||
}))
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useImport', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(api.getImportStatus).mockResolvedValue({ has_pending: false })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('starts with no active session', async () => {
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
|
||||
it('uploads content and creates session', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-1',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockPreviewData = {
|
||||
hosts: [{ domain_names: 'test.com' }],
|
||||
conflicts: [],
|
||||
errors: [],
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: mockPreviewData,
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('example.com { reverse_proxy localhost:8080 }')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
expect(api.uploadCaddyfile).toHaveBeenCalledWith('example.com { reverse_proxy localhost:8080 }')
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('handles upload errors', async () => {
|
||||
const mockError = new Error('Upload failed')
|
||||
vi.mocked(api.uploadCaddyfile).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
let threw = false
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.upload('invalid')
|
||||
} catch {
|
||||
threw = true
|
||||
}
|
||||
})
|
||||
expect(threw).toBe(true)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBe('Upload failed')
|
||||
})
|
||||
})
|
||||
|
||||
it('commits import with resolutions', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-2',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
let isCommitted = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCommitted) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.commitImport).mockImplementation(async () => {
|
||||
isCommitted = true
|
||||
return { created: 0, updated: 0, skipped: 0, errors: [] }
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commit({ 'test.com': 'skip' }, { 'test.com': 'Test' })
|
||||
})
|
||||
|
||||
expect(api.commitImport).toHaveBeenCalledWith('session-2', { 'test.com': 'skip' }, { 'test.com': 'Test' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels active import session', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-3',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
let isCancelled = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCancelled) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.cancelImport).mockImplementation(async () => {
|
||||
isCancelled = true
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.cancel()
|
||||
})
|
||||
|
||||
expect(api.cancelImport).toHaveBeenCalled()
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles commit errors', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-4',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockResolvedValue({ has_pending: true, session: mockSession })
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
|
||||
const mockError = new Error('Commit failed')
|
||||
vi.mocked(api.commitImport).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
let threw = false
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.commit({}, {})
|
||||
} catch {
|
||||
threw = true
|
||||
}
|
||||
})
|
||||
expect(threw).toBe(true)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBe('Commit failed')
|
||||
})
|
||||
})
|
||||
|
||||
it('captures and exposes commit result on success', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-5',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
const mockCommitResult = {
|
||||
created: 3,
|
||||
updated: 1,
|
||||
skipped: 2,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
let isCommitted = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCommitted) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.commitImport).mockImplementation(async () => {
|
||||
isCommitted = true
|
||||
return mockCommitResult
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commit({}, {})
|
||||
})
|
||||
|
||||
expect(result.current.commitResult).toEqual(mockCommitResult)
|
||||
expect(result.current.commitSuccess).toBe(true)
|
||||
})
|
||||
|
||||
it('clears commit result when clearCommitResult is called', async () => {
|
||||
const mockSession = {
|
||||
id: 'session-6',
|
||||
state: 'reviewing' as const,
|
||||
created_at: '2025-01-18T10:00:00Z',
|
||||
updated_at: '2025-01-18T10:00:00Z',
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
session: mockSession,
|
||||
preview: { hosts: [], conflicts: [], errors: [] },
|
||||
}
|
||||
|
||||
const mockCommitResult = {
|
||||
created: 2,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
let isCommitted = false
|
||||
vi.mocked(api.uploadCaddyfile).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.getImportStatus).mockImplementation(async () => {
|
||||
if (isCommitted) return { has_pending: false }
|
||||
return { has_pending: true, session: mockSession }
|
||||
})
|
||||
vi.mocked(api.getImportPreview).mockResolvedValue(mockResponse)
|
||||
vi.mocked(api.commitImport).mockImplementation(async () => {
|
||||
isCommitted = true
|
||||
return mockCommitResult
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useImport(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upload('test')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.session).toEqual(mockSession)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commit({}, {})
|
||||
})
|
||||
|
||||
expect(result.current.commitResult).toEqual(mockCommitResult)
|
||||
|
||||
act(() => {
|
||||
result.current.clearCommitResult()
|
||||
})
|
||||
|
||||
expect(result.current.commitResult).toBeNull()
|
||||
expect(result.current.commitSuccess).toBe(false)
|
||||
})
|
||||
})
|
||||
89
frontend/src/hooks/__tests__/useLanguage.test.tsx
Normal file
89
frontend/src/hooks/__tests__/useLanguage.test.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { ReactNode } from 'react'
|
||||
import { useLanguage } from '../useLanguage'
|
||||
import { LanguageProvider } from '../../context/LanguageContext'
|
||||
|
||||
// Mock i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: {
|
||||
changeLanguage: vi.fn(),
|
||||
language: 'en',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useLanguage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('throws error when used outside LanguageProvider', () => {
|
||||
// Suppress console.error for this test as React logs the error
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
consoleSpy.mockImplementation(() => {})
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useLanguage())
|
||||
}).toThrow('useLanguage must be used within a LanguageProvider')
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('provides default language', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<LanguageProvider>{children}</LanguageProvider>
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useLanguage(), { wrapper })
|
||||
|
||||
expect(result.current.language).toBe('en')
|
||||
})
|
||||
|
||||
it('changes language', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<LanguageProvider>{children}</LanguageProvider>
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useLanguage(), { wrapper })
|
||||
|
||||
act(() => {
|
||||
result.current.setLanguage('es')
|
||||
})
|
||||
|
||||
expect(result.current.language).toBe('es')
|
||||
expect(localStorage.getItem('charon-language')).toBe('es')
|
||||
})
|
||||
|
||||
it('persists language selection', () => {
|
||||
localStorage.setItem('charon-language', 'fr')
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<LanguageProvider>{children}</LanguageProvider>
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useLanguage(), { wrapper })
|
||||
|
||||
expect(result.current.language).toBe('fr')
|
||||
})
|
||||
|
||||
it('supports all configured languages', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<LanguageProvider>{children}</LanguageProvider>
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useLanguage(), { wrapper })
|
||||
|
||||
const languages = ['en', 'es', 'fr', 'de', 'zh'] as const
|
||||
|
||||
languages.forEach((lang) => {
|
||||
act(() => {
|
||||
result.current.setLanguage(lang)
|
||||
})
|
||||
expect(result.current.language).toBe(lang)
|
||||
})
|
||||
})
|
||||
})
|
||||
251
frontend/src/hooks/__tests__/useNotifications.test.tsx
Normal file
251
frontend/src/hooks/__tests__/useNotifications.test.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
useSecurityNotificationSettings,
|
||||
useUpdateSecurityNotificationSettings,
|
||||
} from '../useNotifications';
|
||||
import * as notificationsApi from '../../api/notifications';
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../api/notifications', async () => {
|
||||
const actual = await vi.importActual('../../api/notifications');
|
||||
return {
|
||||
...actual,
|
||||
getSecurityNotificationSettings: vi.fn(),
|
||||
updateSecurityNotificationSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock toast
|
||||
vi.mock('../../utils/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useNotifications hooks', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const createWrapper = () => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useSecurityNotificationSettings', () => {
|
||||
it('fetches security notification settings', async () => {
|
||||
const mockSettings: notificationsApi.SecurityNotificationSettings = {
|
||||
enabled: true,
|
||||
min_log_level: 'warn',
|
||||
notify_waf_blocks: true,
|
||||
notify_acl_denials: true,
|
||||
notify_rate_limit_hits: false,
|
||||
webhook_url: 'https://example.com/webhook',
|
||||
email_recipients: 'admin@example.com',
|
||||
};
|
||||
|
||||
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings);
|
||||
|
||||
const { result } = renderHook(() => useSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockSettings);
|
||||
expect(notificationsApi.getSecurityNotificationSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles fetch errors', async () => {
|
||||
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockRejectedValue(
|
||||
new Error('Network error')
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateSecurityNotificationSettings', () => {
|
||||
const mockSettings: notificationsApi.SecurityNotificationSettings = {
|
||||
enabled: true,
|
||||
min_log_level: 'warn',
|
||||
notify_waf_blocks: true,
|
||||
notify_acl_denials: true,
|
||||
notify_rate_limit_hits: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings);
|
||||
});
|
||||
|
||||
it('updates security notification settings', async () => {
|
||||
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
||||
updatedSettings
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ min_log_level: 'error' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith({
|
||||
min_log_level: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('performs optimistic update', async () => {
|
||||
const updatedSettings = { ...mockSettings, enabled: false };
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
||||
updatedSettings
|
||||
);
|
||||
|
||||
// Pre-populate cache
|
||||
queryClient.setQueryData(['security-notification-settings'], mockSettings);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ enabled: false });
|
||||
|
||||
// Wait a bit for the optimistic update to take effect
|
||||
await waitFor(() => {
|
||||
const cachedData = queryClient.getQueryData(['security-notification-settings']);
|
||||
expect(cachedData).toMatchObject({ enabled: false });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
});
|
||||
|
||||
it('rolls back on error', async () => {
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockRejectedValue(
|
||||
new Error('Update failed')
|
||||
);
|
||||
|
||||
// Pre-populate cache
|
||||
queryClient.setQueryData(['security-notification-settings'], mockSettings);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ enabled: false });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
// Check that original data is restored
|
||||
const cachedData = queryClient.getQueryData(['security-notification-settings']);
|
||||
expect(cachedData).toEqual(mockSettings);
|
||||
});
|
||||
|
||||
it('shows success toast on successful update', async () => {
|
||||
const toast = await import('../../utils/toast');
|
||||
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
||||
updatedSettings
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ min_log_level: 'error' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(toast.toast.success).toHaveBeenCalledWith('Notification settings updated');
|
||||
});
|
||||
|
||||
it('shows error toast on failed update', async () => {
|
||||
const toast = await import('../../utils/toast');
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockRejectedValue(
|
||||
new Error('Update failed')
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ enabled: false });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(toast.toast.error).toHaveBeenCalledWith('Update failed');
|
||||
});
|
||||
|
||||
it('invalidates queries on success', async () => {
|
||||
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
||||
updatedSettings
|
||||
);
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ min_log_level: 'error' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({
|
||||
queryKey: ['security-notification-settings'],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles updates with multiple fields', async () => {
|
||||
const updatedSettings = {
|
||||
...mockSettings,
|
||||
enabled: false,
|
||||
min_log_level: 'error',
|
||||
webhook_url: 'https://new-webhook.com',
|
||||
};
|
||||
|
||||
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
||||
updatedSettings
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
enabled: false,
|
||||
min_log_level: 'error',
|
||||
webhook_url: 'https://new-webhook.com',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith({
|
||||
enabled: false,
|
||||
min_log_level: 'error',
|
||||
webhook_url: 'https://new-webhook.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
159
frontend/src/hooks/__tests__/useProxyHosts-bulk.test.tsx
Normal file
159
frontend/src/hooks/__tests__/useProxyHosts-bulk.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useProxyHosts } from '../useProxyHosts';
|
||||
import * as proxyHostsApi from '../../api/proxyHosts';
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('../../api/proxyHosts');
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useProxyHosts bulk operations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('bulkUpdateACL', () => {
|
||||
it('should apply ACL to multiple hosts', async () => {
|
||||
const mockResponse = {
|
||||
updated: 2,
|
||||
errors: [],
|
||||
};
|
||||
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([]);
|
||||
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
const hostUUIDs = ['uuid-1', 'uuid-2'];
|
||||
const accessListID = 5;
|
||||
|
||||
const response = await result.current.bulkUpdateACL(hostUUIDs, accessListID);
|
||||
|
||||
expect(proxyHostsApi.bulkUpdateACL).toHaveBeenCalledWith(hostUUIDs, accessListID);
|
||||
expect(response.updated).toBe(2);
|
||||
expect(response.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove ACL from hosts', async () => {
|
||||
const mockResponse = {
|
||||
updated: 1,
|
||||
errors: [],
|
||||
};
|
||||
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([]);
|
||||
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
const response = await result.current.bulkUpdateACL(['uuid-1'], null);
|
||||
|
||||
expect(proxyHostsApi.bulkUpdateACL).toHaveBeenCalledWith(['uuid-1'], null);
|
||||
expect(response.updated).toBe(1);
|
||||
});
|
||||
|
||||
it('should invalidate queries after successful bulk update', async () => {
|
||||
const mockHosts = [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
name: 'Host 1',
|
||||
domain_names: 'host1.example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8001,
|
||||
ssl_forced: false,
|
||||
http2_support: false,
|
||||
hsts_enabled: false,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
application: 'none' as const,
|
||||
locations: [],
|
||||
enabled: true,
|
||||
access_list_id: null,
|
||||
certificate_id: null,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(proxyHostsApi.getProxyHosts)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce(mockHosts);
|
||||
|
||||
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({
|
||||
updated: 1,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.hosts).toEqual([]);
|
||||
|
||||
await result.current.bulkUpdateACL(['uuid-1'], 10);
|
||||
|
||||
// Query should be invalidated and refetch
|
||||
await waitFor(() => expect(result.current.hosts).toEqual(mockHosts));
|
||||
});
|
||||
|
||||
it('should handle bulk update errors', async () => {
|
||||
const error = new Error('Bulk update failed');
|
||||
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([]);
|
||||
vi.mocked(proxyHostsApi.bulkUpdateACL).mockRejectedValue(error);
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
await expect(result.current.bulkUpdateACL(['uuid-1'], 5)).rejects.toThrow(
|
||||
'Bulk update failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should track bulk updating state', async () => {
|
||||
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([]);
|
||||
vi.mocked(proxyHostsApi.bulkUpdateACL).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ updated: 1, errors: [] }), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.isBulkUpdating).toBe(false);
|
||||
|
||||
const promise = result.current.bulkUpdateACL(['uuid-1'], 1);
|
||||
|
||||
await waitFor(() => expect(result.current.isBulkUpdating).toBe(true));
|
||||
|
||||
await promise;
|
||||
|
||||
await waitFor(() => expect(result.current.isBulkUpdating).toBe(false));
|
||||
});
|
||||
});
|
||||
});
|
||||
200
frontend/src/hooks/__tests__/useProxyHosts.test.tsx
Normal file
200
frontend/src/hooks/__tests__/useProxyHosts.test.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
import { useProxyHosts } from '../useProxyHosts'
|
||||
import * as api from '../../api/proxyHosts'
|
||||
import { createMockProxyHost } from '../../testUtils/createMockProxyHost'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../api/proxyHosts', () => ({
|
||||
getProxyHosts: vi.fn(),
|
||||
createProxyHost: vi.fn(),
|
||||
updateProxyHost: vi.fn(),
|
||||
deleteProxyHost: vi.fn(),
|
||||
}))
|
||||
|
||||
const createMockHost = (overrides: Partial<api.ProxyHost> = {}) => createMockProxyHost(overrides)
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useProxyHosts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('loads proxy hosts on mount', async () => {
|
||||
const mockHosts = [
|
||||
createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }),
|
||||
createMockHost({ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 }),
|
||||
]
|
||||
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue(mockHosts)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.hosts).toEqual([])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.hosts).toEqual(mockHosts)
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(api.getProxyHosts).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('handles loading errors', async () => {
|
||||
const mockError = new Error('Failed to fetch')
|
||||
vi.mocked(api.getProxyHosts).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe('Failed to fetch')
|
||||
expect(result.current.hosts).toEqual([])
|
||||
})
|
||||
|
||||
it('creates a new proxy host', async () => {
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([])
|
||||
const newHost = { domain_names: 'new.com', forward_host: 'localhost', forward_port: 9000 }
|
||||
const createdHost = createMockHost({ uuid: '3', ...newHost, enabled: true })
|
||||
|
||||
vi.mocked(api.createProxyHost).mockImplementation(async () => {
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([createdHost])
|
||||
return createdHost
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createHost(newHost)
|
||||
})
|
||||
|
||||
expect(api.createProxyHost).toHaveBeenCalledWith(newHost)
|
||||
await waitFor(() => {
|
||||
expect(result.current.hosts).toContainEqual(createdHost)
|
||||
})
|
||||
})
|
||||
|
||||
it('updates an existing proxy host', async () => {
|
||||
const existingHost = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
let hosts = [existingHost]
|
||||
vi.mocked(api.getProxyHosts).mockImplementation(() => Promise.resolve(hosts))
|
||||
|
||||
vi.mocked(api.updateProxyHost).mockImplementation(async (_, data) => {
|
||||
hosts = [{ ...existingHost, ...data }]
|
||||
return hosts[0]
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateHost('1', { domain_names: 'updated.com' })
|
||||
})
|
||||
|
||||
expect(api.updateProxyHost).toHaveBeenCalledWith('1', { domain_names: 'updated.com' })
|
||||
await waitFor(() => {
|
||||
expect(result.current.hosts[0].domain_names).toBe('updated.com')
|
||||
})
|
||||
})
|
||||
|
||||
it('deletes a proxy host', async () => {
|
||||
const hosts = [
|
||||
createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 }),
|
||||
createMockHost({ uuid: '2', domain_names: 'app.com', enabled: true, forward_host: 'localhost', forward_port: 3000 }),
|
||||
]
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue(hosts)
|
||||
vi.mocked(api.deleteProxyHost).mockImplementation(async (uuid) => {
|
||||
const remaining = hosts.filter(h => h.uuid !== uuid)
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue(remaining)
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteHost('1')
|
||||
})
|
||||
|
||||
expect(api.deleteProxyHost).toHaveBeenCalledWith('1')
|
||||
await waitFor(() => {
|
||||
expect(result.current.hosts).toHaveLength(1)
|
||||
expect(result.current.hosts[0].uuid).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
it('handles create errors', async () => {
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([])
|
||||
const mockError = new Error('Failed to create')
|
||||
vi.mocked(api.createProxyHost).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.createHost({ domain_names: 'test.com', forward_host: 'localhost', forward_port: 8080 })).rejects.toThrow('Failed to create')
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const host = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.updateProxyHost).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.updateHost('1', { domain_names: 'updated.com' })).rejects.toThrow('Failed to update')
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const host = createMockHost({ uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 })
|
||||
vi.mocked(api.getProxyHosts).mockResolvedValue([host])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.deleteProxyHost).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useProxyHosts(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.deleteHost('1')).rejects.toThrow('Failed to delete')
|
||||
})
|
||||
})
|
||||
242
frontend/src/hooks/__tests__/useRemoteServers.test.tsx
Normal file
242
frontend/src/hooks/__tests__/useRemoteServers.test.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react'
|
||||
import { useRemoteServers } from '../useRemoteServers'
|
||||
import * as api from '../../api/remoteServers'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('../../api/remoteServers', () => ({
|
||||
getRemoteServers: vi.fn(),
|
||||
createRemoteServer: vi.fn(),
|
||||
updateRemoteServer: vi.fn(),
|
||||
deleteRemoteServer: vi.fn(),
|
||||
testRemoteServerConnection: vi.fn(),
|
||||
}))
|
||||
|
||||
const createMockServer = (overrides: Partial<api.RemoteServer> = {}): api.RemoteServer => ({
|
||||
uuid: '1',
|
||||
name: 'Server 1',
|
||||
provider: 'generic',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
enabled: true,
|
||||
reachable: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useRemoteServers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('loads all remote servers on mount', async () => {
|
||||
const mockServers = [
|
||||
createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }),
|
||||
createMockServer({ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false }),
|
||||
]
|
||||
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue(mockServers)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.servers).toEqual([])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.servers).toEqual(mockServers)
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(api.getRemoteServers).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('handles loading errors', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
vi.mocked(api.getRemoteServers).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe('Network error')
|
||||
expect(result.current.servers).toEqual([])
|
||||
})
|
||||
|
||||
it('creates a new remote server', async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([])
|
||||
const newServer = { name: 'New Server', host: 'new.local', port: 5000, provider: 'generic' }
|
||||
const createdServer = createMockServer({ uuid: '4', ...newServer, enabled: true })
|
||||
|
||||
vi.mocked(api.createRemoteServer).mockImplementation(async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([createdServer])
|
||||
return createdServer
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createServer(newServer)
|
||||
})
|
||||
|
||||
expect(api.createRemoteServer).toHaveBeenCalledWith(newServer)
|
||||
await waitFor(() => {
|
||||
expect(result.current.servers).toContainEqual(createdServer)
|
||||
})
|
||||
})
|
||||
|
||||
it('updates an existing remote server', async () => {
|
||||
const existingServer = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
let servers = [existingServer]
|
||||
vi.mocked(api.getRemoteServers).mockImplementation(() => Promise.resolve(servers))
|
||||
|
||||
vi.mocked(api.updateRemoteServer).mockImplementation(async (_, data) => {
|
||||
servers = [{ ...existingServer, ...data }]
|
||||
return servers[0]
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateServer('1', { name: 'Updated Server' })
|
||||
})
|
||||
|
||||
expect(api.updateRemoteServer).toHaveBeenCalledWith('1', { name: 'Updated Server' })
|
||||
await waitFor(() => {
|
||||
expect(result.current.servers[0].name).toBe('Updated Server')
|
||||
})
|
||||
})
|
||||
|
||||
it('deletes a remote server', async () => {
|
||||
const servers = [
|
||||
createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true }),
|
||||
createMockServer({ uuid: '2', name: 'Server 2', host: '192.168.1.100', port: 3000, enabled: false }),
|
||||
]
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue(servers)
|
||||
vi.mocked(api.deleteRemoteServer).mockImplementation(async (uuid) => {
|
||||
const remaining = servers.filter(s => s.uuid !== uuid)
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue(remaining)
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteServer('1')
|
||||
})
|
||||
|
||||
expect(api.deleteRemoteServer).toHaveBeenCalledWith('1')
|
||||
await waitFor(() => {
|
||||
expect(result.current.servers).toHaveLength(1)
|
||||
expect(result.current.servers[0].uuid).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
it('tests server connection', async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([])
|
||||
const testResult = { reachable: true, address: 'localhost:8080' }
|
||||
vi.mocked(api.testRemoteServerConnection).mockResolvedValue(testResult)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
const response = await result.current.testConnection('1')
|
||||
|
||||
expect(api.testRemoteServerConnection).toHaveBeenCalledWith('1')
|
||||
expect(response).toEqual(testResult)
|
||||
})
|
||||
|
||||
it('handles create errors', async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([])
|
||||
const mockError = new Error('Failed to create')
|
||||
vi.mocked(api.createRemoteServer).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.createServer({ name: 'Test', host: 'localhost', port: 8080 })).rejects.toThrow('Failed to create')
|
||||
})
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const server = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to update')
|
||||
vi.mocked(api.updateRemoteServer).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.updateServer('1', { name: 'Updated Server' })).rejects.toThrow('Failed to update')
|
||||
})
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const server = createMockServer({ uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true })
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([server])
|
||||
const mockError = new Error('Failed to delete')
|
||||
vi.mocked(api.deleteRemoteServer).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.deleteServer('1')).rejects.toThrow('Failed to delete')
|
||||
})
|
||||
|
||||
it('handles connection test errors', async () => {
|
||||
vi.mocked(api.getRemoteServers).mockResolvedValue([])
|
||||
const mockError = new Error('Connection failed')
|
||||
vi.mocked(api.testRemoteServerConnection).mockRejectedValue(mockError)
|
||||
|
||||
const { result } = renderHook(() => useRemoteServers(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
await expect(result.current.testConnection('1')).rejects.toThrow('Connection failed')
|
||||
})
|
||||
})
|
||||
298
frontend/src/hooks/__tests__/useSecurity.test.tsx
Normal file
298
frontend/src/hooks/__tests__/useSecurity.test.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import {
|
||||
useSecurityStatus,
|
||||
useSecurityConfig,
|
||||
useUpdateSecurityConfig,
|
||||
useGenerateBreakGlassToken,
|
||||
useDecisions,
|
||||
useCreateDecision,
|
||||
useRuleSets,
|
||||
useUpsertRuleSet,
|
||||
useDeleteRuleSet,
|
||||
useEnableCerberus,
|
||||
useDisableCerberus,
|
||||
} from '../useSecurity'
|
||||
import * as securityApi from '../../api/security'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
vi.mock('../../api/security')
|
||||
vi.mock('react-hot-toast')
|
||||
|
||||
describe('useSecurity hooks', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
|
||||
describe('useSecurityStatus', () => {
|
||||
it('should fetch security status', async () => {
|
||||
const mockStatus = {
|
||||
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 }
|
||||
}
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockStatus)
|
||||
|
||||
const { result } = renderHook(() => useSecurityStatus(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(mockStatus)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useSecurityConfig', () => {
|
||||
it('should fetch security config', async () => {
|
||||
const mockConfig = { config: { admin_whitelist: '10.0.0.0/8' } }
|
||||
vi.mocked(securityApi.getSecurityConfig).mockResolvedValue(mockConfig)
|
||||
|
||||
const { result } = renderHook(() => useSecurityConfig(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(mockConfig)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useUpdateSecurityConfig', () => {
|
||||
it('should update security config and invalidate queries on success', async () => {
|
||||
const payload = { admin_whitelist: '192.168.0.0/16' }
|
||||
vi.mocked(securityApi.updateSecurityConfig).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityConfig(), { wrapper })
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.updateSecurityConfig).toHaveBeenCalledWith(payload)
|
||||
expect(toast.success).toHaveBeenCalledWith('Security configuration updated')
|
||||
})
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const error = new Error('Update failed')
|
||||
vi.mocked(securityApi.updateSecurityConfig).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityConfig(), { wrapper })
|
||||
|
||||
result.current.mutate({ admin_whitelist: 'invalid' })
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to update security settings: Update failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGenerateBreakGlassToken', () => {
|
||||
it('should generate break glass token', async () => {
|
||||
const mockToken = { token: 'abc123' }
|
||||
vi.mocked(securityApi.generateBreakGlassToken).mockResolvedValue(mockToken)
|
||||
|
||||
const { result } = renderHook(() => useGenerateBreakGlassToken(), { wrapper })
|
||||
|
||||
result.current.mutate(undefined)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(mockToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDecisions', () => {
|
||||
it('should fetch decisions with default limit', async () => {
|
||||
const mockDecisions = { decisions: [{ ip: '1.2.3.4', type: 'ban' }] }
|
||||
vi.mocked(securityApi.getDecisions).mockResolvedValue(mockDecisions)
|
||||
|
||||
const { result } = renderHook(() => useDecisions(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.getDecisions).toHaveBeenCalledWith(50)
|
||||
expect(result.current.data).toEqual(mockDecisions)
|
||||
})
|
||||
|
||||
it('should fetch decisions with custom limit', async () => {
|
||||
const mockDecisions = { decisions: [] }
|
||||
vi.mocked(securityApi.getDecisions).mockResolvedValue(mockDecisions)
|
||||
|
||||
const { result } = renderHook(() => useDecisions(100), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.getDecisions).toHaveBeenCalledWith(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useCreateDecision', () => {
|
||||
it('should create decision and invalidate queries', async () => {
|
||||
const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
vi.mocked(securityApi.createDecision).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useCreateDecision(), { wrapper })
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.createDecision).toHaveBeenCalledWith(payload)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useRuleSets', () => {
|
||||
it('should fetch rule sets', async () => {
|
||||
const mockRuleSets = {
|
||||
rulesets: [{
|
||||
id: 1,
|
||||
uuid: 'abc-123',
|
||||
name: 'OWASP CRS',
|
||||
source_url: 'https://example.com',
|
||||
mode: 'blocking',
|
||||
last_updated: '2025-12-04',
|
||||
content: 'rules'
|
||||
}]
|
||||
}
|
||||
vi.mocked(securityApi.getRuleSets).mockResolvedValue(mockRuleSets)
|
||||
|
||||
const { result } = renderHook(() => useRuleSets(), { wrapper })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data).toEqual(mockRuleSets)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useUpsertRuleSet', () => {
|
||||
it('should upsert rule set and show success toast', async () => {
|
||||
const payload = { name: 'Custom Rules', content: 'rule data', mode: 'blocking' as const }
|
||||
vi.mocked(securityApi.upsertRuleSet).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useUpsertRuleSet(), { wrapper })
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.upsertRuleSet).toHaveBeenCalledWith(payload)
|
||||
expect(toast.success).toHaveBeenCalledWith('Rule set saved successfully')
|
||||
})
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const error = new Error('Save failed')
|
||||
vi.mocked(securityApi.upsertRuleSet).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useUpsertRuleSet(), { wrapper })
|
||||
|
||||
result.current.mutate({ name: 'Test', content: 'data', mode: 'blocking' })
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to save rule set: Save failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDeleteRuleSet', () => {
|
||||
it('should delete rule set and show success toast', async () => {
|
||||
vi.mocked(securityApi.deleteRuleSet).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useDeleteRuleSet(), { wrapper })
|
||||
|
||||
result.current.mutate(1)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.deleteRuleSet).toHaveBeenCalledWith(1)
|
||||
expect(toast.success).toHaveBeenCalledWith('Rule set deleted')
|
||||
})
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const error = new Error('Delete failed')
|
||||
vi.mocked(securityApi.deleteRuleSet).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useDeleteRuleSet(), { wrapper })
|
||||
|
||||
result.current.mutate(1)
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to delete rule set: Delete failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useEnableCerberus', () => {
|
||||
it('should enable Cerberus and show success toast', async () => {
|
||||
vi.mocked(securityApi.enableCerberus).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useEnableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(undefined)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.enableCerberus).toHaveBeenCalledWith(undefined)
|
||||
expect(toast.success).toHaveBeenCalledWith('Cerberus enabled')
|
||||
})
|
||||
|
||||
it('should enable Cerberus with payload', async () => {
|
||||
const payload = { mode: 'full' }
|
||||
vi.mocked(securityApi.enableCerberus).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useEnableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.enableCerberus).toHaveBeenCalledWith(payload)
|
||||
})
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const error = new Error('Enable failed')
|
||||
vi.mocked(securityApi.enableCerberus).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useEnableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(undefined)
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to enable Cerberus: Enable failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDisableCerberus', () => {
|
||||
it('should disable Cerberus and show success toast', async () => {
|
||||
vi.mocked(securityApi.disableCerberus).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useDisableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(undefined)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.disableCerberus).toHaveBeenCalledWith(undefined)
|
||||
expect(toast.success).toHaveBeenCalledWith('Cerberus disabled')
|
||||
})
|
||||
|
||||
it('should disable Cerberus with payload', async () => {
|
||||
const payload = { reason: 'maintenance' }
|
||||
vi.mocked(securityApi.disableCerberus).mockResolvedValue({ success: true })
|
||||
|
||||
const { result } = renderHook(() => useDisableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(payload)
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(securityApi.disableCerberus).toHaveBeenCalledWith(payload)
|
||||
})
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const error = new Error('Disable failed')
|
||||
vi.mocked(securityApi.disableCerberus).mockRejectedValue(error)
|
||||
|
||||
const { result } = renderHook(() => useDisableCerberus(), { wrapper })
|
||||
|
||||
result.current.mutate(undefined)
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to disable Cerberus: Disable failed')
|
||||
})
|
||||
})
|
||||
})
|
||||
301
frontend/src/hooks/__tests__/useSecurityHeaders.test.tsx
Normal file
301
frontend/src/hooks/__tests__/useSecurityHeaders.test.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
useSecurityHeaderProfiles,
|
||||
useSecurityHeaderProfile,
|
||||
useCreateSecurityHeaderProfile,
|
||||
useUpdateSecurityHeaderProfile,
|
||||
useDeleteSecurityHeaderProfile,
|
||||
useSecurityHeaderPresets,
|
||||
useApplySecurityHeaderPreset,
|
||||
useCalculateSecurityScore,
|
||||
useValidateCSP,
|
||||
useBuildCSP,
|
||||
} from '../useSecurityHeaders';
|
||||
import {
|
||||
securityHeadersApi,
|
||||
SecurityHeaderProfile,
|
||||
SecurityHeaderPreset,
|
||||
CreateProfileRequest,
|
||||
} from '../../api/securityHeaders';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
vi.mock('../../api/securityHeaders');
|
||||
vi.mock('react-hot-toast');
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useSecurityHeaders', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useSecurityHeaderProfiles', () => {
|
||||
it('should fetch profiles successfully', async () => {
|
||||
const mockProfiles: SecurityHeaderProfile[] = [
|
||||
{ id: 1, name: 'Profile 1', security_score: 85 } as SecurityHeaderProfile,
|
||||
{ id: 2, name: 'Profile 2', security_score: 90 } as SecurityHeaderProfile,
|
||||
];
|
||||
|
||||
vi.mocked(securityHeadersApi.listProfiles).mockResolvedValue(mockProfiles);
|
||||
|
||||
const { result } = renderHook(() => useSecurityHeaderProfiles(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockProfiles);
|
||||
expect(securityHeadersApi.listProfiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle error when fetching profiles', async () => {
|
||||
vi.mocked(securityHeadersApi.listProfiles).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useSecurityHeaderProfiles(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSecurityHeaderProfile', () => {
|
||||
it('should fetch a single profile', async () => {
|
||||
const mockProfile: SecurityHeaderProfile = { id: 1, name: 'Profile 1', security_score: 85 } as SecurityHeaderProfile;
|
||||
|
||||
vi.mocked(securityHeadersApi.getProfile).mockResolvedValue(mockProfile);
|
||||
|
||||
const { result } = renderHook(() => useSecurityHeaderProfile(1), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockProfile);
|
||||
expect(securityHeadersApi.getProfile).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not fetch when id is undefined', () => {
|
||||
const { result } = renderHook(() => useSecurityHeaderProfile(undefined), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(securityHeadersApi.getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateSecurityHeaderProfile', () => {
|
||||
it('should create a profile successfully', async () => {
|
||||
const newProfile: CreateProfileRequest = { name: 'New Profile', hsts_enabled: true };
|
||||
const createdProfile: SecurityHeaderProfile = { id: 1, ...newProfile, security_score: 80 } as SecurityHeaderProfile;
|
||||
|
||||
vi.mocked(securityHeadersApi.createProfile).mockResolvedValue(createdProfile);
|
||||
|
||||
const { result } = renderHook(() => useCreateSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(newProfile);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(securityHeadersApi.createProfile).toHaveBeenCalledWith(newProfile);
|
||||
expect(toast.success).toHaveBeenCalledWith('Security header profile created successfully');
|
||||
});
|
||||
|
||||
it('should handle error when creating profile', async () => {
|
||||
vi.mocked(securityHeadersApi.createProfile).mockRejectedValue(new Error('Validation error'));
|
||||
|
||||
const { result } = renderHook(() => useCreateSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ name: 'Test' });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to create profile: Validation error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateSecurityHeaderProfile', () => {
|
||||
it('should update a profile successfully', async () => {
|
||||
const updateData: Partial<CreateProfileRequest> = { name: 'Updated Profile' };
|
||||
const updatedProfile: SecurityHeaderProfile = { id: 1, ...updateData, security_score: 85 } as SecurityHeaderProfile;
|
||||
|
||||
vi.mocked(securityHeadersApi.updateProfile).mockResolvedValue(updatedProfile);
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ id: 1, data: updateData });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(securityHeadersApi.updateProfile).toHaveBeenCalledWith(1, updateData);
|
||||
expect(toast.success).toHaveBeenCalledWith('Security header profile updated successfully');
|
||||
});
|
||||
|
||||
it('should handle error when updating profile', async () => {
|
||||
vi.mocked(securityHeadersApi.updateProfile).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const { result } = renderHook(() => useUpdateSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ id: 1, data: { name: 'Test' } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to update profile: Not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteSecurityHeaderProfile', () => {
|
||||
it('should delete a profile successfully', async () => {
|
||||
vi.mocked(securityHeadersApi.deleteProfile).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useDeleteSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(1);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(securityHeadersApi.deleteProfile).toHaveBeenCalledWith(1);
|
||||
expect(toast.success).toHaveBeenCalledWith('Security header profile deleted successfully');
|
||||
});
|
||||
|
||||
it('should handle error when deleting profile', async () => {
|
||||
vi.mocked(securityHeadersApi.deleteProfile).mockRejectedValue(new Error('Cannot delete preset'));
|
||||
|
||||
const { result } = renderHook(() => useDeleteSecurityHeaderProfile(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(1);
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to delete profile: Cannot delete preset');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSecurityHeaderPresets', () => {
|
||||
it('should fetch presets successfully', async () => {
|
||||
const mockPresets: SecurityHeaderPreset[] = [
|
||||
{ preset_type: 'basic', name: 'Basic Security', security_score: 65 } as SecurityHeaderPreset,
|
||||
{ preset_type: 'strict', name: 'Strict Security', security_score: 85 } as SecurityHeaderPreset,
|
||||
];
|
||||
|
||||
vi.mocked(securityHeadersApi.getPresets).mockResolvedValue(mockPresets);
|
||||
|
||||
const { result } = renderHook(() => useSecurityHeaderPresets(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockPresets);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useApplySecurityHeaderPreset', () => {
|
||||
it('should apply preset successfully', async () => {
|
||||
const appliedProfile: SecurityHeaderProfile = { id: 1, name: 'Basic Security', security_score: 65 } as SecurityHeaderProfile;
|
||||
|
||||
vi.mocked(securityHeadersApi.applyPreset).mockResolvedValue(appliedProfile);
|
||||
|
||||
const { result } = renderHook(() => useApplySecurityHeaderPreset(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ preset_type: 'basic', name: 'Basic Security' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(securityHeadersApi.applyPreset).toHaveBeenCalledWith({ preset_type: 'basic', name: 'Basic Security' });
|
||||
expect(toast.success).toHaveBeenCalledWith('Preset applied successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCalculateSecurityScore', () => {
|
||||
it('should calculate score successfully', async () => {
|
||||
const mockScore = {
|
||||
score: 85,
|
||||
max_score: 100,
|
||||
breakdown: { hsts: 25, csp: 20 },
|
||||
suggestions: ['Enable CSP'],
|
||||
};
|
||||
|
||||
vi.mocked(securityHeadersApi.calculateScore).mockResolvedValue(mockScore);
|
||||
|
||||
const { result } = renderHook(() => useCalculateSecurityScore(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({ hsts_enabled: true });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockScore);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useValidateCSP', () => {
|
||||
it('should validate CSP successfully', async () => {
|
||||
const mockValidation = { valid: true, errors: [] };
|
||||
|
||||
vi.mocked(securityHeadersApi.validateCSP).mockResolvedValue(mockValidation);
|
||||
|
||||
const { result } = renderHook(() => useValidateCSP(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate("default-src 'self'");
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockValidation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBuildCSP', () => {
|
||||
it('should build CSP string successfully', async () => {
|
||||
const mockDirectives = [
|
||||
{ directive: 'default-src', values: ["'self'"] },
|
||||
];
|
||||
const mockResult = { csp: "default-src 'self'" };
|
||||
|
||||
vi.mocked(securityHeadersApi.buildCSP).mockResolvedValue(mockResult);
|
||||
|
||||
const { result } = renderHook(() => useBuildCSP(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate(mockDirectives);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
frontend/src/hooks/__tests__/useTheme.test.tsx
Normal file
17
frontend/src/hooks/__tests__/useTheme.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useTheme } from '../useTheme'
|
||||
|
||||
describe('useTheme', () => {
|
||||
it('throws error when used outside ThemeProvider', () => {
|
||||
// Suppress console.error for this test as React logs the error
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
consoleSpy.mockImplementation(() => {})
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTheme())
|
||||
}).toThrow('useTheme must be used within a ThemeProvider')
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user