Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
461
src/Web/StellaOps.Web/tests/e2e/exception-lifecycle.spec.ts
Normal file
461
src/Web/StellaOps.Web/tests/e2e/exception-lifecycle.spec.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
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('Exception Lifecycle - User Flow', () => {
|
||||
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('/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('/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('/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('Exception Lifecycle - Approval Flow', () => {
|
||||
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('/exceptions/approvals');
|
||||
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('/exceptions/approvals');
|
||||
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('/exceptions/approvals');
|
||||
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('Exception Lifecycle - Admin Flow', () => {
|
||||
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('/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('/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('/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('Exception Lifecycle - Role-Based Access', () => {
|
||||
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('/exceptions/approvals');
|
||||
|
||||
// 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('/exceptions/approvals');
|
||||
|
||||
// Should show approval queue
|
||||
await expect(page.getByRole('heading', { name: /approval queue/i })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Exception Export', () => {
|
||||
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('/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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user