Files
git.stella-ops.org/src/Web/StellaOps.Web/tests/e2e/smoke.spec.ts

549 lines
16 KiB
TypeScript

// -----------------------------------------------------------------------------
// 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);
}