Detailed explanation of: - **Dependency Fix**: Added explicit Chromium installation to Firefox and WebKit security jobs. The authentication fixture depends on Chromium being present, even when testing other browsers, causing previous runs to fail setup. - **Workflow Isolation**: Explicitly routed `tests/security/` to the dedicated "Security Enforcement" jobs and removed them from the general shards. This prevents false negatives where security config tests fail because the middleware is intentionally disabled in standard test runs. - **Metadata**: Added `@security` tags to all security specs (`rate-limiting`, `waf-config`, etc.) to align metadata with the new execution strategy. - **References**: Fixes CI failures in PR
302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
/**
|
|
* 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 @security', () => {
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|