finish secrets finding work and audit remarks work save
This commit is contained in:
518
src/Web/StellaOps.Web/e2e/secret-detection.e2e.spec.ts
Normal file
518
src/Web/StellaOps.Web/e2e/secret-detection.e2e.spec.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user