import { expect, test } from '@playwright/test'; import { exceptionUserSession, exceptionApproverSession, exceptionAdminSession, } from '../../src/app/testing/auth-fixtures'; const mockConfig = { authority: { issuer: 'https://authority.local', clientId: 'stellaops-ui', authorizeEndpoint: 'https://authority.local/connect/authorize', tokenEndpoint: 'https://authority.local/connect/token', logoutEndpoint: 'https://authority.local/connect/logout', redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', scope: 'openid profile email ui.read exceptions:read exceptions:manage exceptions:approve admin', audience: 'https://scanner.local', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: 'https://authority.local', scanner: 'https://scanner.local', policy: 'https://scanner.local', concelier: 'https://concelier.local', attestor: 'https://attestor.local', }, quickstartMode: true, }; const mockException = { exceptionId: 'exc-test-001', name: 'test-exception', displayName: 'Test Exception E2E', description: 'E2E test exception', type: 'vulnerability', severity: 'high', status: 'pending_review', scope: { type: 'global', vulnIds: ['CVE-2024-9999'], }, justification: { text: 'This is a test exception for E2E testing', }, timebox: { startDate: new Date().toISOString(), endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }, labels: {}, createdBy: 'user-exception-requester', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; function setupMockRoutes(page) { // Mock config page.route('**/config.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig), }) ); // Mock exception list API page.route('**/api/v1/exceptions?*', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [mockException], total: 1 }), }) ); // Mock exception detail API page.route(`**/api/v1/exceptions/${mockException.exceptionId}`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockException), }) ); // Mock exception create API page.route('**/api/v1/exceptions', async (route) => { if (route.request().method() === 'POST') { const newException = { ...mockException, exceptionId: 'exc-new-001', status: 'draft', }; route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(newException), }); } else { route.continue(); } }); // Mock exception transition API page.route('**/api/v1/exceptions/*/transition', (route) => { const approvedException = { ...mockException, status: 'approved', }; route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(approvedException), }); }); // Mock exception update API page.route('**/api/v1/exceptions/*', async (route) => { if (route.request().method() === 'PATCH' || route.request().method() === 'PUT') { const updatedException = { ...mockException, description: 'Updated description', }; route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(updatedException), }); } else { route.continue(); } }); // Mock SSE events page.route('**/api/v1/exceptions/events', (route) => route.fulfill({ status: 200, contentType: 'text/event-stream', body: '', }) ); // Block authority page.route('https://authority.local/**', (route) => route.abort()); } test.describe.skip('Exception Lifecycle - User Flow' /* TODO: Exception wizard UI not yet implemented */, () => { test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionUserSession); await setupMockRoutes(page); }); test('create exception flow', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Open wizard const createButton = page.getByRole('button', { name: /create exception/i }); await expect(createButton).toBeVisible(); await createButton.click(); // Wizard should be visible await expect(page.getByRole('dialog', { name: /exception wizard/i })).toBeVisible(); // Fill in basic info await page.getByLabel('Title').fill('Test Exception'); await page.getByLabel('Justification').fill('This is a test justification'); // Select severity await page.getByLabel('Severity').selectOption('high'); // Fill scope (CVEs) await page.getByLabel('CVE IDs').fill('CVE-2024-9999'); // Set expiry await page.getByLabel('Expires in days').fill('30'); // Submit const submitButton = page.getByRole('button', { name: /submit|create/i }); await expect(submitButton).toBeEnabled(); await submitButton.click(); // Wizard should close await expect(page.getByRole('dialog', { name: /exception wizard/i })).toBeHidden(); // Exception should appear in list await expect(page.getByText('Test Exception E2E')).toBeVisible(); }); test('displays exception list', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Exception should be visible in list await expect(page.getByText('Test Exception E2E')).toBeVisible(); await expect(page.getByText('CVE-2024-9999')).toBeVisible(); }); test('opens exception detail panel', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Click on exception to view detail await page.getByText('Test Exception E2E').click(); // Detail panel should open await expect(page.getByText('This is a test exception for E2E testing')).toBeVisible(); await expect(page.getByText('CVE-2024-9999')).toBeVisible(); }); }); test.describe.skip('Exception Lifecycle - Approval Flow' /* TODO: Exception approval queue UI not yet implemented */, () => { test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionApproverSession); await setupMockRoutes(page); }); test('approval queue shows pending exceptions', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /approval queue/i })).toBeVisible({ timeout: 10000, }); // Pending exception should be visible await expect(page.getByText('Test Exception E2E')).toBeVisible(); await expect(page.getByText(/pending/i)).toBeVisible(); }); test('approve exception', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /approval queue/i })).toBeVisible({ timeout: 10000, }); // Select exception const checkbox = page.getByRole('checkbox', { name: /select exception/i }).first(); await checkbox.check(); // Approve button should be enabled const approveButton = page.getByRole('button', { name: /approve/i }); await expect(approveButton).toBeEnabled(); await approveButton.click(); // Confirmation or success message await expect(page.getByText(/approved/i)).toBeVisible({ timeout: 5000 }); }); test('reject exception requires comment', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /approval queue/i })).toBeVisible({ timeout: 10000, }); // Select exception const checkbox = page.getByRole('checkbox', { name: /select exception/i }).first(); await checkbox.check(); // Try to reject without comment const rejectButton = page.getByRole('button', { name: /reject/i }); await rejectButton.click(); // Error message should appear await expect(page.getByText(/comment.*required/i)).toBeVisible(); // Add comment await page.getByLabel(/rejection comment/i).fill('Does not meet policy requirements'); // Reject should now work await rejectButton.click(); // Success or confirmation await expect(page.getByText(/rejected/i)).toBeVisible({ timeout: 5000 }); }); }); test.describe.skip('Exception Lifecycle - Admin Flow' /* TODO: Exception admin UI not yet implemented */, () => { test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionAdminSession); await setupMockRoutes(page); }); test('edit exception details', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Open exception detail await page.getByText('Test Exception E2E').click(); // Edit description const descriptionField = page.getByLabel(/description/i); await descriptionField.fill('Updated description for E2E test'); // Save changes const saveButton = page.getByRole('button', { name: /save/i }); await expect(saveButton).toBeEnabled(); await saveButton.click(); // Success message or confirmation await expect(page.getByText(/saved|updated/i)).toBeVisible({ timeout: 5000 }); }); test('extend exception expiry', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Open exception detail await page.getByText('Test Exception E2E').click(); // Find extend button const extendButton = page.getByRole('button', { name: /extend/i }); await expect(extendButton).toBeVisible(); // Set extension days await page.getByLabel(/extend.*days/i).fill('14'); await extendButton.click(); // Confirmation await expect(page.getByText(/extended/i)).toBeVisible({ timeout: 5000 }); }); test('exception transition workflow', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Open exception detail await page.getByText('Test Exception E2E').click(); // Transition button should be available const transitionButton = page.getByRole('button', { name: /approve|activate/i }).first(); await expect(transitionButton).toBeVisible(); await transitionButton.click(); // Confirmation or success await expect(page.getByText(/approved|activated/i)).toBeVisible({ timeout: 5000 }); }); }); test.describe.skip('Exception Lifecycle - Role-Based Access' /* TODO: Exception RBAC UI not yet implemented */, () => { test('user without approve scope cannot see approval queue', async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionUserSession); await setupMockRoutes(page); await page.goto('/policy/exceptions'); // Should redirect or show access denied await expect( page.getByText(/access denied|not authorized|forbidden/i) ).toBeVisible({ timeout: 10000 }); }); test('approver can access approval queue', async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionApproverSession); await setupMockRoutes(page); await page.goto('/policy/exceptions'); // Should show approval queue await expect(page.getByRole('heading', { name: /approval queue/i })).toBeVisible({ timeout: 10000, }); }); }); test.describe.skip('Exception Export' /* TODO: Exception export UI not yet implemented */, () => { test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors } (window as any).__stellaopsTestSession = session; }, exceptionAdminSession); await setupMockRoutes(page); // Mock export API page.route('**/api/v1/exports/exceptions*', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ reportId: 'report-001', downloadUrl: '/downloads/exception-report.json', }), }) ); }); test('export exception report', async ({ page }) => { await page.goto('/policy/exceptions'); await expect(page.getByRole('heading', { name: /exception/i })).toBeVisible({ timeout: 10000, }); // Find export button const exportButton = page.getByRole('button', { name: /export/i }); await expect(exportButton).toBeVisible(); await exportButton.click(); // Export dialog or confirmation await expect(page.getByText(/export|download/i)).toBeVisible(); // Verify download initiated (check for link or success message) const downloadLink = page.getByRole('link', { name: /download/i }); await expect(downloadLink).toBeVisible({ timeout: 10000 }); }); });