chore: clean .gitignore cache
This commit is contained in:
@@ -1,367 +0,0 @@
|
||||
/**
|
||||
* Audit Logs E2E Tests
|
||||
*
|
||||
* Tests the audit logs functionality:
|
||||
* - Page loading and data display
|
||||
* - Log filtering and search
|
||||
* - Export functionality (CSV)
|
||||
* - Pagination
|
||||
* - Log details view
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('Audit Logs', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/audit-logs');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display audit logs page', async ({ page }) => {
|
||||
// Look for audit logs heading or any audit-related content
|
||||
const heading = page.getByRole('heading', { name: /audit|log/i });
|
||||
const headingVisible = await heading.isVisible().catch(() => false);
|
||||
|
||||
if (headingVisible) {
|
||||
await expect(heading).toBeVisible();
|
||||
} else {
|
||||
// Try finding audit logs content by text
|
||||
const content = page.getByText(/audit|security.*log|activity.*log/i).first();
|
||||
const contentVisible = await content.isVisible().catch(() => false);
|
||||
|
||||
if (contentVisible) {
|
||||
await expect(content).toBeVisible();
|
||||
} else {
|
||||
// Page should at least be loaded
|
||||
await expect(page).toHaveURL(/audit-logs/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should display log data table', async ({ page }) => {
|
||||
// Wait for the app-level loading to complete (showing "Loading application...")
|
||||
try {
|
||||
await page.waitForSelector('[role="status"]', { state: 'hidden', timeout: 5000 });
|
||||
} catch {
|
||||
// Loading might already be done
|
||||
}
|
||||
|
||||
// Wait for content to appear
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Try to find table first
|
||||
const table = page.getByRole('table');
|
||||
const tableVisible = await table.isVisible().catch(() => false);
|
||||
|
||||
if (tableVisible) {
|
||||
await expect(table).toBeVisible();
|
||||
} else {
|
||||
// Might use a different table/grid component, check for common patterns
|
||||
const dataGrid = page.locator('table, [role="grid"], [data-testid*="table"], [data-testid*="grid"], [class*="log"]').first();
|
||||
const dataGridVisible = await dataGrid.isVisible().catch(() => false);
|
||||
|
||||
if (dataGridVisible) {
|
||||
await expect(dataGrid).toBeVisible();
|
||||
} else {
|
||||
// Check for empty state, loading, or heading (page might still be loading)
|
||||
const emptyOrLoading = page.getByText(/no.*logs|no.*data|loading|empty|audit/i).first();
|
||||
const emptyVisible = await emptyOrLoading.isVisible().catch(() => false);
|
||||
|
||||
// Check URL at minimum - page should be correct URL
|
||||
const currentUrl = page.url();
|
||||
const isCorrectPage = currentUrl.includes('audit-logs');
|
||||
|
||||
// Either data display, empty state, or correct page should be present
|
||||
expect(dataGridVisible || emptyVisible || isCorrectPage).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Log Table Structure', () => {
|
||||
test('should display timestamp column', async ({ page }) => {
|
||||
const timestampHeader = page.locator('th, [role="columnheader"]').filter({
|
||||
hasText: /timestamp|date|time|when/i
|
||||
}).first();
|
||||
|
||||
const headerVisible = await timestampHeader.isVisible().catch(() => false);
|
||||
|
||||
if (headerVisible) {
|
||||
await expect(timestampHeader).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display action/event column', async ({ page }) => {
|
||||
const actionHeader = page.locator('th, [role="columnheader"]').filter({
|
||||
hasText: /action|event|type|activity/i
|
||||
}).first();
|
||||
|
||||
const headerVisible = await actionHeader.isVisible().catch(() => false);
|
||||
|
||||
if (headerVisible) {
|
||||
await expect(actionHeader).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display user column', async ({ page }) => {
|
||||
const userHeader = page.locator('th, [role="columnheader"]').filter({
|
||||
hasText: /user|actor|who/i
|
||||
}).first();
|
||||
|
||||
const headerVisible = await userHeader.isVisible().catch(() => false);
|
||||
|
||||
if (headerVisible) {
|
||||
await expect(userHeader).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display log entries', async ({ page }) => {
|
||||
// Wait for logs to load
|
||||
await page.waitForResponse(resp =>
|
||||
resp.url().includes('/audit') || resp.url().includes('/logs'),
|
||||
{ timeout: 10000 }
|
||||
).catch(() => {
|
||||
// API might not be called if no logs
|
||||
});
|
||||
|
||||
const rows = page.locator('tr, [class*="row"]');
|
||||
const count = await rows.count();
|
||||
|
||||
// At least header row should exist
|
||||
expect(count >= 0).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Filtering', () => {
|
||||
test('should have search input', async ({ page }) => {
|
||||
const searchInput = page.getByPlaceholder(/search|filter/i);
|
||||
const searchVisible = await searchInput.isVisible().catch(() => false);
|
||||
|
||||
if (searchVisible) {
|
||||
await expect(searchInput).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter by action type', async ({ page }) => {
|
||||
const typeFilter = page.locator('select, [role="listbox"]').filter({
|
||||
hasText: /type|action|all|login|create|update|delete/i
|
||||
}).first();
|
||||
|
||||
const filterVisible = await typeFilter.isVisible().catch(() => false);
|
||||
|
||||
if (filterVisible) {
|
||||
await expect(typeFilter).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter by date range', async ({ page }) => {
|
||||
const dateFilter = page.locator('input[type="date"], [class*="datepicker"]').first();
|
||||
const dateVisible = await dateFilter.isVisible().catch(() => false);
|
||||
|
||||
if (dateVisible) {
|
||||
await expect(dateFilter).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter by user', async ({ page }) => {
|
||||
const userFilter = page.locator('select, [role="listbox"]').filter({
|
||||
hasText: /user|actor|all.*user/i
|
||||
}).first();
|
||||
|
||||
const userVisible = await userFilter.isVisible().catch(() => false);
|
||||
expect(userVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should perform search when input changes', async ({ page }) => {
|
||||
const searchInput = page.getByPlaceholder(/search|filter/i);
|
||||
const searchVisible = await searchInput.isVisible().catch(() => false);
|
||||
|
||||
if (searchVisible) {
|
||||
await test.step('Enter search term', async () => {
|
||||
await searchInput.fill('login');
|
||||
await page.waitForTimeout(500); // Debounce
|
||||
});
|
||||
|
||||
await test.step('Clear search', async () => {
|
||||
await searchInput.clear();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Export Functionality', () => {
|
||||
test('should have export button', async ({ page }) => {
|
||||
const exportButton = page.getByRole('button', { name: /export|download|csv/i });
|
||||
const exportVisible = await exportButton.isVisible().catch(() => false);
|
||||
|
||||
if (exportVisible) {
|
||||
await expect(exportButton).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should export logs to CSV', async ({ page }) => {
|
||||
const exportButton = page.getByRole('button', { name: /export|download|csv/i });
|
||||
const exportVisible = await exportButton.isVisible().catch(() => false);
|
||||
|
||||
if (exportVisible) {
|
||||
// Set up download handler
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 5000 }).catch(() => null);
|
||||
|
||||
await exportButton.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
if (download) {
|
||||
// Verify download started
|
||||
expect(download.suggestedFilename()).toMatch(/\.csv$/i);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pagination', () => {
|
||||
test('should have pagination controls', async ({ page }) => {
|
||||
const pagination = page.locator('[class*="pagination"], nav').filter({
|
||||
has: page.locator('button, a')
|
||||
}).first();
|
||||
|
||||
const paginationVisible = await pagination.isVisible().catch(() => false);
|
||||
expect(paginationVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display current page info', async ({ page }) => {
|
||||
const pageInfo = page.getByText(/page|of|showing|entries/i).first();
|
||||
const pageInfoVisible = await pageInfo.isVisible().catch(() => false);
|
||||
|
||||
expect(pageInfoVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should navigate between pages', async ({ page }) => {
|
||||
const nextButton = page.getByRole('button', { name: /next|›|»/i });
|
||||
const nextVisible = await nextButton.isVisible().catch(() => false);
|
||||
|
||||
if (nextVisible) {
|
||||
const isEnabled = await nextButton.isEnabled();
|
||||
|
||||
if (isEnabled) {
|
||||
await test.step('Go to next page', async () => {
|
||||
await nextButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
await test.step('Go back to previous page', async () => {
|
||||
const prevButton = page.getByRole('button', { name: /previous|‹|«/i });
|
||||
await prevButton.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Log Details', () => {
|
||||
test('should show log details on row click', async ({ page }) => {
|
||||
const firstRow = page.locator('tr, [class*="row"]').filter({
|
||||
hasText: /\d{1,2}:\d{2}|login|create|update|delete/i
|
||||
}).first();
|
||||
|
||||
const rowVisible = await firstRow.isVisible().catch(() => false);
|
||||
|
||||
if (rowVisible) {
|
||||
await firstRow.click();
|
||||
|
||||
// Should show details modal or expand row
|
||||
const detailsModal = page.getByRole('dialog');
|
||||
const modalVisible = await detailsModal.isVisible().catch(() => false);
|
||||
|
||||
if (modalVisible) {
|
||||
const closeButton = page.getByRole('button', { name: /close|cancel/i });
|
||||
await closeButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Refresh', () => {
|
||||
test('should have refresh button', async ({ page }) => {
|
||||
const refreshButton = page.getByRole('button', { name: /refresh|reload|sync/i });
|
||||
const refreshVisible = await refreshButton.isVisible().catch(() => false);
|
||||
|
||||
if (refreshVisible) {
|
||||
await test.step('Click refresh', async () => {
|
||||
await refreshButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate back to security dashboard', async ({ page }) => {
|
||||
const backLink = page.getByRole('link', { name: /security|back/i });
|
||||
const backVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backVisible) {
|
||||
await backLink.click();
|
||||
await waitForLoadingComplete(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have accessible table structure', async ({ page }) => {
|
||||
const table = page.getByRole('table');
|
||||
const tableVisible = await table.isVisible().catch(() => false);
|
||||
|
||||
if (tableVisible) {
|
||||
// Table should have headers
|
||||
const headers = page.locator('th, [role="columnheader"]');
|
||||
const headerCount = await headers.count();
|
||||
expect(headerCount).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be keyboard navigable', async ({ page }) => {
|
||||
// Wait for the app-level loading to complete
|
||||
try {
|
||||
await page.waitForSelector('[role="status"]', { state: 'hidden', timeout: 5000 });
|
||||
} catch {
|
||||
// Loading might already be done
|
||||
}
|
||||
|
||||
// Wait a moment for focus management
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Tab to the first focusable element
|
||||
await page.keyboard.press('Tab');
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Check if any element received focus
|
||||
const focusedElement = page.locator(':focus');
|
||||
const hasFocus = await focusedElement.count() > 0;
|
||||
|
||||
// Also check if body has focus (acceptable default)
|
||||
const bodyFocused = await page.evaluate(() => document.activeElement?.tagName === 'BODY');
|
||||
|
||||
// Page must have some kind of focus state (either element or body)
|
||||
// If still loading, the URL being correct is acceptable
|
||||
const isCorrectPage = page.url().includes('audit-logs');
|
||||
expect(hasFocus || bodyFocused || isCorrectPage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Empty State', () => {
|
||||
test('should show empty state message when no logs', async ({ page }) => {
|
||||
// This is a soft check - logs may or may not exist
|
||||
const emptyState = page.getByText(/no.*logs|no.*entries|empty|no.*data/i);
|
||||
const emptyVisible = await emptyState.isVisible().catch(() => false);
|
||||
|
||||
// Either empty state or data should be shown
|
||||
expect(emptyVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,301 +0,0 @@
|
||||
/**
|
||||
* CrowdSec Configuration E2E Tests
|
||||
*
|
||||
* Tests the CrowdSec configuration page functionality including:
|
||||
* - Page loading and status display
|
||||
* - Preset management (view, apply, preview)
|
||||
* - Configuration file management
|
||||
* - Import/Export functionality
|
||||
* - Console enrollment (if enabled)
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('CrowdSec Configuration', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/crowdsec');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display CrowdSec configuration page', async ({ page }) => {
|
||||
// The page should load without errors
|
||||
await expect(page.getByRole('heading', { name: /crowdsec/i }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show navigation back to security dashboard', async ({ page }) => {
|
||||
// Should have breadcrumb, back link, or navigation element
|
||||
const backLink = page.getByRole('link', { name: /security|back/i });
|
||||
const backLinkVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backLinkVisible) {
|
||||
await expect(backLink).toBeVisible();
|
||||
} else {
|
||||
// Check for navigation button instead
|
||||
const backButton = page.getByRole('button', { name: /back/i });
|
||||
const buttonVisible = await backButton.isVisible().catch(() => false);
|
||||
|
||||
if (!buttonVisible) {
|
||||
// Check for breadcrumbs or navigation in header
|
||||
const breadcrumb = page.locator('nav, [class*="breadcrumb"], [aria-label*="breadcrumb"]');
|
||||
const breadcrumbVisible = await breadcrumb.isVisible().catch(() => false);
|
||||
|
||||
// At minimum, the page should be loaded
|
||||
if (!breadcrumbVisible) {
|
||||
await expect(page).toHaveURL(/crowdsec/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should display presets section', async ({ page }) => {
|
||||
// Look for presets, packages, scenarios, or collections section
|
||||
// This feature may not be fully implemented
|
||||
const presetsSection = page.getByText(/packages|presets|scenarios|collections|bouncers/i).first();
|
||||
const presetsVisible = await presetsSection.isVisible().catch(() => false);
|
||||
|
||||
if (presetsVisible) {
|
||||
await expect(presetsSection).toBeVisible();
|
||||
} else {
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Presets section not visible - feature may not be implemented'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Preset Management', () => {
|
||||
// Preset management may not be fully implemented
|
||||
test('should display list of available presets', async ({ page }) => {
|
||||
await test.step('Verify presets are listed', async () => {
|
||||
// Wait for presets to load
|
||||
await page.waitForResponse(resp =>
|
||||
resp.url().includes('/presets') || resp.url().includes('/crowdsec') || resp.url().includes('/hub'),
|
||||
{ timeout: 10000 }
|
||||
).catch(() => {
|
||||
// If no API call, presets might be loaded statically or not implemented
|
||||
});
|
||||
|
||||
// Should show preset cards or list items
|
||||
const presetElements = page.locator('[class*="card"], [class*="preset"], button').filter({
|
||||
hasText: /apply|install|owasp|basic|advanced|paranoid/i
|
||||
});
|
||||
|
||||
const count = await presetElements.count();
|
||||
|
||||
if (count === 0) {
|
||||
// Presets might not be implemented - check for config file management instead
|
||||
const configSection = page.getByText(/configuration|file|config/i).first();
|
||||
const configVisible = await configSection.isVisible().catch(() => false);
|
||||
|
||||
if (!configVisible) {
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'No presets displayed - feature may not be implemented'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow searching presets', async ({ page }) => {
|
||||
const searchInput = page.getByPlaceholder(/search/i);
|
||||
const searchVisible = await searchInput.isVisible().catch(() => false);
|
||||
|
||||
if (searchVisible) {
|
||||
await test.step('Search for a preset', async () => {
|
||||
await searchInput.fill('basic');
|
||||
// Results should be filtered
|
||||
await page.waitForTimeout(500); // Debounce
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should show preset preview when selected', async ({ page }) => {
|
||||
// Find and click on a preset to preview
|
||||
const presetButton = page.locator('button').filter({ hasText: /preview|view|select/i }).first();
|
||||
const buttonVisible = await presetButton.isVisible().catch(() => false);
|
||||
|
||||
if (buttonVisible) {
|
||||
await presetButton.click();
|
||||
// Should show preview content
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
test('should apply preset with confirmation', async ({ page }) => {
|
||||
// Find apply button
|
||||
const applyButton = page.locator('button').filter({ hasText: /apply/i }).first();
|
||||
const buttonVisible = await applyButton.isVisible().catch(() => false);
|
||||
|
||||
if (buttonVisible) {
|
||||
await test.step('Click apply button', async () => {
|
||||
await applyButton.click();
|
||||
});
|
||||
|
||||
await test.step('Handle confirmation or result', async () => {
|
||||
// Either a confirmation dialog or success toast should appear
|
||||
const confirmDialog = page.getByRole('dialog');
|
||||
const dialogVisible = await confirmDialog.isVisible().catch(() => false);
|
||||
|
||||
if (dialogVisible) {
|
||||
// Cancel to not make permanent changes
|
||||
const cancelButton = page.getByRole('button', { name: /cancel/i });
|
||||
await cancelButton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Configuration Files', () => {
|
||||
test('should display configuration file list', async ({ page }) => {
|
||||
// Look for file selector or list
|
||||
const fileSelector = page.locator('select, [role="listbox"]').filter({
|
||||
hasNotText: /sort|filter/i
|
||||
}).first();
|
||||
|
||||
const filesVisible = await fileSelector.isVisible().catch(() => false);
|
||||
|
||||
if (filesVisible) {
|
||||
await expect(fileSelector).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show file content when selected', async ({ page }) => {
|
||||
// Find file selector
|
||||
const fileSelector = page.locator('select').first();
|
||||
const selectorVisible = await fileSelector.isVisible().catch(() => false);
|
||||
|
||||
if (selectorVisible) {
|
||||
// Select a file - content area should appear
|
||||
const contentArea = page.locator('textarea, pre, [class*="editor"]');
|
||||
const contentVisible = await contentArea.isVisible().catch(() => false);
|
||||
|
||||
// Content area may or may not be visible depending on selection
|
||||
expect(contentVisible !== undefined).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Import/Export', () => {
|
||||
test('should have export functionality', async ({ page }) => {
|
||||
const exportButton = page.getByRole('button', { name: /export/i });
|
||||
const exportVisible = await exportButton.isVisible().catch(() => false);
|
||||
|
||||
if (exportVisible) {
|
||||
await expect(exportButton).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should have import functionality', async ({ page }) => {
|
||||
// Look for import button or file input
|
||||
const importButton = page.getByRole('button', { name: /import/i });
|
||||
const importInput = page.locator('input[type="file"]');
|
||||
|
||||
const importVisible = await importButton.isVisible().catch(() => false);
|
||||
const inputVisible = await importInput.isVisible().catch(() => false);
|
||||
|
||||
// Import functionality may not be implemented
|
||||
if (importVisible || inputVisible) {
|
||||
expect(importVisible || inputVisible).toBeTruthy();
|
||||
} else {
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Import functionality not visible - feature may not be implemented'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Console Enrollment', () => {
|
||||
test('should display console enrollment section if feature enabled', async ({ page }) => {
|
||||
// Console enrollment is a feature-flagged component
|
||||
const enrollmentSection = page.getByTestId('console-section').or(
|
||||
page.locator('[class*="card"]').filter({ hasText: /console.*enrollment|enroll/i })
|
||||
);
|
||||
|
||||
const enrollmentVisible = await enrollmentSection.isVisible().catch(() => false);
|
||||
|
||||
if (enrollmentVisible) {
|
||||
await test.step('Verify enrollment form elements', async () => {
|
||||
// Should have token input
|
||||
const tokenInput = page.getByTestId('crowdsec-token-input').or(
|
||||
page.getByPlaceholder(/token|key/i)
|
||||
);
|
||||
await expect(tokenInput).toBeVisible();
|
||||
});
|
||||
} else {
|
||||
// Feature might be disabled - that's OK
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Console enrollment feature not enabled'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should show enrollment status when enrolled', async ({ page }) => {
|
||||
const statusText = page.getByTestId('console-token-state').or(
|
||||
page.locator('text=/enrolled|pending|not enrolled/i')
|
||||
);
|
||||
|
||||
const statusVisible = await statusText.isVisible().catch(() => false);
|
||||
|
||||
if (statusVisible) {
|
||||
// Verify status is displayed
|
||||
await expect(statusText).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Status Indicators', () => {
|
||||
test('should display CrowdSec running status', async ({ page }) => {
|
||||
// Look for status indicators
|
||||
const statusBadge = page.locator('[class*="badge"]').filter({
|
||||
hasText: /running|stopped|enabled|disabled/i
|
||||
});
|
||||
|
||||
const statusVisible = await statusBadge.first().isVisible().catch(() => false);
|
||||
|
||||
if (statusVisible) {
|
||||
await expect(statusBadge.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display LAPI status', async ({ page }) => {
|
||||
// LAPI ready status might be displayed
|
||||
const lapiStatus = page.getByText(/lapi.*ready|lapi.*status/i);
|
||||
const lapiVisible = await lapiStatus.isVisible().catch(() => false);
|
||||
|
||||
// LAPI status might not always be visible
|
||||
expect(lapiVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have accessible form controls', async ({ page }) => {
|
||||
// Check that inputs have associated labels
|
||||
const inputs = page.locator('input:not([type="hidden"])');
|
||||
const count = await inputs.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
||||
const input = inputs.nth(i);
|
||||
const visible = await input.isVisible();
|
||||
|
||||
if (visible) {
|
||||
// Input should have some form of label (explicit, aria-label, or placeholder)
|
||||
const hasLabel = await input.getAttribute('aria-label') ||
|
||||
await input.getAttribute('placeholder') ||
|
||||
await input.getAttribute('id');
|
||||
expect(hasLabel).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* CrowdSec Decisions (Bans) E2E Tests
|
||||
*
|
||||
* Tests the CrowdSec decisions/bans management functionality:
|
||||
* - Viewing active decisions/bans
|
||||
* - Adding manual IP bans
|
||||
* - Removing bans (unban)
|
||||
* - Decision details and filtering
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
// NOTE: The /security/crowdsec/decisions route doesn't exist as a separate page.
|
||||
// Decisions are displayed within the main CrowdSec config page at /security/crowdsec.
|
||||
// This test suite is skipped until the dedicated decisions route is implemented.
|
||||
test.describe.skip('CrowdSec Decisions Management', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/crowdsec');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Decisions List', () => {
|
||||
test('should display decisions page', async ({ page }) => {
|
||||
// The page should load - look for heading or table
|
||||
const heading = page.getByRole('heading', { name: /decisions|bans/i });
|
||||
const table = page.getByRole('table');
|
||||
const grid = page.locator('[class*="grid"], [class*="table"], [class*="list"]');
|
||||
|
||||
const headingVisible = await heading.isVisible().catch(() => false);
|
||||
const tableVisible = await table.isVisible().catch(() => false);
|
||||
const gridVisible = await grid.first().isVisible().catch(() => false);
|
||||
|
||||
// At least one should be visible
|
||||
expect(headingVisible || tableVisible || gridVisible).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show active decisions if any exist', async ({ page }) => {
|
||||
// Wait for decisions to load
|
||||
await page.waitForResponse(resp =>
|
||||
resp.url().includes('/decisions') || resp.url().includes('/crowdsec'),
|
||||
{ timeout: 10000 }
|
||||
).catch(() => {
|
||||
// API might not be called if no decisions
|
||||
});
|
||||
|
||||
// Could be empty state or list of decisions
|
||||
const emptyState = page.getByText(/no.*decisions|no.*bans|empty/i);
|
||||
const decisionRows = page.locator('tr, [class*="row"]').filter({
|
||||
hasText: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|ban|captcha/i
|
||||
});
|
||||
|
||||
const emptyVisible = await emptyState.isVisible().catch(() => false);
|
||||
const rowCount = await decisionRows.count();
|
||||
|
||||
// Either empty state or some decisions
|
||||
expect(emptyVisible || rowCount >= 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display decision columns (IP, type, duration, reason)', async ({ page }) => {
|
||||
const table = page.getByRole('table');
|
||||
const tableVisible = await table.isVisible().catch(() => false);
|
||||
|
||||
if (tableVisible) {
|
||||
await test.step('Verify table headers', async () => {
|
||||
const headers = page.locator('th, [role="columnheader"]');
|
||||
const headerTexts = await headers.allTextContents();
|
||||
|
||||
// Headers might include IP, type, duration, scope, reason, origin
|
||||
const headerString = headerTexts.join(' ').toLowerCase();
|
||||
const hasRelevantHeaders = headerString.includes('ip') ||
|
||||
headerString.includes('type') ||
|
||||
headerString.includes('scope') ||
|
||||
headerString.includes('origin');
|
||||
|
||||
expect(hasRelevantHeaders).toBeTruthy();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Add Decision (Ban IP)', () => {
|
||||
test('should have add ban button', async ({ page }) => {
|
||||
const addButton = page.getByRole('button', { name: /add|ban|new/i });
|
||||
const addButtonVisible = await addButton.isVisible().catch(() => false);
|
||||
|
||||
if (addButtonVisible) {
|
||||
await expect(addButton).toBeEnabled();
|
||||
} else {
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Add ban functionality might not be exposed in UI'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should open ban modal on add button click', async ({ page }) => {
|
||||
const addButton = page.getByRole('button', { name: /add.*ban|ban.*ip/i });
|
||||
const addButtonVisible = await addButton.isVisible().catch(() => false);
|
||||
|
||||
if (addButtonVisible) {
|
||||
await addButton.click();
|
||||
|
||||
await test.step('Verify ban modal opens', async () => {
|
||||
const modal = page.getByRole('dialog');
|
||||
const modalVisible = await modal.isVisible().catch(() => false);
|
||||
|
||||
if (modalVisible) {
|
||||
// Modal should have IP input field
|
||||
const ipInput = modal.getByPlaceholder(/ip|address/i).or(
|
||||
modal.locator('input').first()
|
||||
);
|
||||
await expect(ipInput).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
const closeButton = modal.getByRole('button', { name: /cancel|close/i });
|
||||
await closeButton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate IP address format', async ({ page }) => {
|
||||
const addButton = page.getByRole('button', { name: /add.*ban|ban.*ip/i });
|
||||
const addButtonVisible = await addButton.isVisible().catch(() => false);
|
||||
|
||||
if (addButtonVisible) {
|
||||
await addButton.click();
|
||||
|
||||
const modal = page.getByRole('dialog');
|
||||
const modalVisible = await modal.isVisible().catch(() => false);
|
||||
|
||||
if (modalVisible) {
|
||||
const ipInput = modal.locator('input').first();
|
||||
|
||||
await test.step('Enter invalid IP', async () => {
|
||||
await ipInput.fill('invalid-ip');
|
||||
|
||||
// Look for submit button
|
||||
const submitButton = modal.getByRole('button', { name: /ban|submit|add/i });
|
||||
await submitButton.click();
|
||||
|
||||
// Should show validation error
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
// Close modal
|
||||
const closeButton = modal.getByRole('button', { name: /cancel|close/i });
|
||||
const closeVisible = await closeButton.isVisible().catch(() => false);
|
||||
if (closeVisible) {
|
||||
await closeButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Remove Decision (Unban)', () => {
|
||||
test('should show unban action for each decision', async ({ page }) => {
|
||||
// If there are decisions, each should have an unban action
|
||||
const unbanButtons = page.getByRole('button', { name: /unban|remove|delete/i });
|
||||
const count = await unbanButtons.count();
|
||||
|
||||
// Just verify the selector works - actual decisions may or may not exist
|
||||
expect(count >= 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should confirm before unbanning', async ({ page }) => {
|
||||
const unbanButton = page.getByRole('button', { name: /unban|remove/i }).first();
|
||||
const unbanVisible = await unbanButton.isVisible().catch(() => false);
|
||||
|
||||
if (unbanVisible) {
|
||||
await unbanButton.click();
|
||||
|
||||
// Should show confirmation dialog
|
||||
const confirmDialog = page.getByRole('dialog');
|
||||
const dialogVisible = await confirmDialog.isVisible().catch(() => false);
|
||||
|
||||
if (dialogVisible) {
|
||||
// Cancel the action
|
||||
const cancelButton = page.getByRole('button', { name: /cancel|no/i });
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Filtering and Search', () => {
|
||||
test('should have search/filter input', async ({ page }) => {
|
||||
const searchInput = page.getByPlaceholder(/search|filter/i);
|
||||
const searchVisible = await searchInput.isVisible().catch(() => false);
|
||||
|
||||
if (searchVisible) {
|
||||
await expect(searchInput).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter decisions by type', async ({ page }) => {
|
||||
const typeFilter = page.locator('select, [role="listbox"]').filter({
|
||||
hasText: /type|all|ban|captcha/i
|
||||
}).first();
|
||||
|
||||
const filterVisible = await typeFilter.isVisible().catch(() => false);
|
||||
|
||||
if (filterVisible) {
|
||||
await expect(typeFilter).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Refresh and Sync', () => {
|
||||
test('should have refresh button', async ({ page }) => {
|
||||
const refreshButton = page.getByRole('button', { name: /refresh|sync|reload/i });
|
||||
const refreshVisible = await refreshButton.isVisible().catch(() => false);
|
||||
|
||||
if (refreshVisible) {
|
||||
await test.step('Click refresh button', async () => {
|
||||
await refreshButton.click();
|
||||
// Should trigger API call
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate back to CrowdSec config', async ({ page }) => {
|
||||
const backLink = page.getByRole('link', { name: /crowdsec|back|config/i });
|
||||
const backVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backVisible) {
|
||||
await backLink.click();
|
||||
await waitForLoadingComplete(page);
|
||||
await expect(page).toHaveURL(/\/security\/crowdsec(?!\/decisions)/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should be keyboard navigable', async ({ page }) => {
|
||||
await page.keyboard.press('Tab');
|
||||
// Some element should receive focus
|
||||
const focusedElement = page.locator(':focus');
|
||||
await expect(focusedElement).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* Rate Limiting E2E Tests
|
||||
*
|
||||
* Tests the rate limiting configuration:
|
||||
* - Page loading and status
|
||||
* - RPS/Burst settings
|
||||
* - Time window configuration
|
||||
* - Per-route settings
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('Rate Limiting Configuration', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/rate-limiting');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display rate limiting configuration page', async ({ page }) => {
|
||||
const heading = page.getByRole('heading', { name: /rate.*limit/i });
|
||||
const headingVisible = await heading.isVisible().catch(() => false);
|
||||
|
||||
if (!headingVisible) {
|
||||
const content = page.getByText(/rate.*limit|rps|requests per second/i).first();
|
||||
await expect(content).toBeVisible();
|
||||
} else {
|
||||
await expect(heading).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display rate limiting status', async ({ page }) => {
|
||||
const statusBadge = page.locator('[class*="badge"]').filter({
|
||||
hasText: /enabled|disabled|active|inactive/i
|
||||
});
|
||||
|
||||
const statusVisible = await statusBadge.first().isVisible().catch(() => false);
|
||||
expect(statusVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Rate Limiting Toggle', () => {
|
||||
test('should have enable/disable toggle', async ({ page }) => {
|
||||
// The toggle may be on this page or only on the main security dashboard
|
||||
const toggle = page.getByTestId('toggle-rate-limit').or(
|
||||
page.locator('input[type="checkbox"]').first()
|
||||
);
|
||||
|
||||
const toggleVisible = await toggle.isVisible().catch(() => false);
|
||||
|
||||
if (toggleVisible) {
|
||||
// Toggle may be disabled if Cerberus is not enabled
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (!isDisabled) {
|
||||
await expect(toggle).toBeEnabled();
|
||||
}
|
||||
} else {
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Toggle not present on rate limiting config page - located on main security dashboard'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle rate limiting on/off', async ({ page }) => {
|
||||
// The toggle uses checkbox type, not switch role
|
||||
const toggle = page.locator('input[type="checkbox"]').first();
|
||||
const toggleVisible = await toggle.isVisible().catch(() => false);
|
||||
|
||||
if (toggleVisible) {
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Toggle is disabled - Cerberus may not be enabled'
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await test.step('Toggle rate limiting', async () => {
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
await test.step('Revert toggle', async () => {
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('RPS Settings', () => {
|
||||
test('should display RPS input field', async ({ page }) => {
|
||||
const rpsInput = page.getByLabel(/rps|requests per second/i).or(
|
||||
page.locator('input[type="number"]').first()
|
||||
);
|
||||
|
||||
const inputVisible = await rpsInput.isVisible().catch(() => false);
|
||||
|
||||
if (inputVisible) {
|
||||
await expect(rpsInput).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate RPS input (minimum value)', async ({ page }) => {
|
||||
const rpsInput = page.getByLabel(/rps|requests per second/i).or(
|
||||
page.locator('input[type="number"]').first()
|
||||
);
|
||||
|
||||
const inputVisible = await rpsInput.isVisible().catch(() => false);
|
||||
|
||||
if (inputVisible) {
|
||||
const originalValue = await rpsInput.inputValue();
|
||||
|
||||
await test.step('Enter invalid RPS value', async () => {
|
||||
await rpsInput.fill('-1');
|
||||
await rpsInput.blur();
|
||||
});
|
||||
|
||||
await test.step('Restore original value', async () => {
|
||||
await rpsInput.fill(originalValue || '100');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should accept valid RPS value', async ({ page }) => {
|
||||
const rpsInput = page.getByLabel(/rps|requests per second/i).or(
|
||||
page.locator('input[type="number"]').first()
|
||||
);
|
||||
|
||||
const inputVisible = await rpsInput.isVisible().catch(() => false);
|
||||
|
||||
if (inputVisible) {
|
||||
const originalValue = await rpsInput.inputValue();
|
||||
|
||||
await test.step('Enter valid RPS value', async () => {
|
||||
await rpsInput.fill('100');
|
||||
// Should not show error
|
||||
});
|
||||
|
||||
await test.step('Restore original value', async () => {
|
||||
await rpsInput.fill(originalValue || '100');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Burst Settings', () => {
|
||||
test('should display burst limit input', async ({ page }) => {
|
||||
const burstInput = page.getByLabel(/burst/i).or(
|
||||
page.locator('input[type="number"]').nth(1)
|
||||
);
|
||||
|
||||
const inputVisible = await burstInput.isVisible().catch(() => false);
|
||||
|
||||
if (inputVisible) {
|
||||
await expect(burstInput).toBeEnabled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Time Window Settings', () => {
|
||||
test('should display time window setting', async ({ page }) => {
|
||||
const windowInput = page.getByLabel(/window|duration|period/i).or(
|
||||
page.locator('select, input[type="number"]').filter({
|
||||
hasText: /second|minute|hour/i
|
||||
}).first()
|
||||
);
|
||||
|
||||
const inputVisible = await windowInput.isVisible().catch(() => false);
|
||||
expect(inputVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Save Settings', () => {
|
||||
test('should have save button', async ({ page }) => {
|
||||
const saveButton = page.getByRole('button', { name: /save|apply|update/i });
|
||||
const saveVisible = await saveButton.isVisible().catch(() => false);
|
||||
|
||||
if (saveVisible) {
|
||||
await expect(saveButton).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate back to security dashboard', async ({ page }) => {
|
||||
const backLink = page.getByRole('link', { name: /security|back/i });
|
||||
const backVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backVisible) {
|
||||
await backLink.click();
|
||||
await waitForLoadingComplete(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have labeled input fields', async ({ page }) => {
|
||||
const inputs = page.locator('input[type="number"]');
|
||||
const count = await inputs.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 3); i++) {
|
||||
const input = inputs.nth(i);
|
||||
const visible = await input.isVisible();
|
||||
|
||||
if (visible) {
|
||||
const id = await input.getAttribute('id');
|
||||
const label = await input.getAttribute('aria-label');
|
||||
const placeholder = await input.getAttribute('placeholder');
|
||||
|
||||
// Should have some form of accessible identification
|
||||
expect(id || label || placeholder).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,424 +0,0 @@
|
||||
/**
|
||||
* Security Dashboard E2E Tests
|
||||
*
|
||||
* Tests the Security (Cerberus) dashboard functionality including:
|
||||
* - Page loading and layout
|
||||
* - Module toggle states (CrowdSec, ACL, WAF, Rate Limiting)
|
||||
* - Status indicators
|
||||
* - Navigation to sub-pages
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
import {
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('Security Dashboard', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display security dashboard page title', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: /security/i }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display Cerberus dashboard header', async ({ page }) => {
|
||||
await expect(page.getByText(/cerberus.*dashboard/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show all 4 security module cards', async ({ page }) => {
|
||||
await test.step('Verify CrowdSec card exists', async () => {
|
||||
await expect(page.getByText(/crowdsec/i).first()).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify ACL card exists', async () => {
|
||||
await expect(page.getByText(/access.*control/i).first()).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify WAF card exists', async () => {
|
||||
await expect(page.getByText(/coraza.*waf|waf/i).first()).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify Rate Limiting card exists', async () => {
|
||||
await expect(page.getByText(/rate.*limiting/i).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display layer badges for each module', async ({ page }) => {
|
||||
await expect(page.getByText(/layer.*1/i)).toBeVisible();
|
||||
await expect(page.getByText(/layer.*2/i)).toBeVisible();
|
||||
await expect(page.getByText(/layer.*3/i)).toBeVisible();
|
||||
await expect(page.getByText(/layer.*4/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show audit logs button in header', async ({ page }) => {
|
||||
const auditLogsButton = page.getByRole('button', { name: /audit.*logs/i });
|
||||
await expect(auditLogsButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show docs button in header', async ({ page }) => {
|
||||
const docsButton = page.getByRole('button', { name: /docs/i });
|
||||
await expect(docsButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Module Status Indicators', () => {
|
||||
test('should show enabled/disabled badge for each module', async ({ page }) => {
|
||||
// Each card should have an enabled or disabled badge
|
||||
// Look for text that matches enabled/disabled patterns
|
||||
// The Badge component may use various styling approaches
|
||||
await page.waitForTimeout(500); // Wait for UI to settle
|
||||
|
||||
const enabledTexts = page.getByText(/^enabled$/i);
|
||||
const disabledTexts = page.getByText(/^disabled$/i);
|
||||
|
||||
const enabledCount = await enabledTexts.count();
|
||||
const disabledCount = await disabledTexts.count();
|
||||
|
||||
// Should have at least 4 status badges (one per security layer card)
|
||||
expect(enabledCount + disabledCount).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('should display CrowdSec toggle switch', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-crowdsec');
|
||||
await expect(toggle).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display ACL toggle switch', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-acl');
|
||||
await expect(toggle).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display WAF toggle switch', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-waf');
|
||||
await expect(toggle).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display Rate Limiting toggle switch', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-rate-limit');
|
||||
await expect(toggle).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Module Toggle Actions', () => {
|
||||
// Capture state ONCE for this describe block
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async ({ request: reqFixture }) => {
|
||||
try {
|
||||
originalState = await captureSecurityState(reqFixture);
|
||||
} catch (error) {
|
||||
console.warn('Could not capture initial security state:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// CRITICAL: Restore original state even if tests fail
|
||||
if (!originalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fresh request context for cleanup (cannot reuse fixture from beforeAll)
|
||||
const cleanupRequest = await request.newContext({
|
||||
baseURL: 'http://localhost:8080',
|
||||
});
|
||||
|
||||
try {
|
||||
await restoreSecurityState(cleanupRequest, originalState);
|
||||
console.log('✓ Security state restored after toggle tests');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
} finally {
|
||||
await cleanupRequest.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle ACL enabled/disabled', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-acl');
|
||||
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Toggle is disabled because Cerberus security is not enabled',
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await test.step('Toggle ACL state', async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await toggle.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await toggle.click({ force: true });
|
||||
await waitForToast(page, /updated|success|enabled|disabled/i, 10000);
|
||||
});
|
||||
|
||||
// NOTE: Do NOT toggle back here - afterAll handles cleanup
|
||||
});
|
||||
|
||||
test('should toggle WAF enabled/disabled', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-waf');
|
||||
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Toggle is disabled because Cerberus security is not enabled',
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await test.step('Toggle WAF state', async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await toggle.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await toggle.click({ force: true });
|
||||
await waitForToast(page, /updated|success|enabled|disabled/i, 10000);
|
||||
});
|
||||
|
||||
// NOTE: Do NOT toggle back here - afterAll handles cleanup
|
||||
});
|
||||
|
||||
test('should toggle Rate Limiting enabled/disabled', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-rate-limit');
|
||||
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Toggle is disabled because Cerberus security is not enabled',
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await test.step('Toggle Rate Limit state', async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await toggle.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await toggle.click({ force: true });
|
||||
await waitForToast(page, /updated|success|enabled|disabled/i, 10000);
|
||||
});
|
||||
|
||||
// NOTE: Do NOT toggle back here - afterAll handles cleanup
|
||||
});
|
||||
|
||||
test('should persist toggle state after page reload', async ({ page }) => {
|
||||
const toggle = page.getByTestId('toggle-acl');
|
||||
|
||||
const isDisabled = await toggle.isDisabled();
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Toggle is disabled because Cerberus security is not enabled',
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const initialChecked = await toggle.isChecked();
|
||||
|
||||
await test.step('Toggle ACL state', async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await toggle.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await toggle.click({ force: true });
|
||||
await waitForToast(page, /updated|success|enabled|disabled/i, 10000);
|
||||
});
|
||||
|
||||
await test.step('Reload page', async () => {
|
||||
await page.reload();
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
await test.step('Verify state persisted', async () => {
|
||||
const newChecked = await page.getByTestId('toggle-acl').isChecked();
|
||||
expect(newChecked).toBe(!initialChecked);
|
||||
});
|
||||
|
||||
// NOTE: Do NOT restore here - afterAll handles cleanup
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate to CrowdSec page when configure clicked', async ({ page }) => {
|
||||
// Find the CrowdSec card by locating the configure button within a container that has CrowdSec text
|
||||
// Cards use rounded-lg border classes, not [class*="card"]
|
||||
const crowdsecSection = page.locator('div').filter({ hasText: /crowdsec/i }).filter({ has: page.getByRole('button', { name: /configure/i }) }).first();
|
||||
const configureButton = crowdsecSection.getByRole('button', { name: /configure/i });
|
||||
|
||||
// Button may be disabled when Cerberus is off
|
||||
const isDisabled = await configureButton.isDisabled().catch(() => true);
|
||||
if (isDisabled) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Configure button is disabled because Cerberus security is not enabled'
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for any loading overlays to disappear
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Scroll element into view and use force click to bypass pointer interception
|
||||
await configureButton.scrollIntoViewIfNeeded();
|
||||
await configureButton.click({ force: true });
|
||||
await expect(page).toHaveURL(/\/security\/crowdsec/);
|
||||
});
|
||||
|
||||
test('should navigate to Access Lists page when clicked', async ({ page }) => {
|
||||
// The ACL card has a "Manage Lists" or "Configure" button
|
||||
// Find button by looking for buttons with matching text within the page
|
||||
const allConfigButtons = page.getByRole('button', { name: /manage.*lists|configure/i });
|
||||
const count = await allConfigButtons.count();
|
||||
|
||||
// The ACL button should be the second configure button (after CrowdSec)
|
||||
// Or we can find it near the "Access Control" or "ACL" text
|
||||
let aclButton = null;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const btn = allConfigButtons.nth(i);
|
||||
const btnText = await btn.textContent();
|
||||
// The ACL button says "Manage Lists" when enabled, "Configure" when disabled
|
||||
if (btnText?.match(/manage.*lists/i)) {
|
||||
aclButton = btn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to second configure button if no "Manage Lists" found
|
||||
if (!aclButton) {
|
||||
aclButton = allConfigButtons.nth(1);
|
||||
}
|
||||
|
||||
// Wait for any loading overlays and scroll into view
|
||||
await page.waitForLoadState('networkidle');
|
||||
await aclButton.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await aclButton.click({ force: true });
|
||||
await expect(page).toHaveURL(/\/security\/access-lists|\/access-lists/);
|
||||
});
|
||||
|
||||
test('should navigate to WAF page when configure clicked', async ({ page }) => {
|
||||
// WAF is Layer 3 - the third configure button in the security cards grid
|
||||
const allConfigButtons = page.getByRole('button', { name: /configure/i });
|
||||
const count = await allConfigButtons.count();
|
||||
|
||||
// Should have at least 3 configure buttons (CrowdSec, ACL/Manage Lists, WAF)
|
||||
if (count < 3) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Not enough configure buttons found on page'
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// WAF is the 3rd configure button (index 2)
|
||||
const wafButton = allConfigButtons.nth(2);
|
||||
|
||||
// Wait and scroll into view
|
||||
await page.waitForLoadState('networkidle');
|
||||
await wafButton.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await wafButton.click({ force: true });
|
||||
await expect(page).toHaveURL(/\/security\/waf/);
|
||||
});
|
||||
|
||||
test('should navigate to Rate Limiting page when configure clicked', async ({ page }) => {
|
||||
// Rate Limiting is Layer 4 - the fourth configure button in the security cards grid
|
||||
const allConfigButtons = page.getByRole('button', { name: /configure/i });
|
||||
const count = await allConfigButtons.count();
|
||||
|
||||
// Should have at least 4 configure buttons
|
||||
if (count < 4) {
|
||||
test.info().annotations.push({
|
||||
type: 'skip-reason',
|
||||
description: 'Not enough configure buttons found on page'
|
||||
});
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Rate Limit is the 4th configure button (index 3)
|
||||
const rateLimitButton = allConfigButtons.nth(3);
|
||||
|
||||
// Wait and scroll into view
|
||||
await page.waitForLoadState('networkidle');
|
||||
await rateLimitButton.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(200);
|
||||
await rateLimitButton.click({ force: true });
|
||||
await expect(page).toHaveURL(/\/security\/rate-limiting/);
|
||||
});
|
||||
|
||||
test('should navigate to Audit Logs page', async ({ page }) => {
|
||||
const auditLogsButton = page.getByRole('button', { name: /audit.*logs/i });
|
||||
|
||||
await auditLogsButton.click();
|
||||
await expect(page).toHaveURL(/\/security\/audit-logs/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Admin Whitelist', () => {
|
||||
test('should display admin whitelist section when Cerberus enabled', async ({ page }) => {
|
||||
// Check if the admin whitelist input is visible (only shown when Cerberus is enabled)
|
||||
const whitelistInput = page.getByPlaceholder(/192\.168|cidr/i);
|
||||
const isVisible = await whitelistInput.isVisible().catch(() => false);
|
||||
|
||||
if (isVisible) {
|
||||
await expect(whitelistInput).toBeVisible();
|
||||
} else {
|
||||
// Cerberus might be disabled - just verify the page loaded correctly
|
||||
// by checking for the Cerberus Dashboard header which is always visible
|
||||
const cerberusHeader = page.getByText(/cerberus.*dashboard/i);
|
||||
await expect(cerberusHeader).toBeVisible();
|
||||
|
||||
test.info().annotations.push({
|
||||
type: 'info',
|
||||
description: 'Admin whitelist section not visible - Cerberus may be disabled'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have accessible toggle switches with labels', async ({ page }) => {
|
||||
// Each toggle should be within a tooltip that describes its purpose
|
||||
// The Switch component uses an input[type="checkbox"] under the hood
|
||||
const toggles = [
|
||||
page.getByTestId('toggle-crowdsec'),
|
||||
page.getByTestId('toggle-acl'),
|
||||
page.getByTestId('toggle-waf'),
|
||||
page.getByTestId('toggle-rate-limit'),
|
||||
];
|
||||
|
||||
for (const toggle of toggles) {
|
||||
await expect(toggle).toBeVisible();
|
||||
// Switch uses checkbox input type (visually styled as toggle)
|
||||
await expect(toggle).toHaveAttribute('type', 'checkbox');
|
||||
}
|
||||
});
|
||||
|
||||
test('should navigate with keyboard', async ({ page }) => {
|
||||
await test.step('Tab through header buttons', async () => {
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
// Should be able to tab to various interactive elements
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Security Headers E2E Tests
|
||||
*
|
||||
* Tests the security headers configuration:
|
||||
* - Page loading and status
|
||||
* - Header profile management (CRUD)
|
||||
* - Preset selection
|
||||
* - Header score display
|
||||
* - Individual header configuration
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('Security Headers Configuration', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/headers');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display security headers page', async ({ page }) => {
|
||||
const heading = page.getByRole('heading', { name: /security.*headers|headers/i });
|
||||
const headingVisible = await heading.isVisible().catch(() => false);
|
||||
|
||||
if (!headingVisible) {
|
||||
const content = page.getByText(/security.*headers|csp|hsts|x-frame/i).first();
|
||||
await expect(content).toBeVisible();
|
||||
} else {
|
||||
await expect(heading).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Header Score Display', () => {
|
||||
test('should display security score', async ({ page }) => {
|
||||
const scoreDisplay = page.getByText(/score|grade|rating/i).first();
|
||||
const scoreVisible = await scoreDisplay.isVisible().catch(() => false);
|
||||
|
||||
if (scoreVisible) {
|
||||
await expect(scoreDisplay).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show score breakdown', async ({ page }) => {
|
||||
const scoreDetails = page.locator('[class*="score"], [class*="grade"]').filter({
|
||||
hasText: /a|b|c|d|f|\d+%/i
|
||||
});
|
||||
|
||||
const detailsVisible = await scoreDetails.first().isVisible().catch(() => false);
|
||||
expect(detailsVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Preset Profiles', () => {
|
||||
test('should display preset profiles', async ({ page }) => {
|
||||
const presetSection = page.getByText(/preset|profile|template/i).first();
|
||||
const presetVisible = await presetSection.isVisible().catch(() => false);
|
||||
|
||||
if (presetVisible) {
|
||||
await expect(presetSection).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should have preset options (Basic, Strict, Custom)', async ({ page }) => {
|
||||
const presets = page.locator('button, [role="option"]').filter({
|
||||
hasText: /basic|strict|custom|minimal|paranoid/i
|
||||
});
|
||||
|
||||
const count = await presets.count();
|
||||
expect(count >= 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should apply preset when selected', async ({ page }) => {
|
||||
const presetButton = page.locator('button').filter({
|
||||
hasText: /basic|strict|apply/i
|
||||
}).first();
|
||||
|
||||
const presetVisible = await presetButton.isVisible().catch(() => false);
|
||||
|
||||
if (presetVisible) {
|
||||
await test.step('Click preset button', async () => {
|
||||
await presetButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Individual Header Configuration', () => {
|
||||
test('should display CSP (Content-Security-Policy) settings', async ({ page }) => {
|
||||
const cspSection = page.getByText(/content-security-policy|csp/i).first();
|
||||
const cspVisible = await cspSection.isVisible().catch(() => false);
|
||||
|
||||
if (cspVisible) {
|
||||
await expect(cspSection).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display HSTS settings', async ({ page }) => {
|
||||
const hstsSection = page.getByText(/strict-transport-security|hsts/i).first();
|
||||
const hstsVisible = await hstsSection.isVisible().catch(() => false);
|
||||
|
||||
if (hstsVisible) {
|
||||
await expect(hstsSection).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display X-Frame-Options settings', async ({ page }) => {
|
||||
const xframeSection = page.getByText(/x-frame-options|frame/i).first();
|
||||
const xframeVisible = await xframeSection.isVisible().catch(() => false);
|
||||
|
||||
expect(xframeVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display X-Content-Type-Options settings', async ({ page }) => {
|
||||
const xctSection = page.getByText(/x-content-type|nosniff/i).first();
|
||||
const xctVisible = await xctSection.isVisible().catch(() => false);
|
||||
|
||||
expect(xctVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Header Toggle Controls', () => {
|
||||
test('should have toggles for individual headers', async ({ page }) => {
|
||||
const toggles = page.locator('[role="switch"]');
|
||||
const count = await toggles.count();
|
||||
|
||||
// Should have multiple header toggles
|
||||
expect(count >= 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should toggle header on/off', async ({ page }) => {
|
||||
const toggle = page.locator('[role="switch"]').first();
|
||||
const toggleVisible = await toggle.isVisible().catch(() => false);
|
||||
|
||||
if (toggleVisible) {
|
||||
await test.step('Toggle header', async () => {
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
await test.step('Revert toggle', async () => {
|
||||
await toggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Profile Management', () => {
|
||||
test('should have create profile button', async ({ page }) => {
|
||||
const createButton = page.getByRole('button', { name: /create|new|add.*profile/i });
|
||||
const createVisible = await createButton.isVisible().catch(() => false);
|
||||
|
||||
if (createVisible) {
|
||||
await expect(createButton).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should open profile creation modal', async ({ page }) => {
|
||||
const createButton = page.getByRole('button', { name: /create|new.*profile/i });
|
||||
const createVisible = await createButton.isVisible().catch(() => false);
|
||||
|
||||
if (createVisible) {
|
||||
await createButton.click();
|
||||
|
||||
const modal = page.getByRole('dialog');
|
||||
const modalVisible = await modal.isVisible().catch(() => false);
|
||||
|
||||
if (modalVisible) {
|
||||
// Close modal
|
||||
const closeButton = page.getByRole('button', { name: /cancel|close/i });
|
||||
await closeButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should list existing profiles', async ({ page }) => {
|
||||
const profileList = page.locator('[class*="list"], [class*="grid"]').filter({
|
||||
has: page.locator('[class*="card"], tr, [class*="item"]')
|
||||
}).first();
|
||||
|
||||
const listVisible = await profileList.isVisible().catch(() => false);
|
||||
expect(listVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Save Configuration', () => {
|
||||
test('should have save button', async ({ page }) => {
|
||||
const saveButton = page.getByRole('button', { name: /save|apply|update/i });
|
||||
const saveVisible = await saveButton.isVisible().catch(() => false);
|
||||
|
||||
if (saveVisible) {
|
||||
await expect(saveButton).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate back to security dashboard', async ({ page }) => {
|
||||
const backLink = page.getByRole('link', { name: /security|back/i });
|
||||
const backVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backVisible) {
|
||||
await backLink.click();
|
||||
await waitForLoadingComplete(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have accessible toggle controls', async ({ page }) => {
|
||||
const toggles = page.locator('[role="switch"]');
|
||||
const count = await toggles.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
||||
const toggle = toggles.nth(i);
|
||||
const visible = await toggle.isVisible();
|
||||
|
||||
if (visible) {
|
||||
// Toggle should have accessible state
|
||||
const checked = await toggle.getAttribute('aria-checked');
|
||||
expect(['true', 'false', 'mixed'].includes(checked || '')).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,236 +0,0 @@
|
||||
/**
|
||||
* WAF (Coraza) Configuration E2E Tests
|
||||
*
|
||||
* Tests the Web Application Firewall configuration:
|
||||
* - Page loading and status display
|
||||
* - WAF mode toggle (blocking/detection)
|
||||
* - Ruleset management
|
||||
* - Rule group configuration
|
||||
* - Whitelist/exclusions
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Phase 3
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
|
||||
import { waitForLoadingComplete, waitForToast } from '../utils/wait-helpers';
|
||||
|
||||
test.describe('WAF Configuration', () => {
|
||||
test.beforeEach(async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await waitForLoadingComplete(page);
|
||||
await page.goto('/security/waf');
|
||||
await waitForLoadingComplete(page);
|
||||
});
|
||||
|
||||
test.describe('Page Loading', () => {
|
||||
test('should display WAF configuration page', async ({ page }) => {
|
||||
// Page should load with WAF heading or content
|
||||
const heading = page.getByRole('heading', { name: /waf|firewall|coraza/i });
|
||||
const headingVisible = await heading.isVisible().catch(() => false);
|
||||
|
||||
if (!headingVisible) {
|
||||
// Might have different page structure
|
||||
const wafContent = page.getByText(/web application firewall|waf|coraza/i).first();
|
||||
await expect(wafContent).toBeVisible();
|
||||
} else {
|
||||
await expect(heading).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display WAF status indicator', async ({ page }) => {
|
||||
const statusBadge = page.locator('[class*="badge"]').filter({
|
||||
hasText: /enabled|disabled|blocking|detection|active/i
|
||||
});
|
||||
|
||||
const statusVisible = await statusBadge.first().isVisible().catch(() => false);
|
||||
|
||||
if (statusVisible) {
|
||||
await expect(statusBadge.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WAF Mode Toggle', () => {
|
||||
test('should display current WAF mode', async ({ page }) => {
|
||||
const modeIndicator = page.getByText(/blocking|detection|mode/i).first();
|
||||
const modeVisible = await modeIndicator.isVisible().catch(() => false);
|
||||
|
||||
if (modeVisible) {
|
||||
await expect(modeIndicator).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should have mode toggle switch or selector', async ({ page }) => {
|
||||
const modeToggle = page.getByTestId('waf-mode-toggle').or(
|
||||
page.locator('button, [role="switch"]').filter({
|
||||
hasText: /blocking|detection/i
|
||||
})
|
||||
);
|
||||
|
||||
const toggleVisible = await modeToggle.isVisible().catch(() => false);
|
||||
|
||||
if (toggleVisible) {
|
||||
await expect(modeToggle).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle between blocking and detection mode', async ({ page }) => {
|
||||
const modeSwitch = page.locator('[role="switch"]').first();
|
||||
const switchVisible = await modeSwitch.isVisible().catch(() => false);
|
||||
|
||||
if (switchVisible) {
|
||||
await test.step('Click mode switch', async () => {
|
||||
await modeSwitch.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
await test.step('Revert mode switch', async () => {
|
||||
await modeSwitch.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Ruleset Management', () => {
|
||||
test('should display available rulesets', async ({ page }) => {
|
||||
const rulesetSection = page.getByText(/ruleset|rules|owasp|crs/i).first();
|
||||
await expect(rulesetSection).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show rule groups with toggle controls', async ({ page }) => {
|
||||
// Each rule group should be toggleable
|
||||
const ruleGroupToggles = page.locator('[role="switch"], input[type="checkbox"]').filter({
|
||||
has: page.locator('text=/sql|xss|rce|lfi|rfi|scanner/i')
|
||||
});
|
||||
|
||||
// Count available toggles
|
||||
const count = await ruleGroupToggles.count().catch(() => 0);
|
||||
expect(count >= 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should allow enabling/disabling rule groups', async ({ page }) => {
|
||||
const ruleToggle = page.locator('[role="switch"]').first();
|
||||
const toggleVisible = await ruleToggle.isVisible().catch(() => false);
|
||||
|
||||
if (toggleVisible) {
|
||||
// Record initial state
|
||||
const wasPressed = await ruleToggle.getAttribute('aria-pressed') === 'true' ||
|
||||
await ruleToggle.getAttribute('aria-checked') === 'true';
|
||||
|
||||
await test.step('Toggle rule group', async () => {
|
||||
await ruleToggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
await test.step('Restore original state', async () => {
|
||||
await ruleToggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Anomaly Threshold', () => {
|
||||
test('should display anomaly threshold setting', async ({ page }) => {
|
||||
const thresholdSection = page.getByText(/threshold|score|anomaly/i).first();
|
||||
const thresholdVisible = await thresholdSection.isVisible().catch(() => false);
|
||||
|
||||
if (thresholdVisible) {
|
||||
await expect(thresholdSection).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should have threshold input control', async ({ page }) => {
|
||||
const thresholdInput = page.locator('input[type="number"], input[type="range"]').filter({
|
||||
has: page.locator('text=/threshold|score/i')
|
||||
}).first();
|
||||
|
||||
const inputVisible = await thresholdInput.isVisible().catch(() => false);
|
||||
|
||||
// Threshold control might not be visible on all pages
|
||||
expect(inputVisible !== undefined).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Whitelist/Exclusions', () => {
|
||||
test('should display whitelist section', async ({ page }) => {
|
||||
const whitelistSection = page.getByText(/whitelist|exclusion|exception|ignore/i).first();
|
||||
const whitelistVisible = await whitelistSection.isVisible().catch(() => false);
|
||||
|
||||
if (whitelistVisible) {
|
||||
await expect(whitelistSection).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should have ability to add whitelist entries', async ({ page }) => {
|
||||
const addButton = page.getByRole('button', { name: /add.*whitelist|add.*exclusion|add.*exception/i });
|
||||
const addVisible = await addButton.isVisible().catch(() => false);
|
||||
|
||||
if (addVisible) {
|
||||
await expect(addButton).toBeEnabled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Save and Apply', () => {
|
||||
test('should have save button', async ({ page }) => {
|
||||
const saveButton = page.getByRole('button', { name: /save|apply|update/i });
|
||||
const saveVisible = await saveButton.isVisible().catch(() => false);
|
||||
|
||||
if (saveVisible) {
|
||||
await expect(saveButton).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show confirmation on save', async ({ page }) => {
|
||||
const saveButton = page.getByRole('button', { name: /save|apply/i });
|
||||
const saveVisible = await saveButton.isVisible().catch(() => false);
|
||||
|
||||
if (saveVisible) {
|
||||
await saveButton.click();
|
||||
|
||||
// Should show either confirmation dialog or success toast
|
||||
const dialog = page.getByRole('dialog');
|
||||
const dialogVisible = await dialog.isVisible().catch(() => false);
|
||||
|
||||
if (dialogVisible) {
|
||||
const cancelButton = page.getByRole('button', { name: /cancel|close/i });
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test('should navigate back to security dashboard', async ({ page }) => {
|
||||
const backLink = page.getByRole('link', { name: /security|back/i });
|
||||
const backVisible = await backLink.isVisible().catch(() => false);
|
||||
|
||||
if (backVisible) {
|
||||
await backLink.click();
|
||||
await waitForLoadingComplete(page);
|
||||
await expect(page).toHaveURL(/\/security(?!\/waf)/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have accessible controls', async ({ page }) => {
|
||||
const switches = page.locator('[role="switch"]');
|
||||
const count = await switches.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
||||
const switchEl = switches.nth(i);
|
||||
const visible = await switchEl.isVisible();
|
||||
|
||||
if (visible) {
|
||||
// Each switch should have accessible name
|
||||
const name = await switchEl.getAttribute('aria-label') ||
|
||||
await switchEl.getAttribute('aria-labelledby');
|
||||
// Some form of accessible name should exist
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user