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( {ui} ); }; 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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'); }); }); });