chore: clean .gitignore cache
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user