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

View File

@@ -1,82 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { accessListsApi, type CreateAccessListRequest } from '../api/accessLists';
import toast from 'react-hot-toast';
export function useAccessLists() {
return useQuery({
queryKey: ['accessLists'],
queryFn: accessListsApi.list,
});
}
export function useAccessList(id: number | undefined) {
return useQuery({
queryKey: ['accessList', id],
queryFn: () => accessListsApi.get(id!),
enabled: !!id,
});
}
export function useAccessListTemplates() {
return useQuery({
queryKey: ['accessListTemplates'],
queryFn: accessListsApi.getTemplates,
});
}
export function useCreateAccessList() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateAccessListRequest) => accessListsApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['accessLists'] });
toast.success('Access list created successfully');
},
onError: (error: Error) => {
toast.error(`Failed to create access list: ${error.message}`);
},
});
}
export function useUpdateAccessList() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<CreateAccessListRequest> }) =>
accessListsApi.update(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['accessLists'] });
queryClient.invalidateQueries({ queryKey: ['accessList', variables.id] });
toast.success('Access list updated successfully');
},
onError: (error: Error) => {
toast.error(`Failed to update access list: ${error.message}`);
},
});
}
export function useDeleteAccessList() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => accessListsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['accessLists'] });
toast.success('Access list deleted successfully');
},
onError: (error: Error) => {
toast.error(`Failed to delete access list: ${error.message}`);
},
});
}
export function useTestIP() {
return useMutation({
mutationFn: ({ id, ipAddress }: { id: number; ipAddress: string }) =>
accessListsApi.testIP(id, ipAddress),
onError: (error: Error) => {
toast.error(`Failed to test IP: ${error.message}`);
},
});
}

View File

@@ -1,76 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import {
getAuditLogs,
getAuditLog,
getAuditLogsByProvider,
type AuditLog,
type AuditLogFilters,
} from '../api/auditLogs'
/** Query key factory for audit logs */
const queryKeys = {
all: ['audit-logs'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters?: AuditLogFilters, page?: number, limit?: number) =>
[...queryKeys.lists(), filters, page, limit] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (uuid: string) => [...queryKeys.details(), uuid] as const,
byProvider: (providerId: number, page?: number, limit?: number) =>
[...queryKeys.all, 'provider', providerId, page, limit] as const,
}
/**
* Hook for fetching audit logs with pagination and filtering.
* @param filters - Optional filters to apply
* @param page - Page number (1-indexed)
* @param limit - Number of records per page
* @returns Query result with paginated audit logs
*/
export function useAuditLogs(
filters?: AuditLogFilters,
page: number = 1,
limit: number = 50
) {
return useQuery({
queryKey: queryKeys.list(filters, page, limit),
queryFn: () => getAuditLogs(filters, page, limit),
staleTime: 1000 * 30, // 30 seconds - audit logs are relatively static
placeholderData: (previousData) => previousData, // Keep previous data while fetching new page
})
}
/**
* Hook for fetching a single audit log.
* @param uuid - Audit log UUID
* @returns Query result with audit log data
*/
export function useAuditLog(uuid: string | null) {
return useQuery({
queryKey: queryKeys.detail(uuid || ''),
queryFn: () => getAuditLog(uuid!),
enabled: !!uuid,
})
}
/**
* Hook for fetching audit logs for a specific DNS provider.
* @param providerId - DNS provider ID
* @param page - Page number (1-indexed)
* @param limit - Number of records per page
* @returns Query result with paginated audit logs
*/
export function useAuditLogsByProvider(
providerId: number | null,
page: number = 1,
limit: number = 50
) {
return useQuery({
queryKey: queryKeys.byProvider(providerId || 0, page, limit),
queryFn: () => getAuditLogsByProvider(providerId!, page, limit),
enabled: providerId !== null && providerId > 0,
staleTime: 1000 * 30,
placeholderData: (previousData) => previousData,
})
}
export type { AuditLog, AuditLogFilters }

View File

@@ -1,10 +0,0 @@
import { useContext } from 'react';
import { AuthContext } from '../context/AuthContextValue';
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -1,21 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { getCertificates } from '../api/certificates'
interface UseCertificatesOptions {
refetchInterval?: number | false
}
export function useCertificates(options?: UseCertificatesOptions) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['certificates'],
queryFn: getCertificates,
refetchInterval: options?.refetchInterval,
})
return {
certificates: data || [],
isLoading,
error,
refetch,
}
}

View File

@@ -1,27 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { enrollConsole, getConsoleStatus, clearConsoleEnrollment, type ConsoleEnrollPayload, type ConsoleEnrollmentStatus } from '../api/consoleEnrollment'
export function useConsoleStatus(enabled = true) {
return useQuery<ConsoleEnrollmentStatus>({ queryKey: ['crowdsec-console-status'], queryFn: getConsoleStatus, enabled })
}
export function useEnrollConsole() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: ConsoleEnrollPayload) => enrollConsole(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['crowdsec-console-status'] })
},
})
}
export function useClearConsoleEnrollment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: clearConsoleEnrollment,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['crowdsec-console-status'] })
},
})
}

View File

@@ -1,148 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getCredentials,
getCredential,
createCredential,
updateCredential,
deleteCredential,
testCredential,
enableMultiCredentials,
type DNSProviderCredential,
type CredentialRequest,
type CredentialTestResult,
} from '../api/credentials'
/** Query key factory for credentials */
export const credentialQueryKeys = {
all: ['credentials'] as const,
byProvider: (providerId: number) => [...credentialQueryKeys.all, 'provider', providerId] as const,
detail: (providerId: number, credentialId: number) =>
[...credentialQueryKeys.all, 'provider', providerId, 'detail', credentialId] as const,
}
/**
* Hook for fetching all credentials for a DNS provider.
* @param providerId - DNS provider ID
* @returns Query result with credentials array
*/
export function useCredentials(providerId: number) {
return useQuery({
queryKey: credentialQueryKeys.byProvider(providerId),
queryFn: () => getCredentials(providerId),
enabled: providerId > 0,
})
}
/**
* Hook for fetching a single credential.
* @param providerId - DNS provider ID
* @param credentialId - Credential ID
* @returns Query result with credential data
*/
export function useCredential(providerId: number, credentialId: number) {
return useQuery({
queryKey: credentialQueryKeys.detail(providerId, credentialId),
queryFn: () => getCredential(providerId, credentialId),
enabled: providerId > 0 && credentialId > 0,
})
}
/**
* Hook for creating a new credential.
* @returns Mutation function for creating credentials
*/
export function useCreateCredential() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ providerId, data }: { providerId: number; data: CredentialRequest }) =>
createCredential(providerId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: credentialQueryKeys.byProvider(variables.providerId),
})
},
})
}
/**
* Hook for updating an existing credential.
* @returns Mutation function for updating credentials
*/
export function useUpdateCredential() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({
providerId,
credentialId,
data,
}: {
providerId: number
credentialId: number
data: CredentialRequest
}) => updateCredential(providerId, credentialId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: credentialQueryKeys.byProvider(variables.providerId),
})
queryClient.invalidateQueries({
queryKey: credentialQueryKeys.detail(variables.providerId, variables.credentialId),
})
},
})
}
/**
* Hook for deleting a credential.
* @returns Mutation function for deleting credentials
*/
export function useDeleteCredential() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ providerId, credentialId }: { providerId: number; credentialId: number }) =>
deleteCredential(providerId, credentialId),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: credentialQueryKeys.byProvider(variables.providerId),
})
},
})
}
/**
* Hook for testing a credential.
* @returns Mutation function for testing credentials
*/
export function useTestCredential() {
return useMutation({
mutationFn: ({ providerId, credentialId }: { providerId: number; credentialId: number }) =>
testCredential(providerId, credentialId),
})
}
/**
* Hook for enabling multi-credential mode.
* @returns Mutation function for enabling multi-credential mode
*/
export function useEnableMultiCredentials() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (providerId: number) => enableMultiCredentials(providerId),
onSuccess: (_, providerId) => {
// Invalidate DNS provider queries to refresh use_multi_credentials flag
queryClient.invalidateQueries({ queryKey: ['dns-providers'] })
queryClient.invalidateQueries({
queryKey: credentialQueryKeys.byProvider(providerId),
})
},
})
}
export type {
DNSProviderCredential,
CredentialRequest,
CredentialTestResult,
}

View File

@@ -1,65 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
detectDNSProvider,
getDetectionPatterns,
type DetectionResult,
type NameserverPattern,
} from '../api/dnsDetection'
/** Query key factory for DNS detection */
const queryKeys = {
all: ['dns-detection'] as const,
results: () => [...queryKeys.all, 'results'] as const,
result: (domain: string) => [...queryKeys.results(), domain] as const,
patterns: () => [...queryKeys.all, 'patterns'] as const,
}
/**
* Hook for detecting DNS provider for a domain.
* Results are cached for 1 hour per domain.
* @returns Mutation function with detection result
*/
export function useDetectDNSProvider() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (domain: string) => detectDNSProvider(domain),
onSuccess: (result, domain) => {
// Cache result for 1 hour
queryClient.setQueryData(queryKeys.result(domain), result, {
updatedAt: Date.now(),
})
},
})
}
/**
* Hook for fetching cached detection result for a domain.
* @param domain - Domain to get cached result for
* @returns Query result with detection data
*/
export function useCachedDetectionResult(domain: string, enabled = false) {
return useQuery({
queryKey: queryKeys.result(domain),
queryFn: () => detectDNSProvider(domain),
enabled: enabled && !!domain,
staleTime: 60 * 60 * 1000, // 1 hour
gcTime: 60 * 60 * 1000, // Keep cached for 1 hour
})
}
/**
* Hook for fetching nameserver detection patterns.
* Patterns are cached for 24 hours as they rarely change.
* @returns Query result with patterns array
*/
export function useDetectionPatterns() {
return useQuery({
queryKey: queryKeys.patterns(),
queryFn: getDetectionPatterns,
staleTime: 24 * 60 * 60 * 1000, // 24 hours
gcTime: 24 * 60 * 60 * 1000,
})
}
export type { DetectionResult, NameserverPattern }

View File

@@ -1,117 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getDNSProviders,
getDNSProvider,
getDNSProviderTypes,
createDNSProvider,
updateDNSProvider,
deleteDNSProvider,
testDNSProvider,
testDNSProviderCredentials,
type DNSProvider,
type DNSProviderRequest,
type DNSProviderTypeInfo,
type DNSProviderField,
type DNSTestResult,
} from '../api/dnsProviders'
/** Query key factory for DNS providers */
const queryKeys = {
all: ['dns-providers'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: () => [...queryKeys.lists()] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
types: () => [...queryKeys.all, 'types'] as const,
}
/**
* Hook for fetching all DNS providers.
* @returns Query result with providers array
*/
export function useDNSProviders() {
return useQuery({
queryKey: queryKeys.list(),
queryFn: getDNSProviders,
})
}
/**
* Hook for fetching a single DNS provider.
* @param id - DNS provider ID
* @returns Query result with provider data
*/
export function useDNSProvider(id: number) {
return useQuery({
queryKey: queryKeys.detail(id),
queryFn: () => getDNSProvider(id),
enabled: id > 0,
})
}
/**
* Hook for fetching supported DNS provider types.
* @returns Query result with provider types array
*/
export function useDNSProviderTypes() {
return useQuery({
queryKey: queryKeys.types(),
queryFn: getDNSProviderTypes,
staleTime: 1000 * 60 * 60, // 1 hour - types rarely change
})
}
/**
* Hook providing DNS provider mutation operations.
* @returns Object with mutation functions for create, update, delete, and test
*/
export function useDNSProviderMutations() {
const queryClient = useQueryClient()
const createMutation = useMutation({
mutationFn: (data: DNSProviderRequest) => createDNSProvider(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
},
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: DNSProviderRequest }) =>
updateDNSProvider(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
queryClient.invalidateQueries({ queryKey: queryKeys.detail(variables.id) })
},
})
const deleteMutation = useMutation({
mutationFn: (id: number) => deleteDNSProvider(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
},
})
const testMutation = useMutation({
mutationFn: (id: number) => testDNSProvider(id),
})
const testCredentialsMutation = useMutation({
mutationFn: (data: DNSProviderRequest) => testDNSProviderCredentials(data),
})
return {
createMutation,
updateMutation,
deleteMutation,
testMutation,
testCredentialsMutation,
}
}
export type {
DNSProvider,
DNSProviderRequest,
DNSProviderTypeInfo,
DNSProviderField,
DNSTestResult,
}

View File

@@ -1,36 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { dockerApi } from '../api/docker'
export function useDocker(host?: string | null, serverId?: string | null) {
const {
data: containers = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ['docker-containers', host, serverId],
queryFn: async () => {
try {
return await dockerApi.listContainers(host || undefined, serverId || undefined)
} catch (err: unknown) {
// Extract helpful error message from response
const error = err as { response?: { status?: number; data?: { details?: string } } }
if (error.response?.status === 503) {
const details = error.response?.data?.details
const message = details || 'Docker service unavailable. Check that Docker is running.'
throw new Error(message)
}
throw err
}
},
enabled: Boolean(host) || Boolean(serverId),
retry: 1, // Don't retry too much if docker is not available
})
return {
containers,
isLoading,
error,
refetch,
}
}

View File

@@ -1,34 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../api/domains'
export function useDomains() {
const queryClient = useQueryClient()
const { data: domains = [], isLoading, isFetching, error } = useQuery({
queryKey: ['domains'],
queryFn: api.getDomains,
})
const createMutation = useMutation({
mutationFn: api.createDomain,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['domains'] })
},
})
const deleteMutation = useMutation({
mutationFn: api.deleteDomain,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['domains'] })
},
})
return {
domains,
isLoading,
isFetching,
error,
createDomain: createMutation.mutateAsync,
deleteDomain: deleteMutation.mutateAsync,
}
}

View File

@@ -1,78 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getEncryptionStatus,
rotateEncryptionKey,
getRotationHistory,
validateKeyConfiguration,
type RotationStatus,
type RotationResult,
type RotationHistoryEntry,
type KeyValidationResult,
} from '../api/encryption'
/** Query key factory for encryption management */
const queryKeys = {
all: ['encryption'] as const,
status: () => [...queryKeys.all, 'status'] as const,
history: () => [...queryKeys.all, 'history'] as const,
}
/**
* Hook for fetching encryption status with auto-refresh.
* @param refetchInterval - Milliseconds between refetches (default: 5000ms during rotation)
* @returns Query result with status data
*/
export function useEncryptionStatus(refetchInterval?: number) {
return useQuery({
queryKey: queryKeys.status(),
queryFn: getEncryptionStatus,
refetchInterval: refetchInterval || false,
staleTime: 30000, // 30 seconds
})
}
/**
* Hook for fetching rotation audit history.
* @returns Query result with history array
*/
export function useRotationHistory() {
return useQuery({
queryKey: queryKeys.history(),
queryFn: getRotationHistory,
staleTime: 60000, // 1 minute
})
}
/**
* Hook providing key rotation mutation.
* @returns Mutation object for triggering key rotation
*/
export function useRotateKey() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: rotateEncryptionKey,
onSuccess: () => {
// Invalidate status and history to refresh UI
queryClient.invalidateQueries({ queryKey: queryKeys.status() })
queryClient.invalidateQueries({ queryKey: queryKeys.history() })
},
})
}
/**
* Hook providing key validation mutation.
* @returns Mutation object for validating key configuration
*/
export function useValidateKeys() {
return useMutation({
mutationFn: validateKeyConfiguration,
})
}
export type {
RotationStatus,
RotationResult,
RotationHistoryEntry,
KeyValidationResult,
}

View File

@@ -1,107 +0,0 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
uploadCaddyfile,
getImportPreview,
commitImport,
cancelImport,
getImportStatus,
ImportSession,
ImportPreview,
ImportCommitResult,
} from '../api/import';
export const QUERY_KEY = ['import-session'];
export function useImport() {
const queryClient = useQueryClient();
// Track when commit has succeeded to disable preview fetching
const [commitSucceeded, setCommitSucceeded] = useState(false);
// Store the commit result for display in success modal
const [commitResult, setCommitResult] = useState<ImportCommitResult | null>(null);
// Poll for status if we think there's an active session
const statusQuery = useQuery({
queryKey: QUERY_KEY,
queryFn: getImportStatus,
refetchInterval: (query) => {
const data = query.state.data;
// Poll if we have a pending session in reviewing state (but not transient, as those don't change)
if (data?.has_pending && data?.session?.state === 'reviewing') {
return 3000;
}
return false;
},
});
const previewQuery = useQuery({
queryKey: ['import-preview'],
queryFn: getImportPreview,
// Only enable when there's an active session AND commit hasn't just succeeded
enabled: !!statusQuery.data?.has_pending &&
(statusQuery.data?.session?.state === 'reviewing' || statusQuery.data?.session?.state === 'pending' || statusQuery.data?.session?.state === 'transient') &&
!commitSucceeded,
});
const uploadMutation = useMutation({
mutationFn: (content: string) => uploadCaddyfile(content),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
queryClient.invalidateQueries({ queryKey: ['import-preview'] });
},
});
const commitMutation = useMutation({
mutationFn: ({ resolutions, names }: { resolutions: Record<string, string>; names: Record<string, string> }) => {
const sessionId = statusQuery.data?.session?.id;
if (!sessionId) throw new Error("No active session");
return commitImport(sessionId, resolutions, names);
},
onSuccess: (result) => {
// Store the commit result for display in success modal
setCommitResult(result);
// Mark commit as succeeded to prevent preview refetch (which would 404)
setCommitSucceeded(true);
// Remove preview cache entirely to prevent 404 refetch after commit
// (the session no longer exists, so preview endpoint returns 404)
queryClient.removeQueries({ queryKey: ['import-preview'] });
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
// Also invalidate proxy hosts as they might have changed
queryClient.invalidateQueries({ queryKey: ['proxy-hosts'] });
},
});
const cancelMutation = useMutation({
mutationFn: () => cancelImport(),
onSuccess: () => {
// Remove preview cache entirely to prevent 404 refetch after cancel
queryClient.removeQueries({ queryKey: ['import-preview'] });
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const clearCommitResult = () => {
setCommitResult(null);
setCommitSucceeded(false);
};
return {
session: statusQuery.data?.session || null,
preview: previewQuery.data || null,
loading: statusQuery.isLoading || uploadMutation.isPending || commitMutation.isPending || cancelMutation.isPending,
// Only include previewQuery.error if there's an active session and commit hasn't succeeded
// (404 expected when no session or after commit)
error: (statusQuery.error || (previewQuery.error && statusQuery.data?.has_pending && !commitSucceeded) || uploadMutation.error || commitMutation.error || cancelMutation.error)
? ((statusQuery.error || (previewQuery.error && statusQuery.data?.has_pending && !commitSucceeded ? previewQuery.error : null) || uploadMutation.error || commitMutation.error || cancelMutation.error) as Error)?.message
: null,
commitSuccess: commitSucceeded,
commitResult,
clearCommitResult,
upload: uploadMutation.mutateAsync,
commit: (resolutions: Record<string, string>, names: Record<string, string>) =>
commitMutation.mutateAsync({ resolutions, names }),
cancel: cancelMutation.mutateAsync,
};
}
export type { ImportSession, ImportPreview, ImportCommitResult };

View File

@@ -1,84 +0,0 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
uploadJSONExport,
commitJSONImport,
cancelJSONImport,
JSONImportPreview,
JSONImportCommitResult,
} from '../api/jsonImport';
/**
* Hook for managing JSON import workflow.
* Provides upload, commit, and cancel functionality with state management.
*/
export function useJSONImport() {
const queryClient = useQueryClient();
const [preview, setPreview] = useState<JSONImportPreview | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [commitResult, setCommitResult] = useState<JSONImportCommitResult | null>(null);
const uploadMutation = useMutation({
mutationFn: uploadJSONExport,
onSuccess: (data) => {
setPreview(data);
setSessionId(data.session.id);
},
});
const commitMutation = useMutation({
mutationFn: ({
resolutions,
names,
}: {
resolutions: Record<string, string>;
names: Record<string, string>;
}) => {
if (!sessionId) throw new Error('No active session');
return commitJSONImport(sessionId, resolutions, names);
},
onSuccess: (data) => {
setCommitResult(data);
setPreview(null);
setSessionId(null);
queryClient.invalidateQueries({ queryKey: ['proxy-hosts'] });
},
});
const cancelMutation = useMutation({
mutationFn: cancelJSONImport,
onSuccess: () => {
setPreview(null);
setSessionId(null);
},
});
const clearCommitResult = () => {
setCommitResult(null);
};
const reset = () => {
setPreview(null);
setSessionId(null);
setCommitResult(null);
};
return {
preview,
sessionId,
loading: uploadMutation.isPending,
error: uploadMutation.error,
upload: uploadMutation.mutateAsync,
commit: (resolutions: Record<string, string>, names: Record<string, string>) =>
commitMutation.mutateAsync({ resolutions, names }),
committing: commitMutation.isPending,
commitError: commitMutation.error,
commitResult,
clearCommitResult,
cancel: cancelMutation.mutateAsync,
cancelling: cancelMutation.isPending,
reset,
};
}
export type { JSONImportPreview, JSONImportCommitResult };

View File

@@ -1,10 +0,0 @@
import { useContext } from 'react'
import { LanguageContext, LanguageContextType } from '../context/LanguageContextValue'
export function useLanguage(): LanguageContextType {
const context = useContext(LanguageContext)
if (!context) {
throw new Error('useLanguage must be used within a LanguageProvider')
}
return context
}

View File

@@ -1,111 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getChallenge,
createChallenge,
verifyChallenge,
pollChallenge,
deleteChallenge,
type ManualChallenge,
type CreateChallengeRequest,
type ChallengePollResponse,
type ChallengeVerifyResponse,
} from '../api/manualChallenge'
/** Query key factory for manual challenges */
const queryKeys = {
all: ['manual-challenges'] as const,
detail: (providerId: number, challengeId: string) =>
[...queryKeys.all, 'detail', providerId, challengeId] as const,
poll: (providerId: number, challengeId: string) =>
[...queryKeys.all, 'poll', providerId, challengeId] as const,
}
/**
* Hook for fetching a manual challenge by ID.
* @param providerId - DNS provider ID
* @param challengeId - Challenge UUID
* @returns Query result with challenge data
*/
export function useManualChallenge(providerId: number, challengeId: string) {
return useQuery({
queryKey: queryKeys.detail(providerId, challengeId),
queryFn: () => getChallenge(providerId, challengeId),
enabled: providerId > 0 && !!challengeId,
staleTime: 1000 * 5, // 5 seconds
})
}
/**
* Hook for polling challenge status with automatic refresh.
* @param providerId - DNS provider ID
* @param challengeId - Challenge UUID
* @param enabled - Whether polling is active
* @param refetchInterval - Polling interval in ms (default 10s)
* @returns Query result with poll data
*/
export function useChallengePoll(
providerId: number,
challengeId: string,
enabled: boolean = true,
refetchInterval: number = 10000
) {
return useQuery({
queryKey: queryKeys.poll(providerId, challengeId),
queryFn: () => pollChallenge(providerId, challengeId),
enabled: enabled && providerId > 0 && !!challengeId,
refetchInterval: enabled ? refetchInterval : false,
refetchIntervalInBackground: false,
})
}
/**
* Hook providing manual challenge mutation operations.
* @returns Object with mutation functions for create, verify, and delete
*/
export function useManualChallengeMutations() {
const queryClient = useQueryClient()
const createMutation = useMutation({
mutationFn: ({ providerId, data }: { providerId: number; data: CreateChallengeRequest }) =>
createChallenge(providerId, data),
})
const verifyMutation = useMutation({
mutationFn: ({ providerId, challengeId }: { providerId: number; challengeId: string }) =>
verifyChallenge(providerId, challengeId),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.poll(variables.providerId, variables.challengeId),
})
queryClient.invalidateQueries({
queryKey: queryKeys.detail(variables.providerId, variables.challengeId),
})
},
})
const deleteMutation = useMutation({
mutationFn: ({ providerId, challengeId }: { providerId: number; challengeId: string }) =>
deleteChallenge(providerId, challengeId),
onSuccess: (_, variables) => {
queryClient.removeQueries({
queryKey: queryKeys.poll(variables.providerId, variables.challengeId),
})
queryClient.removeQueries({
queryKey: queryKeys.detail(variables.providerId, variables.challengeId),
})
},
})
return {
createMutation,
verifyMutation,
deleteMutation,
}
}
export type {
ManualChallenge,
CreateChallengeRequest,
ChallengePollResponse,
ChallengeVerifyResponse,
}

View File

@@ -1,84 +0,0 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
uploadNPMExport,
commitNPMImport,
cancelNPMImport,
NPMImportPreview,
NPMImportCommitResult,
} from '../api/npmImport';
/**
* Hook for managing NPM import workflow.
* Provides upload, commit, and cancel functionality with state management.
*/
export function useNPMImport() {
const queryClient = useQueryClient();
const [preview, setPreview] = useState<NPMImportPreview | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [commitResult, setCommitResult] = useState<NPMImportCommitResult | null>(null);
const uploadMutation = useMutation({
mutationFn: uploadNPMExport,
onSuccess: (data) => {
setPreview(data);
setSessionId(data.session.id);
},
});
const commitMutation = useMutation({
mutationFn: ({
resolutions,
names,
}: {
resolutions: Record<string, string>;
names: Record<string, string>;
}) => {
if (!sessionId) throw new Error('No active session');
return commitNPMImport(sessionId, resolutions, names);
},
onSuccess: (data) => {
setCommitResult(data);
setPreview(null);
setSessionId(null);
queryClient.invalidateQueries({ queryKey: ['proxy-hosts'] });
},
});
const cancelMutation = useMutation({
mutationFn: cancelNPMImport,
onSuccess: () => {
setPreview(null);
setSessionId(null);
},
});
const clearCommitResult = () => {
setCommitResult(null);
};
const reset = () => {
setPreview(null);
setSessionId(null);
setCommitResult(null);
};
return {
preview,
sessionId,
loading: uploadMutation.isPending,
error: uploadMutation.error,
upload: uploadMutation.mutateAsync,
commit: (resolutions: Record<string, string>, names: Record<string, string>) =>
commitMutation.mutateAsync({ resolutions, names }),
committing: commitMutation.isPending,
commitError: commitMutation.error,
commitResult,
clearCommitResult,
cancel: cancelMutation.mutateAsync,
cancelling: cancelMutation.isPending,
reset,
};
}
export type { NPMImportPreview, NPMImportCommitResult };

View File

@@ -1,52 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
getSecurityNotificationSettings,
updateSecurityNotificationSettings,
SecurityNotificationSettings,
} from '../api/notifications';
import { toast } from '../utils/toast';
export function useSecurityNotificationSettings() {
return useQuery({
queryKey: ['security-notification-settings'],
queryFn: getSecurityNotificationSettings,
});
}
export function useUpdateSecurityNotificationSettings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (settings: Partial<SecurityNotificationSettings>) =>
updateSecurityNotificationSettings(settings),
onMutate: async (newSettings) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['security-notification-settings'] });
// Snapshot the previous value
const previousSettings = queryClient.getQueryData(['security-notification-settings']);
// Optimistically update to the new value
queryClient.setQueryData(['security-notification-settings'], (old: unknown) => {
if (old && typeof old === 'object') {
return { ...old, ...newSettings };
}
return old;
});
return { previousSettings };
},
onError: (err, _newSettings, context) => {
// Rollback on error
if (context?.previousSettings) {
queryClient.setQueryData(['security-notification-settings'], context.previousSettings);
}
const message = err instanceof Error ? err.message : 'Failed to update notification settings';
toast.error(message);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['security-notification-settings'] });
toast.success('Notification settings updated');
},
});
}

View File

@@ -1,106 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getPlugins,
getPlugin,
enablePlugin,
disablePlugin,
reloadPlugins,
getProviderFields,
type PluginInfo,
type ProviderFieldsResponse,
} from '../api/plugins'
/** Query key factory for plugins */
const queryKeys = {
all: ['plugins'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: () => [...queryKeys.lists()] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
providerFields: (type: string) => ['dns-providers', 'fields', type] as const,
}
/**
* Hook for fetching all plugins.
* @returns Query result with plugins array
*/
export function usePlugins() {
return useQuery({
queryKey: queryKeys.list(),
queryFn: getPlugins,
})
}
/**
* Hook for fetching a single plugin.
* @param id - Plugin ID
* @returns Query result with plugin data
*/
export function usePlugin(id: number) {
return useQuery({
queryKey: queryKeys.detail(id),
queryFn: () => getPlugin(id),
enabled: id > 0,
})
}
/**
* Hook for fetching provider credential field definitions.
* @param providerType - Provider type identifier
* @returns Query result with field specifications
*/
export function useProviderFields(providerType: string) {
return useQuery({
queryKey: queryKeys.providerFields(providerType),
queryFn: () => getProviderFields(providerType),
enabled: !!providerType,
staleTime: 1000 * 60 * 60, // 1 hour - field definitions rarely change
})
}
/**
* Hook for enabling a plugin.
* @returns Mutation function for enabling plugins
*/
export function useEnablePlugin() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => enablePlugin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
},
})
}
/**
* Hook for disabling a plugin.
* @returns Mutation function for disabling plugins
*/
export function useDisablePlugin() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => disablePlugin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
},
})
}
/**
* Hook for reloading all plugins.
* @returns Mutation function for reloading plugins
*/
export function useReloadPlugins() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: reloadPlugins,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.list() })
},
})
}
export type { PluginInfo, ProviderFieldsResponse }

View File

@@ -1,80 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getProxyHosts,
createProxyHost,
updateProxyHost,
deleteProxyHost,
bulkUpdateACL,
bulkUpdateSecurityHeaders,
ProxyHost
} from '../api/proxyHosts';
export const QUERY_KEY = ['proxy-hosts'];
export function useProxyHosts() {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: QUERY_KEY,
queryFn: getProxyHosts,
});
const createMutation = useMutation({
mutationFn: (host: Partial<ProxyHost>) => createProxyHost(host),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const updateMutation = useMutation({
mutationFn: ({ uuid, data }: { uuid: string; data: Partial<ProxyHost> }) =>
updateProxyHost(uuid, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const deleteMutation = useMutation({
mutationFn: (opts: { uuid: string; deleteUptime?: boolean } | string) =>
typeof opts === 'string' ? deleteProxyHost(opts) : (opts.deleteUptime !== undefined ? deleteProxyHost(opts.uuid, opts.deleteUptime) : deleteProxyHost(opts.uuid)),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const bulkUpdateACLMutation = useMutation({
mutationFn: ({ hostUUIDs, accessListID }: { hostUUIDs: string[]; accessListID: number | null }) =>
bulkUpdateACL(hostUUIDs, accessListID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const bulkUpdateSecurityHeadersMutation = useMutation({
mutationFn: ({ hostUUIDs, securityHeaderProfileId }: { hostUUIDs: string[]; securityHeaderProfileId: number | null }) =>
bulkUpdateSecurityHeaders(hostUUIDs, securityHeaderProfileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
return {
hosts: query.data || [],
loading: query.isLoading,
isFetching: query.isFetching,
error: query.error ? (query.error as Error).message : null,
createHost: createMutation.mutateAsync,
updateHost: (uuid: string, data: Partial<ProxyHost>) => updateMutation.mutateAsync({ uuid, data }),
deleteHost: (uuid: string, deleteUptime?: boolean) => deleteMutation.mutateAsync(deleteUptime !== undefined ? { uuid, deleteUptime } : uuid),
bulkUpdateACL: (hostUUIDs: string[], accessListID: number | null) =>
bulkUpdateACLMutation.mutateAsync({ hostUUIDs, accessListID }),
bulkUpdateSecurityHeaders: (hostUUIDs: string[], securityHeaderProfileId: number | null) =>
bulkUpdateSecurityHeadersMutation.mutateAsync({ hostUUIDs, securityHeaderProfileId }),
isCreating: createMutation.isPending,
isUpdating: updateMutation.isPending,
isDeleting: deleteMutation.isPending,
isBulkUpdating: bulkUpdateACLMutation.isPending || bulkUpdateSecurityHeadersMutation.isPending,
};
}
export type { ProxyHost };

View File

@@ -1,63 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getRemoteServers,
createRemoteServer,
updateRemoteServer,
deleteRemoteServer,
testRemoteServerConnection,
RemoteServer
} from '../api/remoteServers';
export const QUERY_KEY = ['remote-servers'];
export function useRemoteServers(enabledOnly = false) {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: [...QUERY_KEY, { enabled: enabledOnly }],
queryFn: () => getRemoteServers(enabledOnly),
});
const createMutation = useMutation({
mutationFn: (server: Partial<RemoteServer>) => createRemoteServer(server),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const updateMutation = useMutation({
mutationFn: ({ uuid, data }: { uuid: string; data: Partial<RemoteServer> }) =>
updateRemoteServer(uuid, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const deleteMutation = useMutation({
mutationFn: (uuid: string) => deleteRemoteServer(uuid),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
},
});
const testConnectionMutation = useMutation({
mutationFn: (uuid: string) => testRemoteServerConnection(uuid),
});
return {
servers: query.data || [],
loading: query.isLoading,
isFetching: query.isFetching,
error: query.error ? (query.error as Error).message : null,
createServer: createMutation.mutateAsync,
updateServer: (uuid: string, data: Partial<RemoteServer>) => updateMutation.mutateAsync({ uuid, data }),
deleteServer: deleteMutation.mutateAsync,
testConnection: testConnectionMutation.mutateAsync,
isCreating: createMutation.isPending,
isUpdating: updateMutation.isPending,
isDeleting: deleteMutation.isPending,
isTestingConnection: testConnectionMutation.isPending,
};
}
export type { RemoteServer };

View File

@@ -1,119 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getSecurityStatus,
getSecurityConfig,
updateSecurityConfig,
generateBreakGlassToken,
enableCerberus,
disableCerberus,
getDecisions,
createDecision,
getRuleSets,
upsertRuleSet,
deleteRuleSet,
type UpsertRuleSetPayload,
type SecurityConfigPayload,
type CreateDecisionPayload,
} from '../api/security'
import toast from 'react-hot-toast'
export function useSecurityStatus() {
return useQuery({ queryKey: ['securityStatus'], queryFn: getSecurityStatus })
}
export function useSecurityConfig() {
return useQuery({ queryKey: ['securityConfig'], queryFn: getSecurityConfig })
}
export function useUpdateSecurityConfig() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: SecurityConfigPayload) => updateSecurityConfig(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['securityConfig'] })
qc.invalidateQueries({ queryKey: ['securityStatus'] })
toast.success('Security configuration updated')
},
onError: (err: Error) => {
toast.error(`Failed to update security settings: ${err.message}`)
},
})
}
export function useGenerateBreakGlassToken() {
return useMutation({ mutationFn: () => generateBreakGlassToken() })
}
export function useDecisions(limit = 50) {
return useQuery({ queryKey: ['securityDecisions', limit], queryFn: () => getDecisions(limit) })
}
export function useCreateDecision() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: CreateDecisionPayload) => createDecision(payload),
onSuccess: () => qc.invalidateQueries({ queryKey: ['securityDecisions'] }),
})
}
export function useRuleSets() {
return useQuery({ queryKey: ['securityRulesets'], queryFn: () => getRuleSets() })
}
export function useUpsertRuleSet() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload: UpsertRuleSetPayload) => upsertRuleSet(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['securityRulesets'] })
toast.success('Rule set saved successfully')
},
onError: (err: Error) => {
toast.error(`Failed to save rule set: ${err.message}`)
},
})
}
export function useDeleteRuleSet() {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: number) => deleteRuleSet(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['securityRulesets'] })
toast.success('Rule set deleted')
},
onError: (err: Error) => {
toast.error(`Failed to delete rule set: ${err.message}`)
},
})
}
export function useEnableCerberus() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload?: Record<string, unknown>) => enableCerberus(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['securityConfig'] })
qc.invalidateQueries({ queryKey: ['securityStatus'] })
toast.success('Cerberus enabled')
},
onError: (err: Error) => {
toast.error(`Failed to enable Cerberus: ${err.message}`)
},
})
}
export function useDisableCerberus() {
const qc = useQueryClient()
return useMutation({
mutationFn: (payload?: Record<string, unknown>) => disableCerberus(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['securityConfig'] })
qc.invalidateQueries({ queryKey: ['securityStatus'] })
toast.success('Cerberus disabled')
},
onError: (err: Error) => {
toast.error(`Failed to disable Cerberus: ${err.message}`)
},
})
}

View File

@@ -1,107 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { securityHeadersApi } from '../api/securityHeaders';
import type { CreateProfileRequest, ApplyPresetRequest } from '../api/securityHeaders';
import toast from 'react-hot-toast';
export function useSecurityHeaderProfiles() {
return useQuery({
queryKey: ['securityHeaderProfiles'],
queryFn: securityHeadersApi.listProfiles,
});
}
export function useSecurityHeaderProfile(id: number | string | undefined) {
return useQuery({
queryKey: ['securityHeaderProfile', id],
queryFn: () => securityHeadersApi.getProfile(id!),
enabled: !!id,
});
}
export function useCreateSecurityHeaderProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateProfileRequest) => securityHeadersApi.createProfile(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['securityHeaderProfiles'] });
toast.success('Security header profile created successfully');
},
onError: (error: Error) => {
toast.error(`Failed to create profile: ${error.message}`);
},
});
}
export function useUpdateSecurityHeaderProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<CreateProfileRequest> }) =>
securityHeadersApi.updateProfile(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['securityHeaderProfiles'] });
queryClient.invalidateQueries({ queryKey: ['securityHeaderProfile', variables.id] });
toast.success('Security header profile updated successfully');
},
onError: (error: Error) => {
toast.error(`Failed to update profile: ${error.message}`);
},
});
}
export function useDeleteSecurityHeaderProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => securityHeadersApi.deleteProfile(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['securityHeaderProfiles'] });
toast.success('Security header profile deleted successfully');
},
onError: (error: Error) => {
toast.error(`Failed to delete profile: ${error.message}`);
},
});
}
export function useSecurityHeaderPresets() {
return useQuery({
queryKey: ['securityHeaderPresets'],
queryFn: securityHeadersApi.getPresets,
});
}
export function useApplySecurityHeaderPreset() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: ApplyPresetRequest) => securityHeadersApi.applyPreset(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['securityHeaderProfiles'] });
toast.success('Preset applied successfully');
},
onError: (error: Error) => {
toast.error(`Failed to apply preset: ${error.message}`);
},
});
}
export function useCalculateSecurityScore() {
return useMutation({
mutationFn: (config: Partial<CreateProfileRequest>) => securityHeadersApi.calculateScore(config),
});
}
export function useValidateCSP() {
return useMutation({
mutationFn: (csp: string) => securityHeadersApi.validateCSP(csp),
});
}
export function useBuildCSP() {
return useMutation({
mutationFn: (directives: { directive: string; values: string[] }[]) =>
securityHeadersApi.buildCSP(directives),
});
}

View File

@@ -1,10 +0,0 @@
import { useContext } from 'react'
import { ThemeContext } from '../context/ThemeContextValue'
export function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}

View File

@@ -1,24 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { getWebSocketConnections, getWebSocketStats } from '../api/websocket';
/**
* Hook to fetch and manage WebSocket connection data
*/
export const useWebSocketConnections = () => {
return useQuery({
queryKey: ['websocket', 'connections'],
queryFn: getWebSocketConnections,
refetchInterval: 5000, // Refresh every 5 seconds
});
};
/**
* Hook to fetch and manage WebSocket statistics
*/
export const useWebSocketStats = () => {
return useQuery({
queryKey: ['websocket', 'stats'],
queryFn: getWebSocketStats,
refetchInterval: 5000, // Refresh every 5 seconds
});
};