Files
Charon/tests/security/audit-logs.spec.ts
2026-03-04 18:34:49 +00:00

366 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
*/
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();
await expect(userFilter).toBeVisible();
});
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();
await expect(pagination).toBeVisible();
});
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();
});
});
});