chore: clean git cache

This commit is contained in:
GitHub Actions
2026-01-02 01:01:54 +00:00
parent aae55a8ae9
commit 5ea207ab47
290 changed files with 62353 additions and 0 deletions

View 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);
});
});
});

View 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()
})
})

View 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()
})
})
})

View 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);
});
});

View 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);
});
});
});

View 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)
})
})

View 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)
})
})
})

View 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',
});
});
});
});

View 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));
});
});
});

View 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')
})
})

View 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')
})
})

View 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')
})
})
})

View 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);
});
});
});

View 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()
})
})