product advisories, stella router improval, tests streghthening
This commit is contained in:
548
src/Web/StellaOps.Web/tests/e2e/smoke.spec.ts
Normal file
548
src/Web/StellaOps.Web/tests/e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// smoke.spec.ts
|
||||
// Sprint: SPRINT_5100_0009_0011_ui_tests
|
||||
// Tasks: UI-5100-007, UI-5100-008, UI-5100-009, UI-5100-010
|
||||
// Description: E2E smoke tests for critical user journeys
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E Smoke Tests for Critical User Journeys
|
||||
* Task UI-5100-007: Login → view dashboard → success
|
||||
* Task UI-5100-008: View scan results → navigate to SBOM → success
|
||||
* Task UI-5100-009: Apply policy → view verdict → success
|
||||
* Task UI-5100-010: User without permissions → denied access → correct error message
|
||||
*/
|
||||
|
||||
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 findings:read',
|
||||
audience: 'https://scanner.local',
|
||||
},
|
||||
apiBaseUrls: {
|
||||
authority: 'https://authority.local',
|
||||
scanner: 'https://scanner.local',
|
||||
policy: 'https://policy.local',
|
||||
concelier: 'https://concelier.local',
|
||||
attestor: 'https://attestor.local',
|
||||
},
|
||||
quickstartMode: true,
|
||||
};
|
||||
|
||||
// Mock data for tests
|
||||
const mockScanResults = {
|
||||
items: [
|
||||
{
|
||||
id: 'scan-001',
|
||||
imageRef: 'stellaops/demo:v1.0.0',
|
||||
digest: 'sha256:abc123def456',
|
||||
status: 'completed',
|
||||
createdAt: '2025-12-24T10:00:00Z',
|
||||
completedAt: '2025-12-24T10:05:00Z',
|
||||
packageCount: 142,
|
||||
vulnerabilityCount: 7,
|
||||
},
|
||||
{
|
||||
id: 'scan-002',
|
||||
imageRef: 'stellaops/api:v2.0.0',
|
||||
digest: 'sha256:789xyz000',
|
||||
status: 'completed',
|
||||
createdAt: '2025-12-24T11:00:00Z',
|
||||
completedAt: '2025-12-24T11:03:00Z',
|
||||
packageCount: 89,
|
||||
vulnerabilityCount: 2,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
const mockSbom = {
|
||||
bomFormat: 'CycloneDX',
|
||||
specVersion: '1.6',
|
||||
metadata: {
|
||||
component: {
|
||||
type: 'container',
|
||||
name: 'stellaops/demo',
|
||||
version: 'v1.0.0',
|
||||
},
|
||||
},
|
||||
components: [
|
||||
{
|
||||
type: 'library',
|
||||
name: 'lodash',
|
||||
version: '4.17.21',
|
||||
purl: 'pkg:npm/lodash@4.17.21',
|
||||
},
|
||||
{
|
||||
type: 'library',
|
||||
name: 'express',
|
||||
version: '4.18.2',
|
||||
purl: 'pkg:npm/express@4.18.2',
|
||||
},
|
||||
],
|
||||
vulnerabilities: [
|
||||
{
|
||||
id: 'CVE-2024-1234',
|
||||
source: { name: 'NVD' },
|
||||
ratings: [{ severity: 'critical', score: 9.8 }],
|
||||
affects: [{ ref: 'pkg:npm/lodash@4.17.21' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockVerdict = {
|
||||
passed: true,
|
||||
policyName: 'default-policy',
|
||||
imageRef: 'stellaops/demo:v1.0.0',
|
||||
digest: 'sha256:abc123def456',
|
||||
checks: [
|
||||
{ name: 'no-critical', passed: true, message: 'No critical vulnerabilities' },
|
||||
{ name: 'sbom-complete', passed: true, message: 'SBOM is complete' },
|
||||
{ name: 'signature-valid', passed: true, message: 'Signature verified' },
|
||||
],
|
||||
failureReasons: [],
|
||||
};
|
||||
|
||||
const mockDashboard = {
|
||||
summary: {
|
||||
totalScans: 156,
|
||||
completedScans: 150,
|
||||
pendingScans: 6,
|
||||
criticalVulnerabilities: 12,
|
||||
highVulnerabilities: 45,
|
||||
totalPolicies: 8,
|
||||
activePolicies: 5,
|
||||
},
|
||||
recentScans: mockScanResults.items,
|
||||
};
|
||||
|
||||
test.describe('UI-5100-007: Login → Dashboard Smoke Test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
});
|
||||
|
||||
test('sign in button is visible on landing page', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const signInButton = page.getByRole('button', { name: /sign in/i });
|
||||
await expect(signInButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking sign in redirects to authority', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const signInButton = page.getByRole('button', { name: /sign in/i });
|
||||
await expect(signInButton).toBeVisible();
|
||||
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest('https://authority.local/connect/authorize*'),
|
||||
signInButton.click({ noWaitAfter: true }),
|
||||
]);
|
||||
|
||||
expect(request.url()).toContain('authority.local');
|
||||
expect(request.url()).toContain('authorize');
|
||||
});
|
||||
|
||||
test('authenticated user sees dashboard', async ({ page }) => {
|
||||
await setupAuthenticatedSession(page);
|
||||
await page.route('**/api/dashboard*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockDashboard),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/dashboard');
|
||||
// Dashboard elements should be visible
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI-5100-008: Scan Results → SBOM Smoke Test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
await setupAuthenticatedSession(page);
|
||||
});
|
||||
|
||||
test('scan results list displays scans', async ({ page }) => {
|
||||
await page.route('**/api/scans*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans');
|
||||
// Should show scan results
|
||||
await expect(page.getByText('stellaops/demo:v1.0.0')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('stellaops/api:v2.0.0')).toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking scan navigates to details', async ({ page }) => {
|
||||
await page.route('**/api/scans', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults.items[0]),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001/sbom*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockSbom),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans');
|
||||
await page.getByText('stellaops/demo:v1.0.0').click();
|
||||
|
||||
// Should navigate to scan details
|
||||
await expect(page).toHaveURL(/\/scans\/scan-001/);
|
||||
});
|
||||
|
||||
test('scan details shows SBOM components', async ({ page }) => {
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults.items[0]),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001/sbom*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockSbom),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// SBOM data should be visible
|
||||
await expect(
|
||||
page.getByText(/lodash|express|components/i).first()
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('vulnerability count is displayed', async ({ page }) => {
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults.items[0]),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// Should show vulnerability count (7 from mock data)
|
||||
await expect(page.getByText(/7|vulnerabilities/i).first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI-5100-009: Apply Policy → View Verdict Smoke Test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
await setupAuthenticatedSession(page);
|
||||
});
|
||||
|
||||
test('policy application triggers verification', async ({ page }) => {
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockScanResults.items[0]),
|
||||
})
|
||||
);
|
||||
|
||||
let verifyRequested = false;
|
||||
await page.route('**/api/verify*', (route) => {
|
||||
verifyRequested = true;
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockVerdict),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// Find and click verify/apply policy button if present
|
||||
const verifyButton = page.getByRole('button', { name: /verify|apply.*policy/i });
|
||||
if (await verifyButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
await verifyButton.click();
|
||||
expect(verifyRequested).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('verdict shows pass status', async ({ page }) => {
|
||||
await page.route('**/api/verify*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockVerdict),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
...mockScanResults.items[0],
|
||||
verdict: mockVerdict,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// Should show pass indicator or policy check results
|
||||
const passIndicators = page.locator('text=/pass|✓|success|compliant/i');
|
||||
if ((await passIndicators.count()) > 0) {
|
||||
await expect(passIndicators.first()).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('verdict shows check details', async ({ page }) => {
|
||||
await page.route('**/api/verify*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockVerdict),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
...mockScanResults.items[0],
|
||||
verdict: mockVerdict,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// Check details might be visible (depends on UI implementation)
|
||||
const checkNames = ['no-critical', 'sbom-complete', 'signature-valid'];
|
||||
for (const checkName of checkNames) {
|
||||
const checkElement = page.getByText(new RegExp(checkName.replace('-', '\\s*'), 'i'));
|
||||
if ((await checkElement.count()) > 0) {
|
||||
await expect(checkElement.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('failed verdict shows failure reasons', async ({ page }) => {
|
||||
const failedVerdict = {
|
||||
...mockVerdict,
|
||||
passed: false,
|
||||
checks: [
|
||||
{ name: 'no-critical', passed: false, message: '2 critical vulnerabilities found' },
|
||||
],
|
||||
failureReasons: ['Critical vulnerability CVE-2024-9999 found'],
|
||||
};
|
||||
|
||||
await page.route('**/api/verify*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(failedVerdict),
|
||||
})
|
||||
);
|
||||
await page.route('**/api/scans/scan-001*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
...mockScanResults.items[0],
|
||||
verdict: failedVerdict,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans/scan-001');
|
||||
|
||||
// Should show failure indicator
|
||||
const failIndicators = page.locator('text=/fail|✗|error|non-compliant|CVE/i');
|
||||
if ((await failIndicators.count()) > 0) {
|
||||
await expect(failIndicators.first()).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI-5100-010: Permission Denied Smoke Test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
});
|
||||
|
||||
test('unauthenticated user redirected to login', async ({ page }) => {
|
||||
// Don't set up authenticated session
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Should redirect to login or show sign in
|
||||
const signInVisible = await page
|
||||
.getByRole('button', { name: /sign in/i })
|
||||
.isVisible({ timeout: 10000 })
|
||||
.catch(() => false);
|
||||
|
||||
const redirectedToAuth = page.url().includes('auth') || page.url().includes('login');
|
||||
|
||||
expect(signInVisible || redirectedToAuth).toBe(true);
|
||||
});
|
||||
|
||||
test('unauthorized API request shows error message', async ({ page }) => {
|
||||
await setupAuthenticatedSession(page);
|
||||
|
||||
// Return 403 Forbidden
|
||||
await page.route('**/api/scans*', (route) =>
|
||||
route.fulfill({
|
||||
status: 403,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: 'Forbidden',
|
||||
message: 'You do not have permission to access this resource',
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans');
|
||||
|
||||
// Should show error message
|
||||
const errorMessages = page.locator(
|
||||
'text=/permission|forbidden|denied|unauthorized|access/i'
|
||||
);
|
||||
await expect(errorMessages.first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('insufficient scope shows appropriate error', async ({ page }) => {
|
||||
// Set up session without required scopes
|
||||
await setupAuthenticatedSession(page, { scope: 'openid profile' }); // Missing findings:read
|
||||
|
||||
await page.route('**/api/scans*', (route) =>
|
||||
route.fulfill({
|
||||
status: 403,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: 'insufficient_scope',
|
||||
message: 'Required scope: findings:read',
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans');
|
||||
|
||||
// Should show scope-related error
|
||||
const scopeError = page.locator('text=/scope|permission|access/i');
|
||||
if ((await scopeError.count()) > 0) {
|
||||
await expect(scopeError.first()).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('expired token triggers re-authentication', async ({ page }) => {
|
||||
await setupAuthenticatedSession(page);
|
||||
|
||||
// Return 401 Unauthorized
|
||||
await page.route('**/api/scans*', (route) =>
|
||||
route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: 'invalid_token',
|
||||
message: 'Token has expired',
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await page.goto('/scans');
|
||||
|
||||
// Should show login option or redirect
|
||||
await page.waitForTimeout(2000); // Give time for redirect/UI update
|
||||
|
||||
const signInVisible = await page
|
||||
.getByRole('button', { name: /sign in/i })
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const errorVisible = await page
|
||||
.locator('text=/expired|session|sign in again/i')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
expect(signInVisible || errorVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
|
||||
async function setupBasicMocks(page: Page) {
|
||||
page.on('console', (message) => {
|
||||
console.log('[browser]', message.type(), message.text());
|
||||
});
|
||||
page.on('pageerror', (error) => {
|
||||
console.log('[pageerror]', error.message);
|
||||
});
|
||||
|
||||
await page.route('**/config.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockConfig),
|
||||
})
|
||||
);
|
||||
|
||||
// Block actual auth requests
|
||||
await page.route('https://authority.local/**', (route) => {
|
||||
if (route.request().url().includes('authorize')) {
|
||||
// Let authorize requests through to verify URL construction
|
||||
return route.abort();
|
||||
}
|
||||
return route.fulfill({ status: 400, body: 'blocked' });
|
||||
});
|
||||
}
|
||||
|
||||
async function setupAuthenticatedSession(page: Page, options?: { scope?: string }) {
|
||||
const mockToken = {
|
||||
access_token: 'mock-access-token',
|
||||
id_token: 'mock-id-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
scope: options?.scope ?? 'openid profile email ui.read findings:read',
|
||||
};
|
||||
|
||||
await page.addInitScript((tokenData) => {
|
||||
// Mock authenticated session
|
||||
(window as any).__stellaopsTestSession = {
|
||||
isAuthenticated: true,
|
||||
accessToken: tokenData.access_token,
|
||||
idToken: tokenData.id_token,
|
||||
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
||||
};
|
||||
|
||||
// Override fetch to add auth header
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${tokenData.access_token}`);
|
||||
}
|
||||
return originalFetch(input, { ...init, headers });
|
||||
};
|
||||
}, mockToken);
|
||||
}
|
||||
Reference in New Issue
Block a user