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
368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
/**
|
||
* 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 @security', () => {
|
||
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();
|
||
});
|
||
});
|
||
});
|