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:
@@ -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 }))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user