586 lines
17 KiB
TypeScript
586 lines
17 KiB
TypeScript
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 { createMockProxyHost } from '../../testUtils/createMockProxyHost';
|
|
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 { toast } from 'react-hot-toast';
|
|
|
|
// Mock toast
|
|
vi.mock('react-hot-toast', () => ({
|
|
toast: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
loading: vi.fn(),
|
|
dismiss: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock API modules
|
|
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/backups', () => ({
|
|
createBackup: vi.fn(),
|
|
getBackups: vi.fn(),
|
|
restoreBackup: vi.fn(),
|
|
deleteBackup: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../api/certificates', () => ({
|
|
getCertificates: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../api/accessLists', () => ({
|
|
accessListsApi: {
|
|
list: vi.fn(),
|
|
get: vi.fn(),
|
|
getTemplates: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn(),
|
|
testIP: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../api/settings', () => ({
|
|
getSettings: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../hooks/useSecurityHeaders', () => ({
|
|
useSecurityHeaderProfiles: vi.fn(() => ({ data: [], isLoading: false, error: null })),
|
|
}));
|
|
|
|
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 mockAccessLists = [
|
|
{
|
|
id: 1,
|
|
uuid: 'acl-1',
|
|
name: 'Admin Only',
|
|
description: 'Admin access',
|
|
type: 'whitelist' as const,
|
|
ip_rules: '[]',
|
|
country_codes: '',
|
|
local_network_only: false,
|
|
enabled: true,
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 2,
|
|
uuid: 'acl-2',
|
|
name: 'Local Network',
|
|
description: 'Local network only',
|
|
type: 'whitelist' as const,
|
|
ip_rules: '[]',
|
|
country_codes: '',
|
|
local_network_only: true,
|
|
enabled: true,
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 3,
|
|
uuid: 'acl-3',
|
|
name: 'Disabled ACL',
|
|
description: 'This is disabled',
|
|
type: 'blacklist' as const,
|
|
ip_rules: '[]',
|
|
country_codes: '',
|
|
local_network_only: false,
|
|
enabled: false,
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
},
|
|
];
|
|
|
|
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 ACL Modal', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
// Setup default mocks
|
|
vi.mocked(proxyHostsApi.getProxyHosts).mockResolvedValue(mockProxyHosts);
|
|
vi.mocked(certificatesApi.getCertificates).mockResolvedValue([]);
|
|
vi.mocked(accessListsApi.accessListsApi.list).mockResolvedValue(mockAccessLists);
|
|
vi.mocked(settingsApi.getSettings).mockResolvedValue({});
|
|
});
|
|
|
|
it('renders Manage ACL button when hosts are selected', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select all hosts using the select-all checkbox
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
// Manage ACL button should appear
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('opens bulk ACL modal when Manage ACL is clicked', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
|
|
// Click Manage ACL
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Modal should open
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Apply Access List')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('shows Apply ACL and Remove ACL toggle buttons', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Should show toggle buttons
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Apply ACL' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: 'Remove ACL' })).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('shows only enabled access lists in the selection', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Should show enabled ACLs
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
expect(screen.getByText('Local Network')).toBeTruthy();
|
|
});
|
|
|
|
// Should NOT show disabled ACL
|
|
expect(screen.queryByText('Disabled ACL')).toBeNull();
|
|
});
|
|
|
|
it('shows ACL type alongside name', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Should show type - the modal should display ACL types
|
|
await waitFor(() => {
|
|
// Check that the ACL list is rendered with names
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
expect(screen.getByText('Local Network')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('has Apply button disabled when no ACL is selected', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const selectAll = screen.getByLabelText('Select all rows');
|
|
await userEvent.click(selectAll);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Wait for modal to open
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Apply Access List')).toBeTruthy();
|
|
});
|
|
|
|
// Apply action button should be disabled (the one with bg-blue-600 class, not the toggle)
|
|
// The action button text is "Apply" or "Apply (N)"
|
|
const buttons = screen.getAllByRole('button');
|
|
const applyButton = buttons.find(btn => {
|
|
const text = btn.textContent?.trim() || '';
|
|
// Match "Apply" exactly but not "Apply ACL" (which is the toggle)
|
|
const isApplyAction = text === 'Apply' || /^Apply \(\d+\)$/.test(text);
|
|
return isApplyAction;
|
|
});
|
|
expect(applyButton).toBeTruthy();
|
|
expect((applyButton as HTMLButtonElement)?.disabled).toBe(true);
|
|
});
|
|
|
|
it('enables Apply button when ACL is selected', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
const user = userEvent.setup();
|
|
await user.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await user.click(screen.getByText('Manage ACL'));
|
|
|
|
// Wait for ACL list
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
});
|
|
|
|
// Select an ACL
|
|
const aclCheckboxes = screen.getAllByRole('checkbox');
|
|
// Find the checkbox for Admin Only (should be after the host selection checkboxes)
|
|
const aclCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Admin Only')
|
|
);
|
|
if (aclCheckbox) {
|
|
await userEvent.click(aclCheckbox);
|
|
}
|
|
|
|
// Apply button should be enabled and show count
|
|
await waitFor(() => {
|
|
const applyButton = screen.getByRole('button', { name: /Apply \(1\)/ });
|
|
expect(applyButton).toBeTruthy();
|
|
expect(applyButton).toHaveProperty('disabled', false);
|
|
});
|
|
});
|
|
|
|
it('can select multiple ACLs', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Wait for ACL list
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
});
|
|
|
|
// Select multiple ACLs
|
|
const aclCheckboxes = screen.getAllByRole('checkbox');
|
|
const adminCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Admin Only')
|
|
);
|
|
const localCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Local Network')
|
|
);
|
|
|
|
if (adminCheckbox) await userEvent.click(adminCheckbox);
|
|
if (localCheckbox) await userEvent.click(localCheckbox);
|
|
|
|
// Apply button should show count of 2
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /Apply \(2\)/ })).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('applies ACL to selected hosts successfully', async () => {
|
|
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({
|
|
updated: 2,
|
|
errors: [],
|
|
});
|
|
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Wait for ACL list and select one
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
});
|
|
|
|
const aclCheckboxes = screen.getAllByRole('checkbox');
|
|
const adminCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Admin Only')
|
|
);
|
|
if (adminCheckbox) await userEvent.click(adminCheckbox);
|
|
|
|
// Click Apply
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /Apply \(1\)/ })).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByRole('button', { name: /Apply \(1\)/ }));
|
|
|
|
// Should call API
|
|
await waitFor(() => {
|
|
expect(proxyHostsApi.bulkUpdateACL).toHaveBeenCalledWith(
|
|
['host-1', 'host-2'],
|
|
1
|
|
);
|
|
});
|
|
|
|
// Should show success toast
|
|
await waitFor(() => {
|
|
expect(toast.success).toHaveBeenCalledWith('Updated successfully');
|
|
});
|
|
});
|
|
|
|
it('shows Remove ACL confirmation when Remove is selected', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Wait for modal and find Remove ACL toggle (it's a button with flex-1 class)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Apply Access List')).toBeTruthy();
|
|
});
|
|
|
|
// Find the toggle button by looking for flex-1 class
|
|
const buttons = screen.getAllByRole('button');
|
|
const removeToggle = buttons.find(btn =>
|
|
btn.textContent === 'Remove ACL' && btn.className.includes('flex-1')
|
|
);
|
|
expect(removeToggle).toBeTruthy();
|
|
if (removeToggle) await userEvent.click(removeToggle);
|
|
|
|
// Should show warning message
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/will become publicly accessible/i)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('closes modal on Cancel', async () => {
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Modal should open
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Apply Access List')).toBeTruthy();
|
|
});
|
|
|
|
// Click Cancel
|
|
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
|
|
// Modal should close
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Apply Access List')).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('clears selection and closes modal after successful apply', async () => {
|
|
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({
|
|
updated: 2,
|
|
errors: [],
|
|
});
|
|
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Select ACL and apply
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
});
|
|
|
|
const aclCheckboxes = screen.getAllByRole('checkbox');
|
|
const adminCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Admin Only')
|
|
);
|
|
if (adminCheckbox) await userEvent.click(adminCheckbox);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /Apply \(1\)/ })).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByRole('button', { name: /Apply \(1\)/ }));
|
|
|
|
// Wait for completion
|
|
await waitFor(() => {
|
|
expect(proxyHostsApi.bulkUpdateACL).toHaveBeenCalled();
|
|
});
|
|
|
|
// Modal should close
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Apply Access List')).toBeNull();
|
|
});
|
|
|
|
// Selection should be cleared (Manage ACL button should be gone)
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Manage ACL')).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('shows error toast on API failure', async () => {
|
|
vi.mocked(proxyHostsApi.bulkUpdateACL).mockResolvedValue({
|
|
updated: 1,
|
|
errors: [{ uuid: 'host-2', error: 'Failed' }],
|
|
});
|
|
|
|
renderWithProviders(<ProxyHosts />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test Host 1')).toBeTruthy();
|
|
});
|
|
|
|
// Select hosts and open modal
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
await userEvent.click(checkboxes[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage ACL')).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByText('Manage ACL'));
|
|
|
|
// Select ACL and apply
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Admin Only')).toBeTruthy();
|
|
});
|
|
|
|
const aclCheckboxes = screen.getAllByRole('checkbox');
|
|
const adminCheckbox = aclCheckboxes.find(cb =>
|
|
cb.closest('label')?.textContent?.includes('Admin Only')
|
|
);
|
|
if (adminCheckbox) await userEvent.click(adminCheckbox);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /Apply \(1\)/ })).toBeTruthy();
|
|
});
|
|
await userEvent.click(screen.getByRole('button', { name: /Apply \(1\)/ }));
|
|
|
|
// Should show error toast
|
|
await waitFor(() => {
|
|
expect(toast.error).toHaveBeenCalledWith('Failed to update');
|
|
});
|
|
});
|
|
});
|