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