chore: clean .gitignore cache

This commit is contained in:
GitHub Actions
2026-01-26 19:21:33 +00:00
parent 1b1b3a70b1
commit e5f0fec5db
1483 changed files with 0 additions and 472793 deletions

View File

@@ -1,179 +0,0 @@
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

@@ -1,136 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAuditLogs, useAuditLog, useAuditLogsByProvider } from '../useAuditLogs'
// Mock the API module
vi.mock('../../api/auditLogs', () => ({
getAuditLogs: vi.fn(),
getAuditLog: vi.fn(),
getAuditLogsByProvider: vi.fn(),
}))
import { getAuditLogs, getAuditLog, getAuditLogsByProvider } from '../../api/auditLogs'
const mockAuditLog = {
id: 1,
uuid: 'test-uuid-123',
actor: 'admin@test.com',
action: 'dns_provider_create' as const,
event_category: 'dns_provider' as const,
resource_id: 1,
details: 'Created DNS provider',
ip_address: '127.0.0.1',
created_at: '2026-01-24T12:00:00Z',
}
const mockListResponse = {
logs: [mockAuditLog],
total: 1,
page: 1,
limit: 50,
}
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useAuditLogs hook', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches audit logs with default parameters', async () => {
vi.mocked(getAuditLogs).mockResolvedValue(mockListResponse)
const { result } = renderHook(() => useAuditLogs(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getAuditLogs).toHaveBeenCalledWith(undefined, 1, 50)
expect(result.current.data).toEqual(mockListResponse)
})
it('fetches audit logs with filters', async () => {
vi.mocked(getAuditLogs).mockResolvedValue(mockListResponse)
const filters = { event_category: 'dns_provider' as const }
const { result } = renderHook(() => useAuditLogs(filters, 2, 25), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getAuditLogs).toHaveBeenCalledWith(filters, 2, 25)
})
})
describe('useAuditLog hook', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches a single audit log by UUID', async () => {
vi.mocked(getAuditLog).mockResolvedValue(mockAuditLog)
const { result } = renderHook(() => useAuditLog('test-uuid-123'), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getAuditLog).toHaveBeenCalledWith('test-uuid-123')
expect(result.current.data).toEqual(mockAuditLog)
})
it('does not fetch when uuid is null', () => {
const { result } = renderHook(() => useAuditLog(null), { wrapper: createWrapper() })
expect(result.current.fetchStatus).toBe('idle')
expect(getAuditLog).not.toHaveBeenCalled()
})
})
describe('useAuditLogsByProvider hook', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches audit logs for a provider', async () => {
vi.mocked(getAuditLogsByProvider).mockResolvedValue(mockListResponse)
const { result } = renderHook(() => useAuditLogsByProvider(123), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getAuditLogsByProvider).toHaveBeenCalledWith(123, 1, 50)
expect(result.current.data).toEqual(mockListResponse)
})
it('does not fetch when providerId is null', () => {
const { result } = renderHook(() => useAuditLogsByProvider(null), { wrapper: createWrapper() })
expect(result.current.fetchStatus).toBe('idle')
expect(getAuditLogsByProvider).not.toHaveBeenCalled()
})
it('does not fetch when providerId is 0', () => {
const { result } = renderHook(() => useAuditLogsByProvider(0), { wrapper: createWrapper() })
expect(result.current.fetchStatus).toBe('idle')
expect(getAuditLogsByProvider).not.toHaveBeenCalled()
})
it('fetches with custom pagination', async () => {
vi.mocked(getAuditLogsByProvider).mockResolvedValue(mockListResponse)
const { result } = renderHook(() => useAuditLogsByProvider(456, 3, 100), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getAuditLogsByProvider).toHaveBeenCalledWith(456, 3, 100)
})
})

View File

@@ -1,26 +0,0 @@
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

@@ -1,529 +0,0 @@
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

@@ -1,243 +0,0 @@
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 {
useCredentials,
useCredential,
useCreateCredential,
useUpdateCredential,
useDeleteCredential,
useTestCredential,
useEnableMultiCredentials,
} from '../useCredentials'
import * as credentialsApi from '../../api/credentials'
vi.mock('../../api/credentials')
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
}
describe('useCredentials', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('useCredentials', () => {
it('fetches credentials for a provider', async () => {
const mockCredentials = [
{ id: 1, label: 'Test', zone_filter: 'example.com' },
{ id: 2, label: 'Test2', zone_filter: '*.test.com' },
] as Awaited<ReturnType<typeof credentialsApi.getCredentials>>
vi.mocked(credentialsApi.getCredentials).mockResolvedValue(mockCredentials)
const { result } = renderHook(() => useCredentials(1), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockCredentials)
expect(credentialsApi.getCredentials).toHaveBeenCalledWith(1)
})
it('does not fetch when provider ID is 0', () => {
renderHook(() => useCredentials(0), { wrapper: createWrapper() })
expect(credentialsApi.getCredentials).not.toHaveBeenCalled()
})
it('handles fetch errors', async () => {
vi.mocked(credentialsApi.getCredentials).mockRejectedValue(new Error('Network error'))
const { result } = renderHook(() => useCredentials(1), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isError).toBe(true))
expect(result.current.error).toBeTruthy()
})
})
describe('useCredential', () => {
it('fetches a single credential', async () => {
const mockCredential = { id: 1, label: 'Test', zone_filter: 'example.com' } as Awaited<ReturnType<typeof credentialsApi.getCredential>>
vi.mocked(credentialsApi.getCredential).mockResolvedValue(mockCredential)
const { result } = renderHook(() => useCredential(1, 1), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockCredential)
expect(credentialsApi.getCredential).toHaveBeenCalledWith(1, 1)
})
it('does not fetch when provider or credential ID is 0', () => {
renderHook(() => useCredential(0, 1), { wrapper: createWrapper() })
expect(credentialsApi.getCredential).not.toHaveBeenCalled()
renderHook(() => useCredential(1, 0), { wrapper: createWrapper() })
expect(credentialsApi.getCredential).not.toHaveBeenCalled()
})
})
describe('useCreateCredential', () => {
it('creates a credential and invalidates queries', async () => {
const mockCredential = { id: 3, label: 'New', zone_filter: 'new.com' } as Awaited<ReturnType<typeof credentialsApi.createCredential>>
vi.mocked(credentialsApi.createCredential).mockResolvedValue(mockCredential)
const { result } = renderHook(() => useCreateCredential(), { wrapper: createWrapper() })
const data = {
label: 'New',
zone_filter: 'new.com',
credentials: { api_token: 'test' },
}
await result.current.mutateAsync({ providerId: 1, data })
expect(credentialsApi.createCredential).toHaveBeenCalledWith(1, data)
})
it('handles creation errors', async () => {
vi.mocked(credentialsApi.createCredential).mockRejectedValue(
new Error('Validation failed')
)
const { result } = renderHook(() => useCreateCredential(), { wrapper: createWrapper() })
const data = {
label: '',
zone_filter: '',
credentials: {},
}
await expect(result.current.mutateAsync({ providerId: 1, data })).rejects.toThrow(
'Validation failed'
)
})
})
describe('useUpdateCredential', () => {
it('updates a credential and invalidates queries', async () => {
const mockCredential = { id: 1, label: 'Updated', zone_filter: 'updated.com' } as Awaited<ReturnType<typeof credentialsApi.updateCredential>>
vi.mocked(credentialsApi.updateCredential).mockResolvedValue(mockCredential)
const { result } = renderHook(() => useUpdateCredential(), { wrapper: createWrapper() })
const data = {
label: 'Updated',
zone_filter: 'updated.com',
credentials: { api_token: 'new_token' },
}
await result.current.mutateAsync({ providerId: 1, credentialId: 1, data })
expect(credentialsApi.updateCredential).toHaveBeenCalledWith(1, 1, data)
})
it('handles update errors', async () => {
vi.mocked(credentialsApi.updateCredential).mockRejectedValue(new Error('Not found'))
const { result } = renderHook(() => useUpdateCredential(), { wrapper: createWrapper() })
const data = {
label: 'Updated',
zone_filter: 'updated.com',
credentials: {},
}
await expect(
result.current.mutateAsync({ providerId: 1, credentialId: 999, data })
).rejects.toThrow('Not found')
})
})
describe('useDeleteCredential', () => {
it('deletes a credential and invalidates queries', async () => {
vi.mocked(credentialsApi.deleteCredential).mockResolvedValue()
const { result } = renderHook(() => useDeleteCredential(), { wrapper: createWrapper() })
await result.current.mutateAsync({ providerId: 1, credentialId: 1 })
expect(credentialsApi.deleteCredential).toHaveBeenCalledWith(1, 1)
})
it('handles delete errors', async () => {
vi.mocked(credentialsApi.deleteCredential).mockRejectedValue(
new Error('Credential in use')
)
const { result } = renderHook(() => useDeleteCredential(), { wrapper: createWrapper() })
await expect(
result.current.mutateAsync({ providerId: 1, credentialId: 1 })
).rejects.toThrow('Credential in use')
})
})
describe('useTestCredential', () => {
it('tests a credential successfully', async () => {
const mockResult = { success: true, message: 'Test passed', propagation_time_ms: 1500 }
vi.mocked(credentialsApi.testCredential).mockResolvedValue(mockResult)
const { result } = renderHook(() => useTestCredential(), { wrapper: createWrapper() })
const testResult = await result.current.mutateAsync({ providerId: 1, credentialId: 1 })
expect(credentialsApi.testCredential).toHaveBeenCalledWith(1, 1)
expect(testResult).toEqual(mockResult)
})
it('handles test failures', async () => {
const mockResult = { success: false, error: 'Invalid credentials' }
vi.mocked(credentialsApi.testCredential).mockResolvedValue(mockResult)
const { result } = renderHook(() => useTestCredential(), { wrapper: createWrapper() })
const testResult = await result.current.mutateAsync({ providerId: 1, credentialId: 1 })
expect(testResult.success).toBe(false)
expect(testResult.error).toBe('Invalid credentials')
})
it('handles network errors during test', async () => {
vi.mocked(credentialsApi.testCredential).mockRejectedValue(new Error('Network timeout'))
const { result } = renderHook(() => useTestCredential(), { wrapper: createWrapper() })
await expect(
result.current.mutateAsync({ providerId: 1, credentialId: 1 })
).rejects.toThrow('Network timeout')
})
})
describe('useEnableMultiCredentials', () => {
it('enables multi-credentials and invalidates queries', async () => {
vi.mocked(credentialsApi.enableMultiCredentials).mockResolvedValue()
const { result } = renderHook(() => useEnableMultiCredentials(), {
wrapper: createWrapper(),
})
await result.current.mutateAsync(1)
expect(credentialsApi.enableMultiCredentials).toHaveBeenCalledWith(1)
})
it('handles enable errors', async () => {
vi.mocked(credentialsApi.enableMultiCredentials).mockRejectedValue(
new Error('Already enabled')
)
const { result } = renderHook(() => useEnableMultiCredentials(), {
wrapper: createWrapper(),
})
await expect(result.current.mutateAsync(1)).rejects.toThrow('Already enabled')
})
})
})

View File

@@ -1,204 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useDetectDNSProvider, useCachedDetectionResult, useDetectionPatterns } from '../useDNSDetection'
import * as api from '../../api/dnsDetection'
import type { DetectionResult, NameserverPattern } from '../../api/dnsDetection'
vi.mock('../../api/dnsDetection')
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
return Wrapper
}
describe('useDNSDetection hooks', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('useDetectDNSProvider', () => {
it('should detect DNS provider successfully', async () => {
const mockResult: DetectionResult = {
domain: 'example.com',
detected: true,
provider_type: 'cloudflare',
nameservers: ['ns1.cloudflare.com'],
confidence: 'high',
}
vi.mocked(api.detectDNSProvider).mockResolvedValue(mockResult)
const { result } = renderHook(() => useDetectDNSProvider(), {
wrapper: createWrapper(),
})
result.current.mutate('example.com')
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockResult)
expect(api.detectDNSProvider).toHaveBeenCalledWith('example.com')
})
it('should handle detection error', async () => {
vi.mocked(api.detectDNSProvider).mockRejectedValue(new Error('Network error'))
const { result } = renderHook(() => useDetectDNSProvider(), {
wrapper: createWrapper(),
})
result.current.mutate('example.com')
await waitFor(() => expect(result.current.isError).toBe(true))
expect(result.current.error).toEqual(new Error('Network error'))
})
it('should cache detection result for 1 hour', async () => {
const mockResult: DetectionResult = {
domain: 'example.com',
detected: true,
provider_type: 'cloudflare',
nameservers: ['ns1.cloudflare.com'],
confidence: 'high',
}
vi.mocked(api.detectDNSProvider).mockResolvedValue(mockResult)
const { result } = renderHook(() => useDetectDNSProvider(), {
wrapper: createWrapper(),
})
result.current.mutate('example.com')
await waitFor(() => expect(result.current.isSuccess).toBe(true))
// Result should be cached
expect(result.current.data).toEqual(mockResult)
})
it('should handle not detected scenario', async () => {
const mockResult: DetectionResult = {
domain: 'example.com',
detected: false,
nameservers: ['ns1.unknown.com'],
confidence: 'none',
}
vi.mocked(api.detectDNSProvider).mockResolvedValue(mockResult)
const { result } = renderHook(() => useDetectDNSProvider(), {
wrapper: createWrapper(),
})
result.current.mutate('example.com')
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data?.detected).toBe(false)
})
})
describe('useCachedDetectionResult', () => {
it('should fetch and cache detection result', async () => {
const mockResult: DetectionResult = {
domain: 'example.com',
detected: true,
provider_type: 'cloudflare',
nameservers: ['ns1.cloudflare.com'],
confidence: 'high',
}
vi.mocked(api.detectDNSProvider).mockResolvedValue(mockResult)
const { result } = renderHook(() => useCachedDetectionResult('example.com', true), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockResult)
})
it('should not fetch when disabled', async () => {
const { result } = renderHook(() => useCachedDetectionResult('example.com', false), {
wrapper: createWrapper(),
})
// Wait a bit to ensure no fetch happens
await new Promise(resolve => setTimeout(resolve, 100))
expect(result.current.data).toBeUndefined()
expect(api.detectDNSProvider).not.toHaveBeenCalled()
})
it('should not fetch when domain is empty', async () => {
const { result } = renderHook(() => useCachedDetectionResult('', true), {
wrapper: createWrapper(),
})
await new Promise(resolve => setTimeout(resolve, 100))
expect(result.current.data).toBeUndefined()
expect(api.detectDNSProvider).not.toHaveBeenCalled()
})
})
describe('useDetectionPatterns', () => {
it('should fetch detection patterns successfully', async () => {
const mockPatterns: NameserverPattern[] = [
{ pattern: '.ns.cloudflare.com', provider_type: 'cloudflare' },
{ pattern: '.awsdns', provider_type: 'route53' },
]
vi.mocked(api.getDetectionPatterns).mockResolvedValue(mockPatterns)
const { result } = renderHook(() => useDetectionPatterns(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual(mockPatterns)
expect(result.current.data).toHaveLength(2)
})
it('should cache patterns for 24 hours', async () => {
const mockPatterns: NameserverPattern[] = [
{ pattern: '.ns.cloudflare.com', provider_type: 'cloudflare' },
]
vi.mocked(api.getDetectionPatterns).mockResolvedValue(mockPatterns)
const { result } = renderHook(() => useDetectionPatterns(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
// Should only call API once due to caching
expect(api.getDetectionPatterns).toHaveBeenCalledTimes(1)
})
it('should handle error fetching patterns', async () => {
vi.mocked(api.getDetectionPatterns).mockRejectedValue(new Error('API error'))
const { result } = renderHook(() => useDetectionPatterns(), {
wrapper: createWrapper(),
})
await waitFor(() => expect(result.current.isError).toBe(true))
expect(result.current.error).toEqual(new Error('API error'))
})
})
})

View File

@@ -1,570 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import {
useDNSProviders,
useDNSProvider,
useDNSProviderTypes,
useDNSProviderMutations,
} from '../useDNSProviders'
import * as api from '../../api/dnsProviders'
vi.mock('../../api/dnsProviders')
const mockProvider: api.DNSProvider = {
id: 1,
uuid: 'test-uuid-1',
name: 'Cloudflare Production',
provider_type: 'cloudflare',
enabled: true,
is_default: true,
has_credentials: true,
propagation_timeout: 120,
polling_interval: 2,
success_count: 5,
failure_count: 0,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockProviderType: api.DNSProviderTypeInfo = {
type: 'cloudflare',
name: 'Cloudflare',
fields: [
{
name: 'api_token',
label: 'API Token',
type: 'password',
required: true,
},
],
documentation_url: 'https://developers.cloudflare.com/api/',
}
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useDNSProviders', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns providers list on mount', async () => {
const mockProviders = [mockProvider, { ...mockProvider, id: 2, name: 'Secondary' }]
vi.mocked(api.getDNSProviders).mockResolvedValue(mockProviders)
const { result } = renderHook(() => useDNSProviders(), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
expect(result.current.data).toBeUndefined()
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockProviders)
expect(result.current.isError).toBe(false)
expect(api.getDNSProviders).toHaveBeenCalledTimes(1)
})
it('handles loading state during fetch', async () => {
vi.mocked(api.getDNSProviders).mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve([mockProvider]), 100))
)
const { result } = renderHook(() => useDNSProviders(), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
expect(result.current.data).toBeUndefined()
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual([mockProvider])
})
it('handles error state on failure', async () => {
const mockError = new Error('Failed to fetch providers')
vi.mocked(api.getDNSProviders).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviders(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
expect(result.current.data).toBeUndefined()
})
it('uses correct query key', async () => {
vi.mocked(api.getDNSProviders).mockResolvedValue([mockProvider])
const { result } = renderHook(() => useDNSProviders(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
// Query key should be consistent for cache management
expect(api.getDNSProviders).toHaveBeenCalled()
})
})
describe('useDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches single provider when id > 0', async () => {
vi.mocked(api.getDNSProvider).mockResolvedValue(mockProvider)
const { result } = renderHook(() => useDNSProvider(1), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockProvider)
expect(api.getDNSProvider).toHaveBeenCalledWith(1)
})
it('is disabled when id = 0', async () => {
vi.mocked(api.getDNSProvider).mockResolvedValue(mockProvider)
const { result } = renderHook(() => useDNSProvider(0), { wrapper: createWrapper() })
// Should not fetch when disabled
expect(result.current.isLoading).toBe(false)
expect(result.current.data).toBeUndefined()
expect(api.getDNSProvider).not.toHaveBeenCalled()
})
it('is disabled when id < 0', async () => {
vi.mocked(api.getDNSProvider).mockResolvedValue(mockProvider)
const { result } = renderHook(() => useDNSProvider(-1), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(false)
expect(result.current.data).toBeUndefined()
expect(api.getDNSProvider).not.toHaveBeenCalled()
})
it('handles loading state', async () => {
vi.mocked(api.getDNSProvider).mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve(mockProvider), 100))
)
const { result } = renderHook(() => useDNSProvider(1), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
})
it('handles error state', async () => {
const mockError = new Error('Provider not found')
vi.mocked(api.getDNSProvider).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProvider(999), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useDNSProviderTypes', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches types list', async () => {
const mockTypes = [
mockProviderType,
{ ...mockProviderType, type: 'route53' as const, name: 'AWS Route 53' },
]
vi.mocked(api.getDNSProviderTypes).mockResolvedValue(mockTypes)
const { result } = renderHook(() => useDNSProviderTypes(), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockTypes)
expect(api.getDNSProviderTypes).toHaveBeenCalledTimes(1)
})
it('applies staleTime of 1 hour', async () => {
vi.mocked(api.getDNSProviderTypes).mockResolvedValue([mockProviderType])
const { result } = renderHook(() => useDNSProviderTypes(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
// The staleTime is configured in the hook, data should be cached for 1 hour
expect(result.current.data).toEqual([mockProviderType])
})
it('handles loading state', async () => {
vi.mocked(api.getDNSProviderTypes).mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve([mockProviderType]), 100))
)
const { result } = renderHook(() => useDNSProviderTypes(), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
})
it('handles error state', async () => {
const mockError = new Error('Failed to fetch types')
vi.mocked(api.getDNSProviderTypes).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderTypes(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useDNSProviderMutations', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('createMutation', () => {
it('creates provider successfully', async () => {
const newProvider = { ...mockProvider, id: 3, name: 'New Provider' }
vi.mocked(api.createDNSProvider).mockResolvedValue(newProvider)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
const createData: api.DNSProviderRequest = {
name: 'New Provider',
provider_type: 'cloudflare',
credentials: { api_token: 'test-token' },
}
result.current.createMutation.mutate(createData)
await waitFor(() => {
expect(result.current.createMutation.isSuccess).toBe(true)
})
expect(api.createDNSProvider).toHaveBeenCalledWith(createData)
expect(result.current.createMutation.data).toEqual(newProvider)
})
it('invalidates list query on success', async () => {
vi.mocked(api.createDNSProvider).mockResolvedValue(mockProvider)
vi.mocked(api.getDNSProviders).mockResolvedValue([mockProvider])
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useDNSProviderMutations(), { wrapper })
result.current.createMutation.mutate({
name: 'Test',
provider_type: 'cloudflare',
credentials: {},
})
await waitFor(() => {
expect(result.current.createMutation.isSuccess).toBe(true)
})
expect(invalidateSpy).toHaveBeenCalled()
})
it('handles creation errors', async () => {
const mockError = new Error('Creation failed')
vi.mocked(api.createDNSProvider).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.createMutation.mutate({
name: 'Test',
provider_type: 'cloudflare',
credentials: {},
})
await waitFor(() => {
expect(result.current.createMutation.isError).toBe(true)
})
expect(result.current.createMutation.error).toEqual(mockError)
})
})
describe('updateMutation', () => {
it('updates provider successfully', async () => {
const updatedProvider = { ...mockProvider, name: 'Updated Name' }
vi.mocked(api.updateDNSProvider).mockResolvedValue(updatedProvider)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
const updateData: api.DNSProviderRequest = {
name: 'Updated Name',
provider_type: 'cloudflare',
credentials: { api_token: 'new-token' },
}
result.current.updateMutation.mutate({ id: 1, data: updateData })
await waitFor(() => {
expect(result.current.updateMutation.isSuccess).toBe(true)
})
expect(api.updateDNSProvider).toHaveBeenCalledWith(1, updateData)
expect(result.current.updateMutation.data).toEqual(updatedProvider)
})
it('invalidates list and detail queries on success', async () => {
vi.mocked(api.updateDNSProvider).mockResolvedValue(mockProvider)
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useDNSProviderMutations(), { wrapper })
result.current.updateMutation.mutate({
id: 1,
data: {
name: 'Updated',
provider_type: 'cloudflare',
credentials: {},
},
})
await waitFor(() => {
expect(result.current.updateMutation.isSuccess).toBe(true)
})
// Should invalidate both list and detail queries
expect(invalidateSpy).toHaveBeenCalledTimes(2)
})
it('handles update errors', async () => {
const mockError = new Error('Update failed')
vi.mocked(api.updateDNSProvider).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.updateMutation.mutate({
id: 1,
data: {
name: 'Test',
provider_type: 'cloudflare',
credentials: {},
},
})
await waitFor(() => {
expect(result.current.updateMutation.isError).toBe(true)
})
expect(result.current.updateMutation.error).toEqual(mockError)
})
})
describe('deleteMutation', () => {
it('deletes provider successfully', async () => {
vi.mocked(api.deleteDNSProvider).mockResolvedValue(undefined)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.deleteMutation.mutate(1)
await waitFor(() => {
expect(result.current.deleteMutation.isSuccess).toBe(true)
})
expect(api.deleteDNSProvider).toHaveBeenCalledWith(1)
})
it('invalidates list query on success', async () => {
vi.mocked(api.deleteDNSProvider).mockResolvedValue(undefined)
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useDNSProviderMutations(), { wrapper })
result.current.deleteMutation.mutate(1)
await waitFor(() => {
expect(result.current.deleteMutation.isSuccess).toBe(true)
})
expect(invalidateSpy).toHaveBeenCalled()
})
it('handles delete errors', async () => {
const mockError = new Error('Delete failed')
vi.mocked(api.deleteDNSProvider).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.deleteMutation.mutate(1)
await waitFor(() => {
expect(result.current.deleteMutation.isError).toBe(true)
})
expect(result.current.deleteMutation.error).toEqual(mockError)
})
})
describe('testMutation', () => {
it('tests provider successfully and returns result', async () => {
const testResult: api.DNSTestResult = {
success: true,
message: 'Test successful',
propagation_time_ms: 1200,
}
vi.mocked(api.testDNSProvider).mockResolvedValue(testResult)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.testMutation.mutate(1)
await waitFor(() => {
expect(result.current.testMutation.isSuccess).toBe(true)
})
expect(api.testDNSProvider).toHaveBeenCalledWith(1)
expect(result.current.testMutation.data).toEqual(testResult)
})
it('handles test errors', async () => {
const mockError = new Error('Test failed')
vi.mocked(api.testDNSProvider).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.testMutation.mutate(1)
await waitFor(() => {
expect(result.current.testMutation.isError).toBe(true)
})
expect(result.current.testMutation.error).toEqual(mockError)
})
})
describe('testCredentialsMutation', () => {
it('tests credentials successfully and returns result', async () => {
const testResult: api.DNSTestResult = {
success: true,
message: 'Credentials valid',
propagation_time_ms: 800,
}
vi.mocked(api.testDNSProviderCredentials).mockResolvedValue(testResult)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
const testData: api.DNSProviderRequest = {
name: 'Test',
provider_type: 'cloudflare',
credentials: { api_token: 'test' },
}
result.current.testCredentialsMutation.mutate(testData)
await waitFor(() => {
expect(result.current.testCredentialsMutation.isSuccess).toBe(true)
})
expect(api.testDNSProviderCredentials).toHaveBeenCalledWith(testData)
expect(result.current.testCredentialsMutation.data).toEqual(testResult)
})
it('handles test credential errors', async () => {
const mockError = new Error('Invalid credentials')
vi.mocked(api.testDNSProviderCredentials).mockRejectedValue(mockError)
const { result } = renderHook(() => useDNSProviderMutations(), {
wrapper: createWrapper(),
})
result.current.testCredentialsMutation.mutate({
name: 'Test',
provider_type: 'cloudflare',
credentials: {},
})
await waitFor(() => {
expect(result.current.testCredentialsMutation.isError).toBe(true)
})
expect(result.current.testCredentialsMutation.error).toEqual(mockError)
})
})
})

View File

@@ -1,173 +0,0 @@
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('extracts details from 503 service unavailable error', async () => {
const mockError = {
response: {
status: 503,
data: {
error: 'Docker daemon unavailable',
details: 'Cannot connect to Docker. Please ensure Docker is running and the socket is accessible (e.g., /var/run/docker.sock is mounted).'
}
}
};
vi.mocked(dockerApi.listContainers).mockRejectedValue(mockError);
const { result } = renderHook(() => useDocker('local'), {
wrapper: createWrapper(),
});
await waitFor(
() => {
expect(result.current.isLoading).toBe(false);
},
{ timeout: 3000 }
);
// Verify error message includes the details
expect(result.current.error).toBeTruthy();
const errorMessage = (result.current.error as Error)?.message;
expect(errorMessage).toContain('Docker is running');
});
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

@@ -1,143 +0,0 @@
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

@@ -1,348 +0,0 @@
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

@@ -1,89 +0,0 @@
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

@@ -1,248 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor, act } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useManualChallenge, useChallengePoll, useManualChallengeMutations } from '../useManualChallenge'
import * as api from '../../api/manualChallenge'
vi.mock('../../api/manualChallenge')
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useManualChallenge hooks', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('useManualChallenge', () => {
it('fetches challenge by ID when enabled', async () => {
const mockChallenge = {
id: 'test-uuid',
status: 'pending' as const,
fqdn: '_acme-challenge.example.com',
value: 'test-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(api.getChallenge).mockResolvedValueOnce(mockChallenge)
const { result } = renderHook(
() => useManualChallenge(1, 'test-uuid'),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(api.getChallenge).toHaveBeenCalledWith(1, 'test-uuid')
expect(result.current.data).toEqual(mockChallenge)
})
it('does not fetch when providerId is 0', async () => {
const { result } = renderHook(
() => useManualChallenge(0, 'test-uuid'),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isLoading).toBe(false))
expect(api.getChallenge).not.toHaveBeenCalled()
})
it('does not fetch when challengeId is empty', async () => {
const { result } = renderHook(
() => useManualChallenge(1, ''),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isLoading).toBe(false))
expect(api.getChallenge).not.toHaveBeenCalled()
})
})
describe('useChallengePoll', () => {
it('fetches poll data when enabled', async () => {
const mockPoll = {
status: 'pending' as const,
dns_propagated: false,
time_remaining_seconds: 480,
last_check_at: '2026-01-11T00:02:00Z',
}
vi.mocked(api.pollChallenge).mockResolvedValue(mockPoll)
const { result } = renderHook(
() => useChallengePoll(1, 'test-uuid', true),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(api.pollChallenge).toHaveBeenCalledWith(1, 'test-uuid')
expect(result.current.data).toEqual(mockPoll)
})
it('does not fetch when disabled', async () => {
const { result } = renderHook(
() => useChallengePoll(1, 'test-uuid', false),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isLoading).toBe(false))
expect(api.pollChallenge).not.toHaveBeenCalled()
})
it('uses custom refetch interval', async () => {
const mockPoll = {
status: 'pending' as const,
dns_propagated: false,
time_remaining_seconds: 480,
last_check_at: '2026-01-11T00:02:00Z',
}
vi.mocked(api.pollChallenge).mockResolvedValue(mockPoll)
const { result } = renderHook(
() => useChallengePoll(1, 'test-uuid', true, 5000),
{ wrapper: createWrapper() }
)
await waitFor(() => expect(result.current.isSuccess).toBe(true))
// Hook configured with 5 second interval
expect(result.current.data).toEqual(mockPoll)
})
})
describe('useManualChallengeMutations', () => {
describe('createMutation', () => {
it('creates a new challenge', async () => {
const mockChallenge = {
id: 'new-uuid',
status: 'created' as const,
fqdn: '_acme-challenge.example.com',
value: 'generated-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(api.createChallenge).mockResolvedValueOnce(mockChallenge)
const { result } = renderHook(
() => useManualChallengeMutations(),
{ wrapper: createWrapper() }
)
await act(async () => {
const response = await result.current.createMutation.mutateAsync({
providerId: 1,
data: { domain: 'example.com' },
})
expect(response).toEqual(mockChallenge)
})
expect(api.createChallenge).toHaveBeenCalledWith(1, { domain: 'example.com' })
})
})
describe('verifyMutation', () => {
it('verifies a challenge', async () => {
const mockResult = {
success: true,
dns_found: true,
message: 'TXT record verified',
}
vi.mocked(api.verifyChallenge).mockResolvedValueOnce(mockResult)
const { result } = renderHook(
() => useManualChallengeMutations(),
{ wrapper: createWrapper() }
)
await act(async () => {
const response = await result.current.verifyMutation.mutateAsync({
providerId: 1,
challengeId: 'test-uuid',
})
expect(response).toEqual(mockResult)
})
expect(api.verifyChallenge).toHaveBeenCalledWith(1, 'test-uuid')
})
it('handles verification failure', async () => {
vi.mocked(api.verifyChallenge).mockRejectedValueOnce(new Error('Network error'))
const { result } = renderHook(
() => useManualChallengeMutations(),
{ wrapper: createWrapper() }
)
await expect(
act(() =>
result.current.verifyMutation.mutateAsync({
providerId: 1,
challengeId: 'test-uuid',
})
)
).rejects.toThrow('Network error')
})
})
describe('deleteMutation', () => {
it('deletes a challenge', async () => {
vi.mocked(api.deleteChallenge).mockResolvedValueOnce(undefined)
const { result } = renderHook(
() => useManualChallengeMutations(),
{ wrapper: createWrapper() }
)
await act(async () => {
await result.current.deleteMutation.mutateAsync({
providerId: 1,
challengeId: 'test-uuid',
})
})
expect(api.deleteChallenge).toHaveBeenCalledWith(1, 'test-uuid')
})
it('handles deletion failure', async () => {
vi.mocked(api.deleteChallenge).mockRejectedValueOnce(
new Error('Challenge not found')
)
const { result } = renderHook(
() => useManualChallengeMutations(),
{ wrapper: createWrapper() }
)
await expect(
act(() =>
result.current.deleteMutation.mutateAsync({
providerId: 1,
challengeId: 'invalid-uuid',
})
)
).rejects.toThrow('Challenge not found')
})
})
})
})

View File

@@ -1,251 +0,0 @@
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

@@ -1,434 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import {
usePlugins,
usePlugin,
useProviderFields,
useEnablePlugin,
useDisablePlugin,
useReloadPlugins,
} from '../usePlugins'
import * as api from '../../api/plugins'
vi.mock('../../api/plugins')
const mockBuiltInPlugin: api.PluginInfo = {
id: 1,
uuid: 'builtin-cloudflare',
name: 'Cloudflare',
type: 'cloudflare',
enabled: true,
status: 'loaded',
is_built_in: true,
version: '1.0.0',
description: 'Cloudflare DNS provider',
documentation_url: 'https://developers.cloudflare.com',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockExternalPlugin: api.PluginInfo = {
id: 2,
uuid: 'external-powerdns',
name: 'PowerDNS',
type: 'powerdns',
enabled: true,
status: 'loaded',
is_built_in: false,
version: '1.0.0',
author: 'Community',
description: 'PowerDNS provider plugin',
documentation_url: 'https://doc.powerdns.com',
loaded_at: '2025-01-06T00:00:00Z',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-06T00:00:00Z',
}
const mockProviderFields: api.ProviderFieldsResponse = {
type: 'powerdns',
name: 'PowerDNS',
required_fields: [
{
name: 'api_url',
label: 'API URL',
type: 'text',
placeholder: 'https://pdns.example.com:8081',
hint: 'PowerDNS HTTP API endpoint',
required: true,
},
{
name: 'api_key',
label: 'API Key',
type: 'password',
placeholder: 'Your API key',
hint: 'X-API-Key header value',
required: true,
},
],
optional_fields: [
{
name: 'server_id',
label: 'Server ID',
type: 'text',
placeholder: 'localhost',
hint: 'PowerDNS server ID',
required: false,
},
],
}
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('usePlugins', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns plugins list on mount', async () => {
const mockPlugins = [mockBuiltInPlugin, mockExternalPlugin]
vi.mocked(api.getPlugins).mockResolvedValue(mockPlugins)
const { result } = renderHook(() => usePlugins(), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
expect(result.current.data).toBeUndefined()
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockPlugins)
expect(result.current.isError).toBe(false)
expect(api.getPlugins).toHaveBeenCalledTimes(1)
})
it('handles empty plugins list', async () => {
vi.mocked(api.getPlugins).mockResolvedValue([])
const { result } = renderHook(() => usePlugins(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual([])
expect(result.current.isError).toBe(false)
})
it('handles error state on failure', async () => {
const mockError = new Error('Failed to fetch plugins')
vi.mocked(api.getPlugins).mockRejectedValue(mockError)
const { result } = renderHook(() => usePlugins(), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
expect(result.current.data).toBeUndefined()
})
})
describe('usePlugin', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches single plugin when id > 0', async () => {
vi.mocked(api.getPlugin).mockResolvedValue(mockExternalPlugin)
const { result } = renderHook(() => usePlugin(2), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockExternalPlugin)
expect(api.getPlugin).toHaveBeenCalledWith(2)
})
it('is disabled when id = 0', async () => {
vi.mocked(api.getPlugin).mockResolvedValue(mockExternalPlugin)
const { result } = renderHook(() => usePlugin(0), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(false)
expect(result.current.data).toBeUndefined()
expect(api.getPlugin).not.toHaveBeenCalled()
})
it('handles error state', async () => {
const mockError = new Error('Plugin not found')
vi.mocked(api.getPlugin).mockRejectedValue(mockError)
const { result } = renderHook(() => usePlugin(999), { wrapper: createWrapper() })
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useProviderFields', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches provider credential fields', async () => {
vi.mocked(api.getProviderFields).mockResolvedValue(mockProviderFields)
const { result } = renderHook(() => useProviderFields('powerdns'), {
wrapper: createWrapper(),
})
expect(result.current.isLoading).toBe(true)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.data).toEqual(mockProviderFields)
expect(api.getProviderFields).toHaveBeenCalledWith('powerdns')
})
it('is disabled when providerType is empty', async () => {
vi.mocked(api.getProviderFields).mockResolvedValue(mockProviderFields)
const { result } = renderHook(() => useProviderFields(''), { wrapper: createWrapper() })
expect(result.current.isLoading).toBe(false)
expect(result.current.data).toBeUndefined()
expect(api.getProviderFields).not.toHaveBeenCalled()
})
it('applies staleTime of 1 hour', async () => {
vi.mocked(api.getProviderFields).mockResolvedValue(mockProviderFields)
const { result } = renderHook(() => useProviderFields('powerdns'), {
wrapper: createWrapper(),
})
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
// The staleTime is configured in the hook, data should be cached for 1 hour
expect(result.current.data).toEqual(mockProviderFields)
})
it('handles error state', async () => {
const mockError = new Error('Provider type not found')
vi.mocked(api.getProviderFields).mockRejectedValue(mockError)
const { result } = renderHook(() => useProviderFields('invalid'), {
wrapper: createWrapper(),
})
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useEnablePlugin', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('enables plugin successfully', async () => {
const mockResponse = { message: 'Plugin enabled successfully' }
vi.mocked(api.enablePlugin).mockResolvedValue(mockResponse)
const { result } = renderHook(() => useEnablePlugin(), { wrapper: createWrapper() })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(api.enablePlugin).toHaveBeenCalledWith(2)
expect(result.current.data).toEqual(mockResponse)
})
it('invalidates plugins list on success', async () => {
vi.mocked(api.enablePlugin).mockResolvedValue({ message: 'Enabled' })
vi.mocked(api.getPlugins).mockResolvedValue([mockExternalPlugin])
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useEnablePlugin(), { wrapper })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(invalidateSpy).toHaveBeenCalled()
})
it('handles enable errors', async () => {
const mockError = new Error('Failed to enable plugin')
vi.mocked(api.enablePlugin).mockRejectedValue(mockError)
const { result } = renderHook(() => useEnablePlugin(), { wrapper: createWrapper() })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useDisablePlugin', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('disables plugin successfully', async () => {
const mockResponse = { message: 'Plugin disabled successfully' }
vi.mocked(api.disablePlugin).mockResolvedValue(mockResponse)
const { result } = renderHook(() => useDisablePlugin(), { wrapper: createWrapper() })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(api.disablePlugin).toHaveBeenCalledWith(2)
expect(result.current.data).toEqual(mockResponse)
})
it('invalidates plugins list on success', async () => {
vi.mocked(api.disablePlugin).mockResolvedValue({ message: 'Disabled' })
vi.mocked(api.getPlugins).mockResolvedValue([mockExternalPlugin])
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useDisablePlugin(), { wrapper })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(invalidateSpy).toHaveBeenCalled()
})
it('handles disable errors', async () => {
const mockError = new Error('Cannot disable: plugin in use')
vi.mocked(api.disablePlugin).mockRejectedValue(mockError)
const { result } = renderHook(() => useDisablePlugin(), { wrapper: createWrapper() })
result.current.mutate(2)
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})
describe('useReloadPlugins', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('reloads plugins successfully', async () => {
const mockResponse = { message: 'Plugins reloaded', count: 3 }
vi.mocked(api.reloadPlugins).mockResolvedValue(mockResponse)
const { result } = renderHook(() => useReloadPlugins(), { wrapper: createWrapper() })
result.current.mutate()
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(api.reloadPlugins).toHaveBeenCalledTimes(1)
expect(result.current.data).toEqual(mockResponse)
})
it('invalidates plugins list on success', async () => {
vi.mocked(api.reloadPlugins).mockResolvedValue({ message: 'Reloaded', count: 2 })
vi.mocked(api.getPlugins).mockResolvedValue([mockBuiltInPlugin, mockExternalPlugin])
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useReloadPlugins(), { wrapper })
result.current.mutate()
await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})
expect(invalidateSpy).toHaveBeenCalled()
})
it('handles reload errors', async () => {
const mockError = new Error('Failed to reload plugins')
vi.mocked(api.reloadPlugins).mockRejectedValue(mockError)
const { result } = renderHook(() => useReloadPlugins(), { wrapper: createWrapper() })
result.current.mutate()
await waitFor(() => {
expect(result.current.isError).toBe(true)
})
expect(result.current.error).toEqual(mockError)
})
})

View File

@@ -1,159 +0,0 @@
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

@@ -1,200 +0,0 @@
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

@@ -1,242 +0,0 @@
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

@@ -1,298 +0,0 @@
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

@@ -1,301 +0,0 @@
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

@@ -1,17 +0,0 @@
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()
})
})