Files
git.stella-ops.org/src/Web/StellaOps.Web/e2e/secret-detection.e2e.spec.ts
2026-01-04 21:48:13 +02:00

519 lines
18 KiB
TypeScript

/**
* Secret Detection UI E2E Tests.
* Sprint: SPRINT_20260104_008_FE (Secret Detection UI)
* Task: SDU-012 - E2E tests
*
* Tests the complete secret detection configuration and viewing flow:
* 1. Settings page navigation and toggles
* 2. Revelation policy selection
* 3. Rule category configuration
* 4. Exception management
* 5. Alert destination configuration
* 6. Findings list and masked value display
*/
import { test, expect, Page } from '@playwright/test';
test.describe('Secret Detection UI', () => {
const settingsUrl = '/tenants/test-tenant/secrets/settings';
const findingsUrl = '/tenants/test-tenant/secrets/findings';
test.describe('Settings Page', () => {
test('displays settings page with header and tabs', async ({ page }) => {
await page.goto(settingsUrl);
// Verify header is visible
const header = page.locator('.settings-header');
await expect(header).toBeVisible();
await expect(header).toContainText('Secret Detection');
// Verify enable/disable toggle
const toggle = page.locator('mat-slide-toggle');
await expect(toggle).toBeVisible();
// Verify tabs are present
const tabs = page.locator('mat-tab-group');
await expect(tabs).toBeVisible();
await expect(page.locator('role=tab')).toHaveCount(3);
});
test('toggles secret detection on and off', async ({ page }) => {
await page.goto(settingsUrl);
const toggle = page.locator('mat-slide-toggle');
const initialState = await toggle.getAttribute('class');
// Toggle the switch
await toggle.click();
// Wait for API call and state change
await page.waitForResponse(resp =>
resp.url().includes('/secrets/config/settings') && resp.status() === 200
);
// Verify snackbar confirmation
const snackbar = page.locator('mat-snack-bar-container');
await expect(snackbar).toBeVisible();
});
test('navigates between tabs', async ({ page }) => {
await page.goto(settingsUrl);
// Click Exceptions tab
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
await expect(page.locator('app-exception-manager')).toBeVisible();
// Click Alerts tab
await page.locator('role=tab', { hasText: 'Alerts' }).click();
await expect(page.locator('app-alert-destination-config')).toBeVisible();
// Back to General tab
await page.locator('role=tab', { hasText: 'General' }).click();
await expect(page.locator('app-revelation-policy-selector')).toBeVisible();
});
});
test.describe('Revelation Policy Selector', () => {
test('displays all policy options', async ({ page }) => {
await page.goto(settingsUrl);
// Wait for component to load
const selector = page.locator('app-revelation-policy-selector');
await expect(selector).toBeVisible();
// Verify all four options are visible
await expect(page.locator('.policy-option')).toHaveCount(4);
await expect(page.getByText('Fully Masked')).toBeVisible();
await expect(page.getByText('Partially Revealed')).toBeVisible();
await expect(page.getByText('Full Revelation')).toBeVisible();
await expect(page.getByText('Redacted')).toBeVisible();
});
test('shows preview for selected policy', async ({ page }) => {
await page.goto(settingsUrl);
// Select partial reveal
const partialOption = page.locator('.policy-option', { hasText: 'Partially Revealed' });
await partialOption.click();
// Verify preview is shown
const preview = partialOption.locator('.preview code');
await expect(preview).toBeVisible();
await expect(preview).toContainText('****');
});
test('shows additional controls when masked policy selected', async ({ page }) => {
await page.goto(settingsUrl);
// Select masked policy
const maskedOption = page.locator('.policy-option', { hasText: 'Fully Masked' });
await maskedOption.click();
// Verify additional controls appear
await expect(maskedOption.locator('.option-controls')).toBeVisible();
await expect(maskedOption.locator('mat-form-field', { hasText: 'Mask Character' })).toBeVisible();
});
test('updates preview when mask length changes', async ({ page }) => {
await page.goto(settingsUrl);
// Select masked policy
const maskedOption = page.locator('.policy-option', { hasText: 'Fully Masked' });
await maskedOption.click();
// Change mask length
const lengthSelect = maskedOption.locator('mat-select', { hasText: /characters/ });
await lengthSelect.click();
await page.locator('mat-option', { hasText: '16 characters' }).click();
// Verify preview updated
const preview = maskedOption.locator('.preview code');
const text = await preview.textContent();
expect(text?.length).toBe(16);
});
});
test.describe('Rule Category Toggles', () => {
test('displays rule categories grouped by type', async ({ page }) => {
await page.goto(settingsUrl);
const toggles = page.locator('app-rule-category-toggles');
await expect(toggles).toBeVisible();
// Verify accordion panels exist
const panels = page.locator('mat-expansion-panel');
await expect(panels.first()).toBeVisible();
});
test('shows selection count in toolbar', async ({ page }) => {
await page.goto(settingsUrl);
const selectionInfo = page.locator('.selection-info');
await expect(selectionInfo).toBeVisible();
await expect(selectionInfo).toContainText(/\d+ of \d+ categories selected/);
});
test('toggles entire group when group checkbox clicked', async ({ page }) => {
await page.goto(settingsUrl);
// Expand first group
const panel = page.locator('mat-expansion-panel').first();
await panel.click();
// Get the group checkbox
const groupCheckbox = panel.locator('mat-expansion-panel-header mat-checkbox');
// Toggle group
await groupCheckbox.click();
// Wait for API call
await page.waitForResponse(resp =>
resp.url().includes('/secrets/config/settings') && resp.status() === 200
);
});
test('select all and clear all buttons work', async ({ page }) => {
await page.goto(settingsUrl);
// Click select all
await page.locator('button', { hasText: 'Select All' }).click();
// Wait for API response
await page.waitForResponse(resp =>
resp.url().includes('/secrets/config/settings') && resp.status() === 200
);
// Click clear all
await page.locator('button', { hasText: 'Clear All' }).click();
await page.waitForResponse(resp =>
resp.url().includes('/secrets/config/settings') && resp.status() === 200
);
});
});
test.describe('Exception Manager', () => {
test('displays exception list on Exceptions tab', async ({ page }) => {
await page.goto(settingsUrl);
// Navigate to exceptions tab
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
const manager = page.locator('app-exception-manager');
await expect(manager).toBeVisible();
});
test('opens create dialog when Add Exception clicked', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
// Click add button
await page.locator('button', { hasText: 'Add Exception' }).click();
// Verify dialog opens
const dialog = page.locator('.dialog-overlay');
await expect(dialog).toBeVisible();
await expect(page.locator('.dialog-card')).toContainText('Create Exception');
});
test('validates required fields in exception form', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
await page.locator('button', { hasText: 'Add Exception' }).click();
// Try to submit without required fields
const submitButton = page.locator('button[type="submit"]');
await expect(submitButton).toBeDisabled();
// Fill required fields
await page.locator('input[formcontrolname="name"]').fill('Test Exception');
await page.locator('input[formcontrolname="pattern"]').fill('test-pattern');
// Button should now be enabled
await expect(submitButton).toBeEnabled();
});
test('closes dialog when Cancel clicked', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
await page.locator('button', { hasText: 'Add Exception' }).click();
// Click cancel
await page.locator('button', { hasText: 'Cancel' }).click();
// Dialog should close
const dialog = page.locator('.dialog-overlay');
await expect(dialog).not.toBeVisible();
});
});
test.describe('Alert Destination Configuration', () => {
test('displays alert configuration on Alerts tab', async ({ page }) => {
await page.goto(settingsUrl);
// Navigate to alerts tab
await page.locator('role=tab', { hasText: 'Alerts' }).click();
const config = page.locator('app-alert-destination-config');
await expect(config).toBeVisible();
await expect(page.getByText('Alert Destinations')).toBeVisible();
});
test('shows global settings when alerts enabled', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Alerts' }).click();
// Enable alerts if not already
const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle');
const isChecked = await enableToggle.getAttribute('class');
if (!isChecked?.includes('checked')) {
await enableToggle.click();
}
// Verify global settings visible
await expect(page.locator('.global-settings')).toBeVisible();
await expect(page.getByText('Minimum Severity')).toBeVisible();
await expect(page.getByText('Rate Limit')).toBeVisible();
});
test('adds new destination when Add Destination clicked', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Alerts' }).click();
// Enable alerts
const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle');
const isChecked = await enableToggle.getAttribute('class');
if (!isChecked?.includes('checked')) {
await enableToggle.click();
}
// Count initial destinations
const initialCount = await page.locator('mat-expansion-panel').count();
// Add destination
await page.locator('button', { hasText: 'Add Destination' }).click();
// Verify new panel added
const newCount = await page.locator('mat-expansion-panel').count();
expect(newCount).toBe(initialCount + 1);
});
test('shows different config fields based on destination type', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Alerts' }).click();
// Enable alerts and add destination
const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle');
const isChecked = await enableToggle.getAttribute('class');
if (!isChecked?.includes('checked')) {
await enableToggle.click();
}
await page.locator('button', { hasText: 'Add Destination' }).click();
// Expand the new destination
const panel = page.locator('mat-expansion-panel').last();
await panel.click();
// Select Slack type
await panel.locator('mat-select', { hasText: 'Type' }).click();
await page.locator('mat-option', { hasText: 'Slack' }).click();
// Verify Slack-specific fields
await expect(panel.locator('mat-form-field', { hasText: 'Webhook URL' })).toBeVisible();
await expect(panel.locator('mat-form-field', { hasText: 'Channel' })).toBeVisible();
});
test('test button sends test alert', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Alerts' }).click();
// Enable alerts
const enableToggle = page.locator('app-alert-destination-config mat-slide-toggle');
const isChecked = await enableToggle.getAttribute('class');
if (!isChecked?.includes('checked')) {
await enableToggle.click();
}
// Expand first destination (if exists)
const panel = page.locator('mat-expansion-panel').first();
if (await panel.isVisible()) {
await panel.click();
// Click test button
const testButton = panel.locator('button', { hasText: 'Test' });
await testButton.click();
// Wait for API response
await page.waitForResponse(resp =>
resp.url().includes('/alerts/test') && resp.status() === 200
);
// Verify snackbar shows result
const snackbar = page.locator('mat-snack-bar-container');
await expect(snackbar).toBeVisible();
}
});
});
test.describe('Findings List', () => {
test('displays findings list with filters', async ({ page }) => {
await page.goto(findingsUrl);
// Verify header
await expect(page.getByText('Secret Findings')).toBeVisible();
// Verify filters
await expect(page.locator('.filters')).toBeVisible();
await expect(page.locator('mat-form-field', { hasText: 'Severity' })).toBeVisible();
await expect(page.locator('mat-form-field', { hasText: 'Status' })).toBeVisible();
});
test('displays table with correct columns', async ({ page }) => {
await page.goto(findingsUrl);
const table = page.locator('table[mat-table]');
await expect(table).toBeVisible();
// Verify column headers
await expect(page.locator('th', { hasText: 'Severity' })).toBeVisible();
await expect(page.locator('th', { hasText: 'Type' })).toBeVisible();
await expect(page.locator('th', { hasText: 'Location' })).toBeVisible();
await expect(page.locator('th', { hasText: 'Value' })).toBeVisible();
await expect(page.locator('th', { hasText: 'Status' })).toBeVisible();
});
test('filters by severity', async ({ page }) => {
await page.goto(findingsUrl);
// Open severity filter
await page.locator('mat-form-field', { hasText: 'Severity' }).click();
// Select Critical
await page.locator('mat-option', { hasText: 'Critical' }).click();
// Close dropdown
await page.keyboard.press('Escape');
// Wait for filtered results
await page.waitForResponse(resp =>
resp.url().includes('/findings') && resp.url().includes('severity')
);
});
test('pagination works correctly', async ({ page }) => {
await page.goto(findingsUrl);
const paginator = page.locator('mat-paginator');
await expect(paginator).toBeVisible();
// Click next page if available
const nextButton = paginator.locator('button[aria-label="Next page"]');
if (await nextButton.isEnabled()) {
await nextButton.click();
// Verify page changed
await page.waitForResponse(resp =>
resp.url().includes('/findings') && resp.url().includes('page=2')
);
}
});
test('clicking row navigates to details', async ({ page }) => {
await page.goto(findingsUrl);
// Click first row
const firstRow = page.locator('tr[mat-row]').first();
if (await firstRow.isVisible()) {
await firstRow.click();
// Verify navigation
await expect(page).toHaveURL(/\/findings\/.+/);
}
});
});
test.describe('Masked Value Display', () => {
test('displays masked value by default', async ({ page }) => {
await page.goto(findingsUrl);
const maskedDisplay = page.locator('app-masked-value-display').first();
if (await maskedDisplay.isVisible()) {
const code = maskedDisplay.locator('.value-text');
await expect(code).toBeVisible();
await expect(code).toContainText(/\*+/);
}
});
test('reveal button shows for authorized users', async ({ page }) => {
await page.goto(findingsUrl);
const maskedDisplay = page.locator('app-masked-value-display').first();
if (await maskedDisplay.isVisible()) {
const revealButton = maskedDisplay.locator('button[mattooltip="Reveal secret value"]');
// Button visibility depends on user permissions
// Just verify the component structure
await expect(maskedDisplay.locator('.value-actions')).toBeVisible();
}
});
test('copy button is disabled when value is masked', async ({ page }) => {
await page.goto(findingsUrl);
const maskedDisplay = page.locator('app-masked-value-display').first();
if (await maskedDisplay.isVisible()) {
const copyButton = maskedDisplay.locator('button[mattooltip*="Copy"]');
if (await copyButton.isVisible()) {
// Copy should be disabled when not revealed
await expect(copyButton).toBeDisabled();
}
}
});
});
test.describe('Accessibility', () => {
test('settings page is keyboard navigable', async ({ page }) => {
await page.goto(settingsUrl);
// Tab through elements
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Verify focus is visible
const focusedElement = await page.locator(':focus');
await expect(focusedElement).toBeVisible();
});
test('tabs can be selected with keyboard', async ({ page }) => {
await page.goto(settingsUrl);
// Focus tab list
const tabList = page.locator('mat-tab-group');
await tabList.focus();
// Navigate tabs with arrow keys
await page.keyboard.press('ArrowRight');
// Verify tab changed
const activeTab = page.locator('[role="tab"][aria-selected="true"]');
await expect(activeTab).toContainText('Exceptions');
});
test('dialogs trap focus', async ({ page }) => {
await page.goto(settingsUrl);
await page.locator('role=tab', { hasText: 'Exceptions' }).click();
await page.locator('button', { hasText: 'Add Exception' }).click();
// Tab through dialog elements
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Focus should stay within dialog
const focusedElement = await page.locator(':focus');
const dialogCard = page.locator('.dialog-card');
await expect(dialogCard.locator(':focus')).toBeVisible();
});
});
});