Files
Charon/tests/security/crowdsec-dashboard.spec.ts
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

274 lines
11 KiB
TypeScript
Executable File

/**
* CrowdSec Dashboard E2E Tests
*
* Tests the CrowdSec Dashboard tab functionality including:
* - Tab navigation between Configuration and Dashboard
* - Summary cards rendering
* - Chart components rendering
* - Time range selector interaction
* - Active decisions table display
* - Alerts list display
* - Export button functionality
* - Refresh button functionality
*
* @see /projects/Charon/docs/plans/current_spec.md PR-2, PR-3
*/
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
import { waitForLoadingComplete } from '../utils/wait-helpers';
test.describe('CrowdSec Dashboard @security', () => {
test.beforeEach(async ({ page, adminUser }) => {
await loginUser(page, adminUser);
await waitForLoadingComplete(page);
await page.goto('/security/crowdsec');
await waitForLoadingComplete(page);
});
test.describe('Tab Navigation', () => {
test('should display Configuration and Dashboard tabs', async ({ page }) => {
await test.step('Verify tab list is present', async () => {
const tabList = page.getByRole('tablist');
await expect(tabList).toBeVisible();
});
await test.step('Verify Configuration tab', async () => {
const configTab = page.getByRole('tab', { name: /configuration/i });
await expect(configTab).toBeVisible();
});
await test.step('Verify Dashboard tab', async () => {
const dashboardTab = page.getByRole('tab', { name: /dashboard/i });
await expect(dashboardTab).toBeVisible();
});
});
test('should default to Configuration tab', async ({ page }) => {
await test.step('Verify Configuration tab is selected by default', async () => {
const configTab = page.getByRole('tab', { name: /configuration/i });
await expect(configTab).toHaveAttribute('data-state', 'active');
});
});
test('should switch to Dashboard tab when clicked', async ({ page }) => {
await test.step('Click Dashboard tab', async () => {
await page.getByRole('tab', { name: /dashboard/i }).click();
});
await test.step('Verify Dashboard tab is selected', async () => {
const dashboardTab = page.getByRole('tab', { name: /dashboard/i });
await expect(dashboardTab).toHaveAttribute('data-state', 'active');
});
await test.step('Verify dashboard content is visible', async () => {
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible({ timeout: 10000 });
});
});
test('should switch back to Configuration tab', async ({ page }) => {
await test.step('Navigate to Dashboard tab', async () => {
await page.getByRole('tab', { name: /dashboard/i }).click();
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible({ timeout: 10000 });
});
await test.step('Switch back to Configuration tab', async () => {
await page.getByRole('tab', { name: /configuration/i }).click();
});
await test.step('Verify Configuration content is visible', async () => {
const configTab = page.getByRole('tab', { name: /configuration/i });
await expect(configTab).toHaveAttribute('data-state', 'active');
});
});
});
test.describe('Dashboard Content', () => {
test.beforeEach(async ({ page }) => {
await page.getByRole('tab', { name: /dashboard/i }).click();
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible({ timeout: 10000 });
});
test('should display summary cards section', async ({ page }) => {
await test.step('Verify summary cards container', async () => {
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible();
});
});
test('should display time range selector', async ({ page }) => {
await test.step('Verify time range buttons are present', async () => {
const timeRangeGroup = page.getByRole('tablist', { name: /time range/i });
const timeRangeVisible = await timeRangeGroup.isVisible().catch(() => false);
if (timeRangeVisible) {
await expect(timeRangeGroup).toBeVisible();
} else {
// Fallback: look for individual time range buttons
const button24h = page.getByRole('tab', { name: /24h/i });
const buttonVisible = await button24h.isVisible().catch(() => false);
if (buttonVisible) {
await expect(button24h).toBeVisible();
} else {
test.info().annotations.push({
type: 'info',
description: 'Time range selector not visible - may use different UI pattern',
});
}
}
});
});
test('should display refresh button', async ({ page }) => {
await test.step('Verify refresh button exists', async () => {
const refreshButton = page.getByRole('button', { name: /refresh/i });
await expect(refreshButton).toBeVisible();
});
});
test('should display chart sections', async ({ page }) => {
await test.step('Verify chart containers are rendered', async () => {
// Charts render as role="img" with aria-labels
const banTimeline = page.getByRole('img', { name: /ban.*timeline|timeline.*chart/i });
const banTimelineVisible = await banTimeline.isVisible().catch(() => false);
const topIPs = page.getByRole('img', { name: /top.*ip|attacking.*ip/i });
const topIPsVisible = await topIPs.isVisible().catch(() => false);
const scenarios = page.getByRole('img', { name: /scenario.*breakdown|scenario.*chart/i });
const scenariosVisible = await scenarios.isVisible().catch(() => false);
// At least one chart should render (even with error/empty state)
// Charts may not render if CrowdSec is not running
if (!banTimelineVisible && !topIPsVisible && !scenariosVisible) {
// Check for error or empty state messages instead
const errorOrEmpty = page.getByText(/error|no data|unavailable|failed/i).first();
const hasMessage = await errorOrEmpty.isVisible().catch(() => false);
if (!hasMessage) {
test.info().annotations.push({
type: 'info',
description: 'Charts not rendered - CrowdSec may not be running or no data available',
});
}
}
});
});
test('should display active decisions table', async ({ page }) => {
await test.step('Verify decisions table or empty state', async () => {
const table = page.getByRole('table');
const tableVisible = await table.isVisible().catch(() => false);
if (tableVisible) {
// If table is visible, verify it has column headers
const headers = page.getByRole('columnheader');
const headerCount = await headers.count();
expect(headerCount).toBeGreaterThan(0);
} else {
// Table may not render if no data or CrowdSec not running
const errorOrEmpty = page.getByText(/error|no.*decisions|no.*alerts|no data/i).first();
const hasMessage = await errorOrEmpty.isVisible().catch(() => false);
if (!hasMessage) {
test.info().annotations.push({
type: 'info',
description: 'Active decisions table not visible - CrowdSec may not be running',
});
}
}
});
});
test('should display alerts list section', async ({ page }) => {
await test.step('Verify alerts list or empty state', async () => {
const alertsList = page.getByTestId('alerts-list');
await expect(alertsList).toBeVisible({ timeout: 10000 });
});
});
test('should display export button', async ({ page }) => {
await test.step('Verify export button is visible', async () => {
const exportBtn = page.getByRole('button', { name: /export/i });
await expect(exportBtn).toBeVisible();
});
await test.step('Open export dropdown', async () => {
const exportBtn = page.getByRole('button', { name: /export/i });
await exportBtn.click();
const menu = page.getByRole('menu');
const menuVisible = await menu.isVisible().catch(() => false);
if (menuVisible) {
const csvOption = page.getByRole('menuitem', { name: /csv/i });
const jsonOption = page.getByRole('menuitem', { name: /json/i });
await expect(csvOption).toBeVisible();
await expect(jsonOption).toBeVisible();
} else {
test.info().annotations.push({
type: 'info',
description: 'Export dropdown menu did not open - may need interaction delay',
});
}
});
});
});
test.describe('Time Range Interaction', () => {
test.beforeEach(async ({ page }) => {
await page.getByRole('tab', { name: /dashboard/i }).click();
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible({ timeout: 10000 });
});
test('should update data when selecting different time range', async ({ page }) => {
await test.step('Select 7d time range', async () => {
const sevenDayButton = page.getByRole('tab', { name: /7d/i });
const sevenDayVisible = await sevenDayButton.isVisible().catch(() => false);
if (sevenDayVisible) {
await sevenDayButton.click();
await expect(sevenDayButton).toHaveAttribute('aria-selected', 'true');
} else {
test.info().annotations.push({
type: 'info',
description: '7d time range button not found - may use different selector pattern',
});
}
});
});
});
test.describe('Refresh Functionality', () => {
test.beforeEach(async ({ page }) => {
await page.getByRole('tab', { name: /dashboard/i }).click();
await expect(page.getByTestId('dashboard-summary-cards')).toBeVisible({ timeout: 10000 });
});
test('should trigger data refresh when clicking refresh button', async ({ page }) => {
await test.step('Click refresh and verify loading state', async () => {
const refreshButton = page.getByRole('button', { name: /refresh/i });
await expect(refreshButton).toBeVisible();
// Click refresh and expect API calls to be triggered
const responsePromise = page.waitForResponse(
resp => resp.url().includes('/crowdsec/dashboard') || resp.url().includes('/crowdsec/alerts'),
{ timeout: 10000 }
).catch(() => null);
await refreshButton.click();
const response = await responsePromise;
if (response) {
// API call was made - refresh working
expect(response.status()).toBeLessThan(500);
} else {
test.info().annotations.push({
type: 'info',
description: 'No dashboard API response detected after refresh - endpoints may not be implemented',
});
}
});
});
});
});