Files
Charon/tests/security-enforcement/zzz-security-ui/access-lists-crud.spec.ts
2026-03-04 18:34:49 +00:00

1060 lines
40 KiB
TypeScript

/**
* Access Lists CRUD E2E Tests
*
* Tests the Access Lists (ACL) management functionality including:
* - List view with table, columns, and empty states
* - Create access list with form validation (IP/Geo whitelist/blacklist)
* - Read/view access list details
* - Update existing access lists
* - Delete access lists with confirmation and backup
* - Test IP functionality
* - Integration with Proxy Hosts
*
* @see /projects/Charon/docs/plans/current_spec.md
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../../fixtures/auth-fixtures';
import { waitForLoadingComplete, waitForToast, waitForModal, waitForDialog, waitForDebounce } from '../../utils/wait-helpers';
import { waitForAPIHealth } from '../../utils/api-helpers';
import { clickSwitch } from '../../utils/ui-helpers';
import {
allowOnlyAccessList,
denyOnlyAccessList,
mixedRulesAccessList,
authEnabledAccessList,
generateAccessList,
invalidACLConfigs,
type AccessListConfig,
} from '../../fixtures/access-lists';
import { generateUniqueId, generateIPAddress, generateCIDR } from '../../fixtures/test-data';
test.describe('Access Lists - CRUD Operations', () => {
test.beforeEach(async ({ page, adminUser }) => {
await waitForAPIHealth(page.request);
await loginUser(page, adminUser);
await waitForLoadingComplete(page);
// Navigate to access lists page - supports both routes
await page.goto('/access-lists');
await waitForLoadingComplete(page);
});
// Helper to get the Create Access List button
const getCreateButton = (page: import('@playwright/test').Page) =>
page.getByRole('button', { name: /create.*access.*list/i }).first();
// Helper to get Save/Create button in form
const getSaveButton = (page: import('@playwright/test').Page) =>
page.getByRole('button', { name: /^(create|update|save)$/i }).first();
// Helper to get Cancel button in form (visible in form footer)
const getCancelButton = (page: import('@playwright/test').Page) =>
page.getByRole('button', { name: /^cancel$/i }).first();
test.describe('List View', () => {
test('should display access lists page with title', async ({ page }) => {
await test.step('Verify page title is visible', async () => {
// Use .first() to avoid strict mode violation (h1 title + h3 empty state)
const heading = page.getByRole('heading', { name: /access.*lists/i }).first();
await expect(heading).toBeVisible();
});
await test.step('Verify Create Access List button is present', async () => {
const createButton = getCreateButton(page);
await expect(createButton).toBeVisible();
});
});
test('should show correct table columns', async ({ page }) => {
await test.step('Verify table headers exist', async () => {
// The table should have columns: Name, Type, Rules, Status, Actions
const expectedColumns = [
/name/i,
/type/i,
/rules/i,
/status/i,
/actions/i,
];
for (const pattern of expectedColumns) {
const header = page.getByRole('columnheader', { name: pattern });
const headerExists = await header.count() > 0;
if (headerExists) {
await expect(header.first()).toBeVisible();
}
}
});
});
test('should display empty state when no ACLs exist', async ({ page }) => {
await test.step('Check for empty state or existing ACLs', async () => {
await waitForDebounce(page, { delay: 1000 }); // Allow initial data fetch
const emptyStateHeading = page.getByRole('heading', { name: /no.*access.*lists/i });
const table = page.getByRole('table');
const hasEmptyState = await emptyStateHeading.isVisible().catch(() => false);
const hasTable = await table.isVisible().catch(() => false);
expect(hasEmptyState || hasTable).toBeTruthy();
if (hasEmptyState) {
// Empty state should have a Create action
const createAction = getCreateButton(page);
await expect(createAction).toBeVisible();
}
});
});
test('should show loading skeleton while fetching data', async ({ page }) => {
await test.step('Navigate and observe loading state', async () => {
await page.reload();
await waitForDebounce(page, { delay: 2000 }); // Allow network requests and render
const table = page.getByRole('table');
const emptyState = page.getByRole('heading', { name: /no.*access.*lists/i });
const hasTable = await table.isVisible().catch(() => false);
const hasEmpty = await emptyState.isVisible().catch(() => false);
expect(hasTable || hasEmpty).toBeTruthy();
});
});
test('should navigate to access lists from sidebar', async ({ page }) => {
await test.step('Navigate via sidebar', async () => {
// Go to a different page first
await page.goto('/');
await waitForLoadingComplete(page);
// Look for Security menu or Access Lists link in sidebar
const securityMenu = page.getByRole('button', { name: /security/i });
const accessListsLink = page.getByRole('link', { name: /access.*lists/i });
if (await securityMenu.isVisible().catch(() => false)) {
await securityMenu.click();
await waitForDebounce(page, { delay: 300 }); // Allow menu to expand
}
if (await accessListsLink.isVisible().catch(() => false)) {
await accessListsLink.click();
await waitForLoadingComplete(page);
// Verify we're on access lists page (use .first() to avoid strict mode)
const heading = page.getByRole('heading', { name: /access.*lists/i }).first();
await expect(heading).toBeVisible({ timeout: 5000 });
}
});
});
test('should display ACL details (name, type, rules)', async ({ page }) => {
await test.step('Check table displays ACL information', async () => {
const table = page.getByRole('table');
const hasTable = await table.isVisible().catch(() => false);
if (hasTable) {
const rows = page.locator('tbody tr');
const rowCount = await rows.count();
if (rowCount > 0) {
const firstRow = rows.first();
await expect(firstRow).toBeVisible();
// Check for expected content patterns (type badges)
const typeBadge = firstRow.locator('text=/allow|deny/i');
const hasBadge = await typeBadge.count() > 0;
expect(hasBadge || true).toBeTruthy();
}
}
});
});
});
test.describe('Create Access List', () => {
test('should open create form when Create button clicked', async ({ page }) => {
await test.step('Click Create Access List button', async () => {
const createButton = getCreateButton(page);
await createButton.click();
});
await test.step('Verify form opens', async () => {
await waitForModal(page); // Wait for form modal to open
// The form should be visible (Card component with heading)
const formTitle = page.getByRole('heading', { name: /create.*access.*list/i });
await expect(formTitle).toBeVisible({ timeout: 5000 });
// Verify essential form fields are present
const nameInput = page.locator('#name');
await expect(nameInput).toBeVisible();
});
});
test('should validate required name field', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page); // Wait for form modal to open
});
await test.step('Try to submit with empty name', async () => {
const saveButton = getSaveButton(page);
await saveButton.click();
// Form should show validation error or prevent submission
const nameInput = page.locator('#name');
const isInvalid = await nameInput.evaluate((el: HTMLInputElement) =>
el.validity.valid === false || el.getAttribute('aria-invalid') === 'true'
).catch(() => false);
// HTML5 validation should prevent submission
expect(isInvalid || true).toBeTruthy();
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should create ACL with name only (IP whitelist)', async ({ page }) => {
const aclName = `Test ACL ${generateUniqueId()}`;
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Fill in ACL name', async () => {
const nameInput = page.locator('#name');
await nameInput.fill(aclName);
});
await test.step('Verify type selector is visible', async () => {
const typeSelect = page.locator('#type');
await expect(typeSelect).toBeVisible();
// Default should be whitelist
const selectedType = await typeSelect.inputValue();
expect(selectedType).toBe('whitelist');
});
await test.step('Submit form', async () => {
const saveButton = getSaveButton(page);
await saveButton.click();
await waitForLoadingComplete(page);
});
await test.step('Verify ACL was created', async () => {
// Wait for form to close (indicates successful submission)
const formTitle = page.getByRole('heading', { name: /create.*access.*list/i });
await expect(formTitle).not.toBeVisible({ timeout: 10000 });
// Then verify ACL appears in list or success toast
const successToast = page.getByText(/success|created|saved/i);
const aclInList = page.getByText(aclName);
const hasSuccess = await successToast.isVisible({ timeout: 3000 }).catch(() => false);
const hasAclInList = await aclInList.isVisible({ timeout: 5000 }).catch(() => false);
expect(hasSuccess || hasAclInList).toBeTruthy();
});
});
test('should add client IP addresses', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Fill in name', async () => {
await page.locator('#name').fill(`IP Test ACL ${generateUniqueId()}`);
});
await test.step('Ensure not local network only mode', async () => {
// Wait for IP rules section to be visible (indicates form is ready)
await page.waitForSelector('text=IP Addresses / CIDR Ranges', { timeout: 5000 });
});
await test.step('Add IP address', async () => {
// Find IP input field using locator with placeholder
const ipInput = page.locator('input[placeholder="192.168.1.0/24 or 10.0.0.1"]');
await ipInput.waitFor({ state: 'visible', timeout: 5000 });
await ipInput.fill('192.168.1.100');
// Press Enter to add the IP (alternative to clicking Add button)
await ipInput.press('Enter');
await waitForDebounce(page);
// Verify IP was added to list (should appear as a separate item in the form)
const addedIP = page.getByText('192.168.1.100');
await expect(addedIP).toBeVisible({ timeout: 5000 });
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should add CIDR ranges', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Fill in name', async () => {
await page.locator('#name').fill(`CIDR Test ACL ${generateUniqueId()}`);
});
await test.step('Ensure not local network only mode', async () => {
// Wait for IP rules section to be visible (indicates form is ready)
await page.waitForSelector('text=IP Addresses / CIDR Ranges', { timeout: 5000 });
});
await test.step('Add CIDR range', async () => {
const ipInput = page.locator('input[placeholder="192.168.1.0/24 or 10.0.0.1"]');
await ipInput.waitFor({ state: 'visible', timeout: 5000 });
await ipInput.fill('10.0.0.0/8');
// Press Enter to add the CIDR (alternative to clicking Add button)
await ipInput.press('Enter');
await waitForDebounce(page);
const addedCIDR = page.getByText('10.0.0.0/8');
await expect(addedCIDR).toBeVisible({ timeout: 5000 });
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should select blacklist type', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Change type to blacklist', async () => {
const typeSelect = page.locator('#type');
await typeSelect.selectOption('blacklist');
// Verify selection
const selectedType = await typeSelect.inputValue();
expect(selectedType).toBe('blacklist');
});
await test.step('Verify recommended badge appears', async () => {
// Blacklist should show "Recommended" info box
const recommendedInfo = page.getByText(/recommended.*block.*lists/i);
const hasInfo = await recommendedInfo.isVisible().catch(() => false);
expect(hasInfo || true).toBeTruthy();
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should select geo-blacklist type and add countries', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Fill in name', async () => {
await page.locator('#name').fill(`Geo ACL ${generateUniqueId()}`);
});
await test.step('Change type to geo_blacklist', async () => {
const typeSelect = page.locator('#type');
await typeSelect.selectOption('geo_blacklist');
});
await test.step('Add country', async () => {
// Find country selector
const countrySelect = page.locator('select').filter({ hasText: /add.*country/i });
if (await countrySelect.isVisible().catch(() => false)) {
await countrySelect.selectOption('CN');
// Verify country was added
const addedCountry = page.getByText(/china/i);
await expect(addedCountry).toBeVisible({ timeout: 3000 });
}
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should toggle enabled/disabled state', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Toggle enabled switch', async () => {
const enabledSwitch = page.getByLabel(/enabled/i).first();
if (await enabledSwitch.isVisible().catch(() => false)) {
const wasChecked = await enabledSwitch.isChecked();
await clickSwitch(enabledSwitch);
const isNowChecked = await enabledSwitch.isChecked();
expect(isNowChecked).toBe(!wasChecked);
}
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should show success toast on creation', async ({ page }) => {
const aclName = `Toast Test ACL ${generateUniqueId()}`;
await test.step('Create ACL', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
await page.locator('#name').fill(aclName);
await getSaveButton(page).click();
});
await test.step('Verify success notification', async () => {
// Wait for toast or list update
await waitForLoadingComplete(page);
const successToast = page.getByText(/success|created/i);
const aclInList = page.getByText(aclName);
const hasToast = await successToast.isVisible({ timeout: 5000 }).catch(() => false);
const hasAcl = await aclInList.isVisible({ timeout: 5000 }).catch(() => false);
expect(hasToast || hasAcl).toBeTruthy();
});
});
test('should show security presets for blacklist type', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Select blacklist type', async () => {
const typeSelect = page.locator('#type');
await typeSelect.selectOption('blacklist');
});
await test.step('Verify security presets section appears', async () => {
const presetsSection = page.getByText(/security.*presets/i);
await expect(presetsSection).toBeVisible({ timeout: 3000 });
// Show presets button
const showPresetsButton = page.getByRole('button', { name: /show.*presets/i });
if (await showPresetsButton.isVisible().catch(() => false)) {
await showPresetsButton.click();
// Verify preset options appear
const presetOption = page.getByText(/botnet|scanner|abuse/i);
const hasPresets = await presetOption.count() > 0;
expect(hasPresets || true).toBeTruthy();
}
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
test('should have Get My IP button', async ({ page }) => {
await test.step('Open create form', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
});
await test.step('Ensure IP rules section is visible', async () => {
// Wait for IP rules section to be visible (indicates form is ready)
await page.waitForSelector('text=IP Addresses / CIDR Ranges', { timeout: 5000 });
});
await test.step('Verify Get My IP button exists', async () => {
// Look for the Get My IP button
const getMyIPButton = page.getByRole('button', { name: /get.*my.*ip/i });
await expect(getMyIPButton).toBeVisible({ timeout: 5000 });
});
await test.step('Close form', async () => {
await getCancelButton(page).click();
});
});
});
test.describe('Update Access List', () => {
test('should open edit form with existing values', async ({ page }) => {
await test.step('Find and click Edit button', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
// Verify form opens with "Edit" title
const formTitle = page.getByRole('heading', { name: /edit.*access.*list/i });
await expect(formTitle).toBeVisible({ timeout: 5000 });
// Verify name field is populated
const nameInput = page.locator('#name');
const nameValue = await nameInput.inputValue();
expect(nameValue.length > 0).toBeTruthy();
// Close form
await getCancelButton(page).click();
}
});
});
test('should update ACL name', async ({ page }) => {
await test.step('Edit an ACL if available', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
const nameInput = page.locator('#name');
const originalName = await nameInput.inputValue();
// Update name
const newName = `Updated ACL ${generateUniqueId()}`;
await nameInput.clear();
await nameInput.fill(newName);
// Save
await getSaveButton(page).click();
await waitForLoadingComplete(page);
// Verify update
const updatedAcl = page.getByText(newName);
const hasUpdated = await updatedAcl.isVisible({ timeout: 5000 }).catch(() => false);
expect(hasUpdated || true).toBeTruthy();
}
});
});
test('should add/remove client IPs', async ({ page }) => {
await test.step('Edit ACL and modify IP rules', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
// Ensure IP mode is enabled
const localNetworkSwitch = page.getByLabel(/local.*network.*only/i);
if (await localNetworkSwitch.isChecked().catch(() => false)) {
await clickSwitch(localNetworkSwitch);
}
// Add new IP
const ipInput = page.locator('input[placeholder*="192.168"], input[placeholder*="CIDR"]').first();
if (await ipInput.isVisible().catch(() => false)) {
await ipInput.fill('172.16.0.1');
const addButton = page.locator('button').filter({ has: page.locator('svg.lucide-plus') }).first();
await addButton.click();
}
// Cancel without saving
await getCancelButton(page).click();
}
});
});
test('should toggle ACL type', async ({ page }) => {
await test.step('Edit and toggle type', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
const typeSelect = page.locator('#type');
const currentType = await typeSelect.inputValue();
// Toggle between whitelist and blacklist
if (currentType === 'whitelist') {
await typeSelect.selectOption('blacklist');
} else if (currentType === 'blacklist') {
await typeSelect.selectOption('whitelist');
}
// Cancel without saving
await getCancelButton(page).click();
}
});
});
test('should show success toast on update', async ({ page }) => {
await test.step('Update ACL and verify toast', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
// Make a small change to description
const descriptionInput = page.locator('#description');
await descriptionInput.fill(`Updated at ${new Date().toISOString()}`);
await getSaveButton(page).click();
await waitForLoadingComplete(page);
// Wait for success indication
const successToast = page.getByText(/success|updated|saved/i);
const hasSuccess = await successToast.isVisible({ timeout: 5000 }).catch(() => false);
expect(hasSuccess || true).toBeTruthy();
}
});
});
});
test.describe('Delete Access List', () => {
test('should show delete confirmation dialog', async ({ page }) => {
await test.step('Click Delete button', async () => {
const deleteButtons = page.locator('button[title*="Delete"], button[title*="delete"]');
const deleteCount = await deleteButtons.count();
if (deleteCount > 0) {
await deleteButtons.first().click();
await waitForDialog(page);
// Confirmation dialog should appear
const dialog = page.getByRole('dialog');
const hasDialog = await dialog.isVisible({ timeout: 3000 }).catch(() => false);
if (hasDialog) {
// Dialog should have confirm and cancel buttons
const confirmButton = dialog.getByRole('button', { name: /delete/i });
const cancelButton = dialog.getByRole('button', { name: /cancel/i });
await expect(confirmButton).toBeVisible();
await expect(cancelButton).toBeVisible();
// Cancel the delete
await cancelButton.click();
}
}
});
});
test('should cancel delete when confirmation dismissed', async ({ page }) => {
await test.step('Trigger delete and cancel', async () => {
const deleteButtons = page.locator('button[title*="Delete"], button[title*="delete"]');
const deleteCount = await deleteButtons.count();
if (deleteCount > 0) {
// Count rows before
const rowsBefore = await page.locator('tbody tr').count();
await deleteButtons.first().click();
await waitForDialog(page);
const dialog = page.getByRole('dialog');
if (await dialog.isVisible().catch(() => false)) {
await dialog.getByRole('button', { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 3000 });
// Rows should remain unchanged
const rowsAfter = await page.locator('tbody tr').count();
expect(rowsAfter).toBe(rowsBefore);
}
}
});
});
test('should show delete confirmation with ACL name', async ({ page }) => {
await test.step('Verify confirmation message includes ACL info', async () => {
const deleteButtons = page.locator('button[title*="Delete"], button[title*="delete"]');
const deleteCount = await deleteButtons.count();
if (deleteCount > 0) {
await deleteButtons.first().click();
await waitForDialog(page);
const dialog = page.getByRole('dialog');
if (await dialog.isVisible().catch(() => false)) {
const dialogText = await dialog.textContent();
expect(dialogText).toBeTruthy();
// Cancel
await dialog.getByRole('button', { name: /cancel/i }).click();
}
}
});
});
test('should create backup before deletion', async ({ page }) => {
await test.step('Verify backup toast on delete', async () => {
// This test verifies the backup-before-delete functionality
const deleteButtons = page.locator('button[title*="Delete"], button[title*="delete"]');
const deleteCount = await deleteButtons.count();
if (deleteCount > 0) {
// For safety, don't actually delete - just verify the dialog mentions backup
await deleteButtons.first().click();
await waitForDialog(page);
const dialog = page.getByRole('dialog');
if (await dialog.isVisible().catch(() => false)) {
// The delete button text or dialog content should reference backup
const dialogText = await dialog.textContent();
// Cancel without deleting
await dialog.getByRole('button', { name: /cancel/i }).click();
}
}
});
});
test('should delete from edit form', async ({ page }) => {
await test.step('Open edit form and find delete button', async () => {
const editButtons = page.getByRole('button', { name: /edit/i });
const editCount = await editButtons.count();
if (editCount > 0) {
await editButtons.first().click();
await waitForModal(page, /edit|access.*list/i);
// Look for delete button in form
const deleteInForm = page.getByRole('button', { name: /delete/i });
await expect(deleteInForm).toBeVisible();
// Cancel without deleting
await getCancelButton(page).click();
}
});
});
});
test.describe('Test IP Functionality', () => {
test('should open Test IP dialog', async ({ page }) => {
await test.step('Find and click Test button', async () => {
// Look for test button with TestTube icon
const testButtons = page.locator('button[title*="Test"], button[title*="test"]');
const testCount = await testButtons.count();
if (testCount > 0) {
await testButtons.first().click();
await waitForDialog(page);
// Test IP dialog should open
const dialog = page.getByRole('dialog');
const hasDialog = await dialog.isVisible({ timeout: 3000 }).catch(() => false);
if (hasDialog) {
const dialogTitle = dialog.getByRole('heading', { name: /test.*ip/i });
await expect(dialogTitle).toBeVisible();
// Close dialog
await dialog
.getByRole('button', { name: /^close$/i })
.filter({ hasText: /^close$/i })
.click();
}
}
});
});
test('should have IP input field in test dialog', async ({ page }) => {
await test.step('Verify IP input in test dialog', async () => {
const testButtons = page.locator('button[title*="Test"], button[title*="test"]');
const testCount = await testButtons.count();
if (testCount > 0) {
await testButtons.first().click();
await waitForDialog(page);
const dialog = page.getByRole('dialog');
if (await dialog.isVisible().catch(() => false)) {
// Should have IP input - placeholder is "192.168.1.100"
const ipInput = dialog.locator('input[placeholder*="192.168"]');
await expect(ipInput.first()).toBeVisible();
// Should have Test button
const testButton = dialog.getByRole('button', { name: /test/i });
await expect(testButton).toBeVisible();
// Close dialog
await dialog
.getByRole('button', { name: /^close$/i })
.filter({ hasText: /^close$/i })
.click();
}
}
});
});
});
test.describe('Bulk Operations', () => {
test('should show row selection checkboxes', async ({ page }) => {
await test.step('Check for selectable rows', async () => {
const selectAllCheckbox = page.locator('thead').getByRole('checkbox');
const rowCheckboxes = page.locator('tbody').getByRole('checkbox');
const hasSelectAll = await selectAllCheckbox.count() > 0;
const hasRowCheckboxes = await rowCheckboxes.count() > 0;
// Selection is available if we have checkboxes
if (hasSelectAll || hasRowCheckboxes) {
if (hasSelectAll) {
await selectAllCheckbox.first().click();
// Should show bulk action bar
const bulkBar = page.getByText(/selected/i);
const hasBulkBar = await bulkBar.isVisible().catch(() => false);
expect(hasBulkBar || true).toBeTruthy();
// Deselect
await selectAllCheckbox.first().click();
}
}
});
});
test('should show bulk delete button when items selected', async ({ page }) => {
await test.step('Select items and verify bulk delete', async () => {
const selectAllCheckbox = page.locator('thead').getByRole('checkbox');
if (await selectAllCheckbox.isVisible().catch(() => false)) {
await selectAllCheckbox.click();
await waitForDebounce(page);
// Look for bulk delete button in header
const bulkDeleteButton = page.getByRole('button', { name: /delete.*\(/i });
const hasBulkDelete = await bulkDeleteButton.isVisible().catch(() => false);
expect(hasBulkDelete || true).toBeTruthy();
// Deselect
await selectAllCheckbox.click();
}
});
});
});
test.describe('ACL Integration with Proxy Hosts', () => {
test('should navigate between Access Lists and Proxy Hosts', async ({ page }) => {
await test.step('Navigate to Proxy Hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
const heading = page.getByRole('heading', { name: /proxy.*hosts/i });
const hasHeading = await heading.isVisible({ timeout: 5000 }).catch(() => false);
expect(hasHeading || true).toBeTruthy();
});
await test.step('Navigate back to Access Lists', async () => {
await page.goto('/access-lists');
await waitForLoadingComplete(page);
// Use .first() to avoid strict mode violation
const heading = page.getByRole('heading', { name: /access.*lists/i }).first();
await expect(heading).toBeVisible({ timeout: 5000 });
});
});
});
test.describe('Form Validation', () => {
test('should reject empty name', async ({ page }) => {
await test.step('Try to create with empty name', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
// Leave name empty and try to submit
const saveButton = getSaveButton(page);
await saveButton.click();
// Should not close form (validation error)
const formTitle = page.getByRole('heading', { name: /create.*access.*list/i });
await expect(formTitle).toBeVisible();
await getCancelButton(page).click();
});
});
test('should handle special characters in name', async ({ page }) => {
await test.step('Test special characters', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
const nameInput = page.locator('#name');
// Test with safe special characters
await nameInput.fill('Test ACL - Special (chars) #1');
// Should accept the input
const value = await nameInput.inputValue();
expect(value).toContain('Special');
await getCancelButton(page).click();
});
});
test('should validate CIDR format', async ({ page }) => {
await test.step('Test CIDR validation', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
await page.locator('#name').fill(`CIDR Validation Test ${generateUniqueId()}`);
// Wait for IP rules section to be visible
await page.waitForSelector('text=IP Addresses / CIDR Ranges', { timeout: 5000 });
// Try adding invalid CIDR
const ipInput = page.locator('input[placeholder="192.168.1.0/24 or 10.0.0.1"]');
await ipInput.waitFor({ state: 'visible', timeout: 5000 });
await ipInput.fill('999.999.999.999/24');
const addButton = page.locator('button').filter({ has: page.locator('svg.lucide-plus') }).first();
await addButton.click();
// Should show error or not add the invalid IP
await waitForDebounce(page);
await getCancelButton(page).click();
});
});
});
test.describe('CGNAT Warning', () => {
test('should show CGNAT warning when ACLs exist', async ({ page }) => {
await test.step('Check for CGNAT warning', async () => {
const table = page.getByRole('table');
const hasTable = await table.isVisible().catch(() => false);
if (hasTable) {
const rows = await page.locator('tbody tr').count();
if (rows > 0) {
// Warning should be visible
const cgnatWarning = page.getByText(/cgnat|carrier.*grade/i);
const hasWarning = await cgnatWarning.isVisible().catch(() => false);
expect(hasWarning || true).toBeTruthy();
}
}
});
});
test('should be dismissible', async ({ page }) => {
await test.step('Dismiss CGNAT warning', async () => {
const cgnatWarning = page.getByRole('alert').filter({ hasText: /cgnat|locked.*out/i });
if (await cgnatWarning.isVisible().catch(() => false)) {
const dismissButton = cgnatWarning.getByRole('button');
if (await dismissButton.isVisible().catch(() => false)) {
await dismissButton.click();
await expect(cgnatWarning).not.toBeVisible({ timeout: 3000 });
}
}
});
});
});
test.describe('Best Practices Link', () => {
test('should show Best Practices button', async ({ page }) => {
await test.step('Verify Best Practices link', async () => {
const bestPracticesLink = page.getByRole('button', { name: /best.*practices/i });
await expect(bestPracticesLink).toBeVisible();
});
});
test('should have external link to documentation', async ({ page }) => {
await test.step('Verify link opens external documentation', async () => {
const bestPracticesLink = page.getByRole('button', { name: /best.*practices/i });
if (await bestPracticesLink.isVisible().catch(() => false)) {
// Verify it has external link icon
const hasExternalIcon = await bestPracticesLink.locator('svg.lucide-external-link').count() > 0;
expect(hasExternalIcon || true).toBeTruthy();
}
});
});
});
test.describe('Form Accessibility', () => {
test('should have accessible form labels', async ({ page }) => {
await test.step('Open form and verify labels', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
// Check that inputs have associated labels
const nameLabel = page.locator('label[for="name"]');
await expect(nameLabel).toBeVisible();
await getCancelButton(page).click();
});
});
test('should be keyboard navigable', async ({ page }) => {
await test.step('Navigate form with keyboard', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
// Tab through form fields
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Some element should be focused
const focusedElement = page.locator(':focus');
const hasFocus = await focusedElement.isVisible().catch(() => false);
expect(hasFocus || true).toBeTruthy();
// Escape or Cancel to close
await getCancelButton(page).click();
});
});
});
test.describe('Local Network Only Mode', () => {
test('should toggle local network only (RFC1918)', async ({ page }) => {
await test.step('Open form and toggle RFC1918 mode', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
const localNetworkSwitch = page.getByLabel(/local.*network.*only/i);
if (await localNetworkSwitch.isVisible().catch(() => false)) {
const wasChecked = await localNetworkSwitch.isChecked();
await clickSwitch(localNetworkSwitch);
const isNowChecked = await localNetworkSwitch.isChecked();
expect(isNowChecked).toBe(!wasChecked);
}
await getCancelButton(page).click();
});
});
test('should hide IP rules when local network only is enabled', async ({ page }) => {
await test.step('Verify IP input hidden in RFC1918 mode', async () => {
await getCreateButton(page).click();
await waitForModal(page, /create|access.*list/i);
const localNetworkSwitch = page.getByLabel(/local.*network.*only/i);
if (await localNetworkSwitch.isVisible().catch(() => false)) {
// Enable local network only
if (!await localNetworkSwitch.isChecked()) {
await clickSwitch(localNetworkSwitch);
}
// IP input should be hidden
const ipInput = page.locator('input[placeholder*="192.168"], input[placeholder*="CIDR"]').first();
const isHidden = !(await ipInput.isVisible().catch(() => false));
expect(isHidden || true).toBeTruthy();
}
await getCancelButton(page).click();
});
});
});
});