feat(tests): add comprehensive tests for ProxyHosts and Uptime components

- Introduced isolated coverage tests for ProxyHosts with various scenarios including rendering, bulk apply, and link behavior.
- Enhanced existing ProxyHosts coverage tests to include additional assertions and error handling.
- Added tests for Uptime component to verify rendering and monitoring toggling functionality.
- Created utility functions for setting labels and help texts related to proxy host settings.
- Implemented bulk settings application logic with progress tracking and error handling.
- Added toast utility tests to ensure callback functionality and ID incrementing.
- Improved type safety in test files by using appropriate TypeScript types.
This commit is contained in:
GitHub Actions
2025-11-30 15:17:38 +00:00
parent d80f545a6e
commit 224a53975d
38 changed files with 1821 additions and 233 deletions
+51 -68
View File
@@ -1,80 +1,63 @@
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import Login from '../Login';
import * as setupApi from '../../api/setup';
// Mock AuthContext so useAuth works in tests
vi.mock('../../hooks/useAuth', () => ({
useAuth: () => ({
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: false,
isLoading: false,
user: null,
}),
}));
// Mock API client
vi.mock('../../api/client', () => ({
default: {
post: vi.fn().mockResolvedValue({ data: {} }),
get: vi.fn().mockResolvedValue({ data: {} }),
},
}));
// Mock react-router-dom
const mockNavigate = vi.fn();
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock react-router-dom useNavigate at module level
const mockNavigate = vi.fn()
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
const actual = await vi.importActual('react-router-dom')
return {
...actual,
useNavigate: () => mockNavigate,
};
});
}
})
// Mock the API module
vi.mock('../../api/setup', () => ({
getSetupStatus: vi.fn(),
performSetup: vi.fn(),
}));
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Login from '../Login'
import * as setupApi from '../../api/setup'
import client from '../../api/client'
import * as authHook from '../../hooks/useAuth'
import type { AuthContextType } from '../../context/AuthContextValue'
import { toast } from '../../utils/toast'
import { MemoryRouter } from 'react-router-dom'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.mock('../../api/setup')
vi.mock('../../hooks/useAuth')
const renderWithProviders = (ui: React.ReactNode) => {
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
{ui}
</MemoryRouter>
</QueryClientProvider>
);
};
describe('<Login />', () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const renderWithProviders = (ui: React.ReactNode) => (
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>
)
)
describe('Login Page', () => {
beforeEach(() => {
vi.clearAllMocks();
queryClient.clear();
});
vi.restoreAllMocks()
vi.spyOn(authHook, 'useAuth').mockReturnValue({ login: vi.fn() } as unknown as AuthContextType)
})
it('renders login form and logo when setup is not required', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: false });
renderWithProviders(<Login />);
// The page will redirect to setup if setup is required; for our test we mock it as not required
it('navigates to /setup when setup is required', async () => {
vi.spyOn(setupApi, 'getSetupStatus').mockResolvedValue({ setupRequired: true })
renderWithProviders(<Login />)
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Sign In' })).toBeTruthy();
});
expect(mockNavigate).toHaveBeenCalledWith('/setup')
})
})
// Verify logo is present
expect(screen.getAllByAltText('Charon').length).toBeGreaterThan(0);
});
});
it('shows error toast when login fails', async () => {
vi.spyOn(setupApi, 'getSetupStatus').mockResolvedValue({ setupRequired: false })
const postSpy = vi.spyOn(client, 'post').mockRejectedValueOnce({ response: { data: { error: 'Bad creds' } } })
const toastSpy = vi.spyOn(toast, 'error')
renderWithProviders(<Login />)
// Fill and submit
const email = screen.getByPlaceholderText(/admin@example.com/i)
const pass = screen.getByPlaceholderText(/••••••••/i)
fireEvent.change(email, { target: { value: 'a@b.com' } })
fireEvent.change(pass, { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('button', { name: /Sign In/i }))
// Wait for the promise chain
await waitFor(() => expect(postSpy).toHaveBeenCalled())
expect(toastSpy).toHaveBeenCalledWith('Bad creds')
})
})
@@ -0,0 +1,88 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import ProxyHosts from '../ProxyHosts';
import * as proxyHostsApi from '../../api/proxyHosts';
import * as certificatesApi from '../../api/certificates';
import type { ProxyHost } from '../../api/proxyHosts'
import type { Certificate } from '../../api/certificates'
import * as accessListsApi from '../../api/accessLists';
import type { AccessList } from '../../api/accessLists'
import * as settingsApi from '../../api/settings';
import { createMockProxyHost } from '../../testUtils/createMockProxyHost';
vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }));
vi.mock('../../api/proxyHosts', () => ({ getProxyHosts: vi.fn(), createProxyHost: vi.fn(), updateProxyHost: vi.fn(), deleteProxyHost: vi.fn(), bulkUpdateACL: vi.fn(), testProxyHostConnection: vi.fn() }));
vi.mock('../../api/certificates', () => ({ getCertificates: vi.fn() }));
vi.mock('../../api/accessLists', () => ({ accessListsApi: { list: vi.fn() } }));
vi.mock('../../api/settings', () => ({ getSettings: vi.fn() }));
const hosts = [
createMockProxyHost({ uuid: 'h1', name: 'Host 1', domain_names: 'one.example.com' }),
createMockProxyHost({ uuid: 'h2', name: 'Host 2', domain_names: 'two.example.com' }),
];
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } });
const renderWithProviders = (ui: React.ReactNode) => {
const queryClient = createQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>
);
};
describe('ProxyHosts - Bulk Apply all settings coverage', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue(hosts as ProxyHost[]);
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([] as Certificate[]);
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([] as AccessList[]);
vi.mocked(settingsApi.getSettings).mockResolvedValue({} as Record<string, string>);
});
it('renders all bulk apply setting labels and allows toggling', async () => {
renderWithProviders(<ProxyHosts />);
await waitFor(() => expect(screen.getByText('Host 1')).toBeTruthy());
// select all
const headerCheckbox = screen.getAllByRole('checkbox')[0];
await userEvent.click(headerCheckbox);
// open Bulk Apply
await waitFor(() => expect(screen.getByText('Bulk Apply')).toBeTruthy());
await userEvent.click(screen.getByText('Bulk Apply'));
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy());
const labels = [
'Force SSL',
'HTTP/2 Support',
'HSTS Enabled',
'HSTS Subdomains',
'Block Exploits',
'Websockets Support',
];
for (const lbl of labels) {
expect(screen.getByText(lbl)).toBeTruthy();
// find close checkbox and click its apply checkbox (the first input in the label area)
const el = screen.getByText(lbl) as HTMLElement;
let container: HTMLElement | null = el;
while (container && !container.querySelector('input[type="checkbox"]')) container = container.parentElement;
const cb = container?.querySelector('input[type="checkbox"]') as HTMLElement | null;
if (cb) await userEvent.click(cb);
}
// After toggling at least one, Apply should be enabled
const modalRoot = screen.getByText('Bulk Apply Settings').closest('div');
const { within } = await import('@testing-library/react');
const applyBtn = modalRoot ? within(modalRoot).getByRole('button', { name: /^Apply$/i }) : screen.getByRole('button', { name: /^Apply$/i });
expect(applyBtn).toBeTruthy();
// Cancel to close
await userEvent.click(modalRoot ? within(modalRoot).getByRole('button', { name: /Cancel/i }) : screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => expect(screen.queryByText('Bulk Apply Settings')).toBeNull());
});
});
@@ -0,0 +1,88 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import ProxyHosts from '../ProxyHosts'
import * as proxyHostsApi from '../../api/proxyHosts'
import * as certificatesApi from '../../api/certificates'
import * as accessListsApi from '../../api/accessLists'
import * as settingsApi from '../../api/settings'
import type { Certificate } from '../../api/certificates'
import type { AccessList } from '../../api/accessLists'
import { createMockProxyHost } from '../../testUtils/createMockProxyHost'
import type { ProxyHost } from '../../api/proxyHosts'
vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }))
vi.mock('../../api/proxyHosts', () => ({ getProxyHosts: vi.fn(), createProxyHost: vi.fn(), updateProxyHost: vi.fn(), deleteProxyHost: vi.fn(), bulkUpdateACL: vi.fn(), testProxyHostConnection: vi.fn() }))
vi.mock('../../api/certificates', () => ({ getCertificates: vi.fn() }))
vi.mock('../../api/accessLists', () => ({ accessListsApi: { list: vi.fn() } }))
vi.mock('../../api/settings', () => ({ getSettings: vi.fn() }))
const hosts = [
createMockProxyHost({ uuid: 'p1', name: 'Progress 1', domain_names: 'p1.example.com' }),
createMockProxyHost({ uuid: 'p2', name: 'Progress 2', domain_names: 'p2.example.com' }),
]
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } })
const renderWithProviders = (ui: React.ReactNode) => {
const queryClient = createQueryClient()
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>
)
}
describe('ProxyHosts - Bulk Apply progress UI', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue(hosts as ProxyHost[])
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([] as Certificate[])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({} as Record<string, string>)
})
it('shows applying progress while updateProxyHost resolves', async () => {
// Make updateProxyHost return controllable promises so we can assert the progress UI
const updateMock = vi.mocked(proxyHostsApi.updateProxyHost)
const resolvers: Array<(v: ProxyHost) => void> = []
updateMock.mockImplementation(() => new Promise((res: (v: ProxyHost) => void) => { resolvers.push(res) }))
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('Progress 1')).toBeTruthy())
// Select all
const selectAll = screen.getAllByRole('checkbox')[0]
await userEvent.click(selectAll)
// Open Bulk Apply
await userEvent.click(screen.getByText('Bulk Apply'))
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy())
// Enable one setting (Force SSL)
const forceLabel = screen.getByText(/Force SSL/i) as HTMLElement
let forceContainer: HTMLElement | null = forceLabel
while (forceContainer && !forceContainer.querySelector('input[type="checkbox"]')) forceContainer = forceContainer.parentElement
const forceCheckbox = forceContainer ? (forceContainer.querySelector('input[type="checkbox"]') as HTMLElement | null) : null
if (forceCheckbox) await userEvent.click(forceCheckbox as HTMLElement)
// Click Apply and assert progress UI appears
const modalRoot = screen.getByText('Bulk Apply Settings').closest('div')
const { within } = await import('@testing-library/react')
const applyButton = modalRoot ? within(modalRoot).getByRole('button', { name: /^Apply$/i }) : screen.getByRole('button', { name: /^Apply$/i })
await userEvent.click(applyButton)
// During the small delay the progress text should appear (there are two matching nodes)
await waitFor(() => expect(screen.getAllByText(/Applying settings/i).length).toBeGreaterThan(0))
// Resolve both pending update promises to finish the operation
resolvers.forEach(r => r(hosts[0]))
// Ensure subsequent tests aren't blocked by the special mock: make updateProxyHost resolve normally
updateMock.mockImplementation(() => Promise.resolve(hosts[0] as ProxyHost))
// Wait for updates to complete
await waitFor(() => expect(updateMock).toHaveBeenCalledTimes(2))
})
})
export {}
@@ -0,0 +1,130 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import ProxyHosts from '../ProxyHosts';
import * as proxyHostsApi from '../../api/proxyHosts';
import * as certificatesApi from '../../api/certificates';
import type { Certificate } from '../../api/certificates'
import type { ProxyHost } from '../../api/proxyHosts'
import * as accessListsApi from '../../api/accessLists';
import type { AccessList } from '../../api/accessLists'
import * as settingsApi from '../../api/settings';
import { createMockProxyHost } from '../../testUtils/createMockProxyHost';
// Mock toast
vi.mock('react-hot-toast', () => ({
toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() },
}));
vi.mock('../../api/proxyHosts', () => ({
getProxyHosts: vi.fn(),
createProxyHost: vi.fn(),
updateProxyHost: vi.fn(),
deleteProxyHost: vi.fn(),
bulkUpdateACL: vi.fn(),
testProxyHostConnection: vi.fn(),
}));
vi.mock('../../api/certificates', () => ({ getCertificates: vi.fn() }));
vi.mock('../../api/accessLists', () => ({ accessListsApi: { list: vi.fn() } }));
vi.mock('../../api/settings', () => ({ getSettings: vi.fn() }));
const mockProxyHosts = [
createMockProxyHost({ uuid: 'host-1', name: 'Test Host 1', domain_names: 'test1.example.com', forward_host: '192.168.1.10' }),
createMockProxyHost({ uuid: 'host-2', name: 'Test Host 2', domain_names: 'test2.example.com', forward_host: '192.168.1.20' }),
];
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } });
const renderWithProviders = (ui: React.ReactNode) => {
const queryClient = createQueryClient();
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>
);
};
describe('ProxyHosts - Bulk Apply Settings', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue(mockProxyHosts as ProxyHost[]);
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([] as Certificate[]);
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([] as AccessList[]);
vi.mocked(settingsApi.getSettings).mockResolvedValue({} as Record<string, string>);
});
it('shows Bulk Apply button when hosts selected and opens modal', async () => {
renderWithProviders(<ProxyHosts />);
await waitFor(() => expect(screen.getByText('Test Host 1')).toBeTruthy());
// Select first host using select-all checkbox
const selectAll = screen.getAllByRole('checkbox')[0];
await userEvent.click(selectAll);
// Bulk Apply button should appear
await waitFor(() => expect(screen.getByText('Bulk Apply')).toBeTruthy());
// Open modal
await userEvent.click(screen.getByText('Bulk Apply'));
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy());
});
it('applies selected settings to all selected hosts by calling updateProxyHost merged payload', async () => {
const updateMock = vi.mocked(proxyHostsApi.updateProxyHost);
updateMock.mockResolvedValue(mockProxyHosts[0] as ProxyHost);
renderWithProviders(<ProxyHosts />);
await waitFor(() => expect(screen.getByText('Test Host 1')).toBeTruthy());
// Select hosts
const selectAll = screen.getAllByRole('checkbox')[0];
await userEvent.click(selectAll);
await waitFor(() => expect(screen.getByText('Bulk Apply')).toBeTruthy());
// Open Bulk Apply modal
await userEvent.click(screen.getByText('Bulk Apply'));
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy());
// Enable first setting checkbox (Force SSL)
// Enable first setting checkbox (Force SSL) - locate by text then find the checkbox inside its container
const forceLabel = screen.getByText(/Force SSL/i) as HTMLElement;
let forceContainer: HTMLElement | null = forceLabel;
while (forceContainer && !forceContainer.querySelector('input[type="checkbox"]')) {
forceContainer = forceContainer.parentElement
}
const forceCheckbox = forceContainer ? (forceContainer.querySelector('input[type="checkbox"]') as HTMLElement | null) : null;
if (forceCheckbox) await userEvent.click(forceCheckbox as HTMLElement);
// Click Apply (scope to modal to avoid matching header 'Bulk Apply' button)
const modalRoot = screen.getByText('Bulk Apply Settings').closest('div');
const { within } = await import('@testing-library/react');
const applyButton = modalRoot ? within(modalRoot).getByRole('button', { name: /^Apply$/i }) : screen.getByRole('button', { name: /^Apply$/i });
await userEvent.click(applyButton);
// Should call updateProxyHost for each selected host with merged payload containing ssl_forced
await waitFor(() => {
expect(updateMock).toHaveBeenCalled();
const calls = updateMock.mock.calls;
expect(calls.length).toBe(2);
expect(calls[0][1]).toHaveProperty('ssl_forced');
expect(calls[1][1]).toHaveProperty('ssl_forced');
});
});
it('cancels bulk apply modal when Cancel clicked', async () => {
renderWithProviders(<ProxyHosts />);
await waitFor(() => expect(screen.getByText('Test Host 1')).toBeTruthy());
const selectAll = screen.getAllByRole('checkbox')[0];
await userEvent.click(selectAll);
await waitFor(() => expect(screen.getByText('Bulk Apply')).toBeTruthy());
await userEvent.click(screen.getByText('Bulk Apply'));
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy());
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByText('Bulk Apply Settings')).toBeNull());
});
});
@@ -0,0 +1,173 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import type { ProxyHost } from '../../api/proxyHosts'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// We'll use per-test module mocks via `vi.doMock` and dynamic imports to avoid
// leaking mocks into other tests. Each test creates its own QueryClient.
describe('ProxyHosts page - coverage targets (isolated)', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
})
afterEach(() => {
vi.useRealTimers()
})
const renderPage = async () => {
// Dynamic mocks
const mockUpdateHost = vi.fn()
vi.doMock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }))
vi.doMock('../../hooks/useProxyHosts', () => ({
useProxyHosts: vi.fn(() => ({
hosts: [
{
uuid: 'host-1',
name: 'StagingHost',
domain_names: 'staging.example.com',
forward_scheme: 'http',
forward_host: '10.0.0.1',
forward_port: 80,
ssl_forced: true,
websocket_support: true,
certificate: undefined,
enabled: true,
created_at: '2025-01-01',
updated_at: '2025-01-01',
},
{
uuid: 'host-2',
name: 'CustomCertHost',
domain_names: 'custom.example.com',
forward_scheme: 'http',
forward_host: '10.0.0.2',
forward_port: 8080,
ssl_forced: false,
websocket_support: false,
certificate: { provider: 'custom', name: 'ACME-CUSTOM' },
enabled: false,
created_at: '2025-01-01',
updated_at: '2025-01-01',
}
],
loading: false,
isFetching: false,
error: null,
createHost: vi.fn(),
updateHost: (uuid: string, data: Partial<ProxyHost>) => mockUpdateHost(uuid, data),
deleteHost: vi.fn(),
bulkUpdateACL: vi.fn(),
isBulkUpdating: false,
}))
}))
vi.doMock('../../hooks/useCertificates', () => ({
useCertificates: vi.fn(() => ({
certificates: [
{ id: 1, name: 'StagingCert', domain: 'staging.example.com', status: 'untrusted', provider: 'letsencrypt-staging' }
],
isLoading: false,
error: null,
}))
}))
vi.doMock('../../hooks/useAccessLists', () => ({ useAccessLists: vi.fn(() => ({ data: [] })) }))
vi.doMock('../../api/settings', () => ({ getSettings: vi.fn(() => Promise.resolve({ 'ui.domain_link_behavior': 'new_window' })) }))
// Import page after mocks are in place
const { default: ProxyHosts } = await import('../ProxyHosts')
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const wrapper = (ui: React.ReactNode) => (
<QueryClientProvider client={qc}>{ui}</QueryClientProvider>
)
return { ProxyHosts, mockUpdateHost, wrapper }
}
it('renders SSL staging badge, websocket badge and custom cert text', async () => {
const { ProxyHosts } = await renderPage()
render(
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<ProxyHosts />
</QueryClientProvider>
)
await waitFor(() => expect(screen.getByText('StagingHost')).toBeInTheDocument())
expect(screen.getByText(/SSL \(Staging\)/)).toBeInTheDocument()
expect(screen.getByText('WS')).toBeInTheDocument()
expect(screen.getByText('ACME-CUSTOM (Custom)')).toBeInTheDocument()
})
it('opens domain link in new window when linkBehavior is new_window', async () => {
const { ProxyHosts } = await renderPage()
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
render(
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<ProxyHosts />
</QueryClientProvider>
)
await waitFor(() => expect(screen.getByText('staging.example.com')).toBeInTheDocument())
const link = screen.getByText('staging.example.com').closest('a') as HTMLAnchorElement
await act(async () => {
await userEvent.click(link!)
})
expect(openSpy).toHaveBeenCalled()
openSpy.mockRestore()
})
it('bulk apply merges host data and calls updateHost', async () => {
const { ProxyHosts, mockUpdateHost } = await renderPage()
render(
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<ProxyHosts />
</QueryClientProvider>
)
await waitFor(() => expect(screen.getByText('StagingHost')).toBeInTheDocument())
const selectBtn1 = screen.getByLabelText('Select StagingHost')
const selectBtn2 = screen.getByLabelText('Select CustomCertHost')
await userEvent.click(selectBtn1)
await userEvent.click(selectBtn2)
const bulkBtn = screen.getByText('Bulk Apply')
await userEvent.click(bulkBtn)
const modal = screen.getByText('Bulk Apply Settings').closest('div')!
const modalWithin = within(modal)
const checkboxes = modal.querySelectorAll('input[type="checkbox"]')
expect(checkboxes.length).toBeGreaterThan(0)
await userEvent.click(checkboxes[0])
const applyBtn = modalWithin.getByRole('button', { name: /Apply/ })
await userEvent.click(applyBtn)
await waitFor(() => {
expect(mockUpdateHost).toHaveBeenCalled()
})
const calls = vi.mocked(mockUpdateHost).mock.calls
expect(calls.length).toBeGreaterThanOrEqual(1)
const [calledUuid, calledData] = calls[0]
expect(typeof calledUuid).toBe('string')
expect(Object.prototype.hasOwnProperty.call(calledData, 'ssl_forced')).toBe(true)
})
})
export {}
@@ -4,16 +4,20 @@ import { act } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import type { ProxyHost } from '../../api/proxyHosts'
import ProxyHosts from '../ProxyHosts'
import { formatSettingLabel, settingHelpText, settingKeyToField, applyBulkSettingsToHosts } from '../../utils/proxyHostsHelpers'
import * as proxyHostsApi from '../../api/proxyHosts'
import * as certificatesApi from '../../api/certificates'
import * as accessListsApi from '../../api/accessLists'
import type { AccessList } from '../../api/accessLists'
import * as settingsApi from '../../api/settings'
import * as uptimeApi from '../../api/uptime'
// Certificate type not required in this spec
import type { UptimeMonitor } from '../../api/uptime'
// toast is mocked in other tests; not used here
vi.mock('react-hot-toast', () => ({
toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() },
}))
vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }))
vi.mock('../../api/proxyHosts', () => ({
getProxyHosts: vi.fn(),
@@ -28,6 +32,7 @@ vi.mock('../../api/certificates', () => ({ getCertificates: vi.fn() }))
vi.mock('../../api/accessLists', () => ({ accessListsApi: { list: vi.fn() } }))
vi.mock('../../api/settings', () => ({ getSettings: vi.fn() }))
vi.mock('../../api/backups', () => ({ createBackup: vi.fn() }))
vi.mock('../../api/uptime', () => ({ getMonitors: vi.fn() }))
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } })
@@ -42,7 +47,7 @@ const renderWithProviders = (ui: React.ReactNode) => {
import { createMockProxyHost } from '../../testUtils/createMockProxyHost'
const baseHost = (overrides: any = {}) => createMockProxyHost(overrides)
const baseHost = (overrides: Partial<ProxyHost> = {}) => createMockProxyHost(overrides)
describe('ProxyHosts - Coverage enhancements', () => {
beforeEach(() => vi.clearAllMocks())
@@ -79,7 +84,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
certificate: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
} as any)
} as ProxyHost)
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
@@ -151,7 +156,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.bulkUpdateACL).mockRejectedValue(new Error('Bad things'))
@@ -251,7 +256,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist', enabled: true, ip_rules: '[]', country_codes: '', local_network_only: false, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
@@ -275,7 +280,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
@@ -303,7 +308,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({ updated: 2, errors: [] })
@@ -331,7 +336,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
@@ -360,7 +365,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({ updated: 1, errors: [{ uuid: 's2', error: 'Bad' }] })
@@ -386,7 +391,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.bulkUpdateACL).mockRejectedValue(new Error('Bulk fail'))
@@ -459,7 +464,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
})
it('renders SSL states: custom, staging, letsencrypt variations', async () => {
const hostCustom = baseHost({ uuid: 'c1', name: 'Custom', domain_names: 'custom.com', ssl_forced: true, certificate: { provider: 'custom', name: 'CustomCert' } })
const hostCustom = baseHost({ uuid: 'c1', name: 'Custom', domain_names: 'custom.com', ssl_forced: true, certificate: { id: 123, uuid: 'cert-1', name: 'CustomCert', provider: 'custom', domains: 'custom.com', expires_at: '2026-01-01' } })
const hostStaging = baseHost({ uuid: 's1', name: 'Staging', domain_names: 'staging.com', ssl_forced: true })
const hostAuto = baseHost({ uuid: 'a1', name: 'Auto', domain_names: 'auto.com', ssl_forced: true })
const hostLets = baseHost({ uuid: 'l1', name: 'Lets', domain_names: 'lets.com', ssl_forced: true })
@@ -542,6 +547,87 @@ describe('ProxyHosts - Coverage enhancements', () => {
confirmSpy.mockRestore()
})
it('deletes associated uptime monitors when confirmed', async () => {
const host = baseHost({ uuid: 'del2', name: 'Del2', forward_host: '127.0.0.5' })
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([host])
vi.mocked(proxyHostsApi.deleteProxyHost).mockResolvedValue()
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
// uptime monitors associated with host
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([{ id: 'm1', name: 'm1', url: 'http://example', type: 'http', interval: 60, enabled: true, status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3, upstream_host: '127.0.0.5' } as UptimeMonitor])
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true)
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('Del2')).toBeTruthy())
const row = screen.getByText('Del2').closest('tr') as HTMLTableRowElement
const delButton = within(row).getByText('Delete')
await userEvent.click(delButton)
// Should call delete with deleteUptime true
await waitFor(() => expect(proxyHostsApi.deleteProxyHost).toHaveBeenCalledWith('del2', true))
confirmSpy.mockRestore()
})
it('ignores uptime API errors and deletes host without deleting uptime', async () => {
const host = baseHost({ uuid: 'del3', name: 'Del3', forward_host: '127.0.0.6' })
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([host])
vi.mocked(proxyHostsApi.deleteProxyHost).mockResolvedValue()
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
// Make getMonitors throw
vi.mocked(uptimeApi.getMonitors).mockRejectedValue(new Error('OOPS'))
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true)
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('Del3')).toBeTruthy())
const row = screen.getByText('Del3').closest('tr') as HTMLTableRowElement
const delButton = within(row).getByText('Delete')
await userEvent.click(delButton)
// Should call delete without second param
await waitFor(() => expect(proxyHostsApi.deleteProxyHost).toHaveBeenCalledWith('del3'))
confirmSpy.mockRestore()
})
it('applies bulk settings sequentially with progress and updates hosts', async () => {
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([
baseHost({ uuid: 'host-1', name: 'H1' }),
baseHost({ uuid: 'host-2', name: 'H2' }),
])
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.updateProxyHost).mockResolvedValue({} as ProxyHost)
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('H1')).toBeTruthy())
// Select both hosts
const headerCheckbox = screen.getAllByRole('checkbox')[0]
await userEvent.click(headerCheckbox)
// Open Bulk Apply modal
await userEvent.click(screen.getByText('Bulk Apply'))
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy())
// In the modal, find Force SSL row and enable apply and set value true
const forceLabel = screen.getByText('Force SSL')
const rowEl = forceLabel.closest('.p-2') as HTMLElement || forceLabel.closest('div') as HTMLElement
// Use within to find checkboxes within this row for robust selection
const rowCheckboxes = within(rowEl).getAllByRole('checkbox', { hidden: true })
if (rowCheckboxes.length >= 1) await userEvent.click(rowCheckboxes[0])
// Click Apply in the modal (narrow to modal scope)
const modal = screen.getByText('Bulk Apply Settings').closest('div') as HTMLElement
const applyBtn = within(modal).getByRole('button', { name: /Apply/i })
await userEvent.click(applyBtn)
// Expect updateProxyHost called for each host with ssl_forced true included in payload
await waitFor(() => expect(proxyHostsApi.updateProxyHost).toHaveBeenCalledTimes(2))
const calls = vi.mocked(proxyHostsApi.updateProxyHost).mock.calls
expect(calls.some(call => call[1] && (call[1] as Partial<ProxyHost>).ssl_forced === true)).toBeTruthy()
})
it('shows Unnamed when name missing', async () => {
const hostNoName = baseHost({ uuid: 'n1', name: '', domain_names: 'no-name.com' })
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([hostNoName])
@@ -668,7 +754,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-a1', name: 'A1', description: 'A1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
{ id: 2, uuid: 'acl-a2', name: 'A2', description: 'A2', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({ updated: 2, errors: [] })
renderWithProviders(<ProxyHosts />)
@@ -707,7 +793,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-1', name: 'List1', description: 'List 1', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
{ id: 2, uuid: 'acl-2', name: 'List2', description: 'List 2', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: true, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
@@ -744,7 +830,7 @@ describe('ProxyHosts - Coverage enhancements', () => {
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([
{ id: 1, uuid: 'acl-disable1', name: 'Disabled1', description: 'Disabled 1', type: 'blacklist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: false, created_at: '2025-01-01', updated_at: '2025-01-01' },
{ id: 2, uuid: 'acl-disable2', name: 'Disabled2', description: 'Disabled 2', type: 'whitelist' as const, ip_rules: '[]', country_codes: '', local_network_only: false, enabled: false, created_at: '2025-01-01', updated_at: '2025-01-01' },
] as any)
] as AccessList[])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
@@ -758,6 +844,120 @@ describe('ProxyHosts - Coverage enhancements', () => {
// Should show the 'No enabled access lists available' message
await waitFor(() => expect(screen.getByText('No enabled access lists available')).toBeTruthy())
})
it('formatSettingLabel, settingHelpText and settingKeyToField return expected values and defaults', () => {
expect(formatSettingLabel('ssl_forced')).toBe('Force SSL')
expect(formatSettingLabel('http2_support')).toBe('HTTP/2 Support')
expect(formatSettingLabel('hsts_enabled')).toBe('HSTS Enabled')
expect(formatSettingLabel('hsts_subdomains')).toBe('HSTS Subdomains')
expect(formatSettingLabel('block_exploits')).toBe('Block Exploits')
expect(formatSettingLabel('websocket_support')).toBe('Websockets Support')
expect(formatSettingLabel('unknown_key')).toBe('unknown_key')
expect(settingHelpText('ssl_forced')).toContain('Redirect all HTTP traffic')
expect(settingHelpText('http2_support')).toContain('Enable HTTP/2')
expect(settingHelpText('hsts_enabled')).toContain('Send HSTS header')
expect(settingHelpText('hsts_subdomains')).toContain('Include subdomains')
expect(settingHelpText('block_exploits')).toContain('Add common exploit-mitigation')
expect(settingHelpText('websocket_support')).toContain('Enable websocket proxying')
expect(settingHelpText('unknown_key')).toBe('')
expect(settingKeyToField('ssl_forced')).toBe('ssl_forced')
expect(settingKeyToField('http2_support')).toBe('http2_support')
expect(settingKeyToField('hsts_enabled')).toBe('hsts_enabled')
expect(settingKeyToField('hsts_subdomains')).toBe('hsts_subdomains')
expect(settingKeyToField('block_exploits')).toBe('block_exploits')
expect(settingKeyToField('websocket_support')).toBe('websocket_support')
expect(settingKeyToField('unknown_key')).toBe('unknown_key')
})
it('closes bulk apply modal when clicking backdrop', async () => {
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([
baseHost({ uuid: 's1', name: 'S1' }),
])
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('S1')).toBeTruthy())
const headerCheckbox = screen.getAllByRole('checkbox')[0]
const user = userEvent.setup()
await user.click(headerCheckbox)
await user.click(screen.getByText('Bulk Apply'))
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy())
// click backdrop
const overlay = document.querySelector('.fixed.inset-0')
if (overlay) await user.click(overlay)
await waitFor(() => expect(screen.queryByText('Bulk Apply Settings')).toBeNull())
})
it('shows toast error when updateHost rejects during bulk apply', async () => {
const h1 = baseHost({ uuid: 'host-1', name: 'H1' })
const h2 = baseHost({ uuid: 'host-2', name: 'H2' })
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue([h1, h2])
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([])
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue([])
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
// mock updateProxyHost to fail for host-2
vi.mocked(proxyHostsApi.updateProxyHost).mockImplementation(async (uuid: string) => {
if (uuid === 'host-2') throw new Error('update fail')
const result = baseHost({ uuid })
return result
})
renderWithProviders(<ProxyHosts />)
await waitFor(() => expect(screen.getByText('H1')).toBeTruthy())
// select both
const headerCheckbox = screen.getAllByRole('checkbox')[0]
const user = userEvent.setup()
await user.click(headerCheckbox)
// Open Bulk Apply
await user.click(screen.getByText('Bulk Apply'))
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeTruthy())
// enable Force SSL apply + set switch
const forceLabel = screen.getByText('Force SSL')
const rowEl = forceLabel.closest('.p-2') as HTMLElement || forceLabel.closest('div') as HTMLElement
// click apply checkbox and toggle switch reliably
const rowChecks = within(rowEl).getAllByRole('checkbox', { hidden: true })
if (rowChecks[0]) await user.click(rowChecks[0])
if (rowChecks[1]) await user.click(rowChecks[1])
// click Apply
const modal = screen.getByText('Bulk Apply Settings').closest('div') as HTMLElement
const applyBtn = within(modal).getByRole('button', { name: /Apply/i })
await user.click(applyBtn)
const toast = (await import('react-hot-toast')).toast
await waitFor(() => expect(toast.error).toHaveBeenCalled())
})
it('applyBulkSettingsToHosts returns error when host is not found and reports progress', async () => {
const hosts: ProxyHost[] = [] // no hosts
const hostUUIDs = ['missing-1']
const keysToApply = ['ssl_forced']
const bulkApplySettings: Record<string, { apply: boolean; value: boolean }> = { ssl_forced: { apply: true, value: true } }
const updateHost = vi.fn().mockResolvedValue({})
const setApplyProgress = vi.fn()
const result = await applyBulkSettingsToHosts({ hosts, hostUUIDs, keysToApply, bulkApplySettings, updateHost, setApplyProgress })
expect(result.errors).toBe(1)
expect(setApplyProgress).toHaveBeenCalled()
expect(updateHost).not.toHaveBeenCalled()
})
it('applyBulkSettingsToHosts handles updateHost rejection and counts errors', async () => {
const h1 = baseHost({ uuid: 'h1', name: 'H1' })
const hosts = [h1]
const hostUUIDs = ['h1']
const keysToApply = ['ssl_forced']
const bulkApplySettings: Record<string, { apply: boolean; value: boolean }> = { ssl_forced: { apply: true, value: true } }
const updateHost = vi.fn().mockRejectedValue(new Error('fail'))
const setApplyProgress = vi.fn()
const result = await applyBulkSettingsToHosts({ hosts, hostUUIDs, keysToApply, bulkApplySettings, updateHost, setApplyProgress })
expect(result.errors).toBe(1)
expect(updateHost).toHaveBeenCalled()
})
})
export {}
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import type { ProxyHost, BulkUpdateACLResponse } from '../../api/proxyHosts'
import ProxyHosts from '../ProxyHosts'
import * as proxyHostsApi from '../../api/proxyHosts'
import * as certificatesApi from '../../api/certificates'
@@ -37,7 +38,7 @@ const renderWithProviders = (ui: React.ReactNode) => {
)
}
const baseHost = (overrides: any = {}) => ({
const baseHost = (overrides: Partial<ProxyHost> = {}): ProxyHost => ({
uuid: 'host-1',
name: 'Host',
domain_names: 'example.com',
@@ -47,7 +48,17 @@ const baseHost = (overrides: any = {}) => ({
enabled: true,
ssl_forced: false,
websocket_support: false,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
block_exploits: false,
application: 'none',
locations: [],
certificate: null,
certificate_id: null,
access_list_id: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
})
@@ -68,9 +79,11 @@ describe('ProxyHosts progress apply', () => {
vi.mocked(settingsApi.getSettings).mockResolvedValue({})
// Create controllable promises for bulkUpdateACL invocations
const resolvers: Array<(value?: any) => void> = []
vi.mocked(proxyHostsApi.bulkUpdateACL).mockImplementation((_hostUUIDs, _aclId) => {
return new Promise((resolve) => { resolvers.push(resolve) })
const resolvers: Array<(value: BulkUpdateACLResponse) => void> = []
vi.mocked(proxyHostsApi.bulkUpdateACL).mockImplementation((...args: unknown[]) => {
const [_hostUUIDs, _aclId] = args
void _hostUUIDs; void _aclId
return new Promise((resolve: (v: BulkUpdateACLResponse) => void) => { resolvers.push(resolve); })
})
renderWithProviders(<ProxyHosts />)
@@ -0,0 +1,51 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Uptime from '../Uptime'
import * as uptimeApi from '../../api/uptime'
vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }))
vi.mock('../../api/uptime')
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
const renderWithProviders = (ui: React.ReactNode) => {
const qc = createQueryClient()
return render(
<QueryClientProvider client={qc}>
{ui}
</QueryClientProvider>
)
}
describe('Uptime page', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders no monitors message', async () => {
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([])
renderWithProviders(<Uptime />)
expect(await screen.findByText(/No monitors found/i)).toBeTruthy()
})
it('calls updateMonitor when toggling monitoring', async () => {
const monitor = {
id: 'm1', name: 'Test Monitor', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3, proxy_host_id: 1,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
vi.mocked(uptimeApi.updateMonitor).mockResolvedValue({ ...monitor, enabled: false })
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('Test Monitor')).toBeInTheDocument())
const card = screen.getByText('Test Monitor').closest('div') as HTMLElement
const settingsBtn = within(card).getByTitle('Monitor settings')
await userEvent.click(settingsBtn)
const toggleBtn = within(card).getByText('Disable Monitoring')
await userEvent.click(toggleBtn)
await waitFor(() => expect(uptimeApi.updateMonitor).toHaveBeenCalledWith('m1', { enabled: false }))
})
})