519 lines
18 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|