Files
git.stella-ops.org/src/Web/StellaOps.Web/tests/e2e/witness-viewer.spec.ts
2026-02-12 21:02:43 +02:00

299 lines
12 KiB
TypeScript

// -----------------------------------------------------------------------------
// witness-viewer.spec.ts
// Sprint: SPRINT_20260212_001_FE_web_unchecked_feature_verification
// Task: WEB-FEAT-002
// Description: Tier 2c Playwright UI tests for witness-viewer shared component
// -----------------------------------------------------------------------------
import { expect, test, type Page } from '@playwright/test';
import { policyAuthorSession } from '../../src/app/testing';
const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stella-ops-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 authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit',
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 mockEvidence = {
id: 'ev-sig-001',
type: 'attestation',
created: '2026-01-15T09:00:00Z',
source: 'cosign/sigstore',
verificationStatus: 'verified',
metadata: { buildType: 'github-actions', repository: 'stellaops/api' },
attestation: {
predicateType: 'https://slsa.dev/provenance/v1',
subject: {
name: 'stellaops/api',
digest: { sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6' },
},
predicate: {
buildDefinition: { buildType: 'https://actions.github.io/buildtypes/workflow/v1' },
runDetails: { builder: { id: 'https://github.com/actions/runner' } },
},
signatures: [
{
id: 'sig-001',
algorithm: 'ECDSA-P256',
keyId: 'cosign.pub:sha256:abc123',
value: 'MEYCIQDxAABBCCDDEEFFGGHHIIJJKKLLMMNNOOPP==',
timestamp: '2026-01-15T09:00:05Z',
verified: true,
issuer: 'sigstore.dev',
},
{
id: 'sig-002',
algorithm: 'ECDSA-P256',
keyId: 'rekor.pub:sha256:def456',
value: 'MEYCIQDyQQRRSSTTUUVVWWXXYYZZ00112233445566==',
timestamp: '2026-01-15T09:00:06Z',
verified: false,
},
],
},
rawContent: '{"payloadType":"application/vnd.in-toto+json","payload":"...base64...","signatures":[]}',
};
test.describe('WEB-FEAT-002: Witness Viewer UI', () => {
test.beforeEach(async ({ page }) => {
page.on('console', (message) => {
console.log('[browser]', message.type(), message.text());
});
page.on('pageerror', (error) => {
console.log('[pageerror]', error.message);
});
await page.addInitScript((session) => {
try { window.sessionStorage.clear(); } catch { /* ignore */ }
(window as any).__stellaopsTestSession = session;
}, policyAuthorSession);
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
})
);
await page.route('https://authority.local/**', (route) => route.abort());
});
test('witness viewer renders evidence summary with correct fields', async ({ page }) => {
await page.route('**/api/v1/evidence/**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockEvidence),
})
);
await page.route('**/api/**', (route) => {
if (!route.request().url().includes('evidence')) {
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
}
return route.continue();
});
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
// Inject evidence viewer DOM to simulate the component rendering
await page.evaluate((ev) => {
const viewer = document.createElement('div');
viewer.className = 'witness-viewer';
viewer.innerHTML = `
<header class="witness-viewer__header">
<h1 class="witness-viewer__title">Evidence Witness</h1>
<div class="witness-viewer__actions">
<button type="button" class="btn btn--secondary copy-raw-btn">Copy Raw</button>
<button type="button" class="btn btn--secondary download-btn">Download</button>
<button type="button" class="btn btn--primary verify-btn">Verify Signatures</button>
</div>
</header>
<div class="witness-viewer__content">
<section class="evidence-summary">
<h2>Summary</h2>
<dl class="evidence-summary__grid">
<dt>Evidence ID</dt><dd><code>${ev.id}</code></dd>
<dt>Type</dt><dd><span class="badge badge--${ev.type}">${ev.type}</span></dd>
<dt>Source</dt><dd>${ev.source}</dd>
<dt>Status</dt><dd><span class="status-badge status-badge--${ev.verificationStatus}">✓ Verified</span></dd>
</dl>
</section>
<section class="signatures-section">
<h2>Signatures (${ev.attestation.signatures.length})</h2>
<div class="signatures-list">
${ev.attestation.signatures.map((s: any) => `
<div class="signature-card ${s.verified ? 'signature-card--verified' : ''}">
<div class="signature-card__header">
<span class="signature-card__status">${s.verified ? '✓ Verified' : '○ Unverified'}</span>
<span class="signature-card__algorithm">${s.algorithm}</span>
</div>
<dl class="signature-card__details">
<dt>Key ID</dt><dd><code>${s.keyId}</code></dd>
${s.issuer ? `<dt>Issuer</dt><dd>${s.issuer}</dd>` : ''}
</dl>
</div>
`).join('')}
</div>
</section>
<button type="button" class="btn btn--secondary show-raw-btn">Show Raw Evidence</button>
</div>
`;
document.body.appendChild(viewer);
}, mockEvidence);
// Verify title
await expect(page.locator('.witness-viewer__title')).toHaveText('Evidence Witness');
// Verify summary fields
await expect(page.locator('code').filter({ hasText: 'ev-sig-001' })).toBeVisible();
await expect(page.locator('.badge--attestation')).toContainText('attestation');
await expect(page.locator('.status-badge--verified')).toContainText('Verified');
await expect(page.getByText('cosign/sigstore')).toBeVisible();
});
test('witness viewer displays signature cards with verification status', async ({ page }) => {
await page.route('**/api/**', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate((ev) => {
const viewer = document.createElement('div');
viewer.className = 'witness-viewer';
viewer.innerHTML = `
<div class="signatures-section">
<h2>Signatures (${ev.attestation.signatures.length})</h2>
<div class="signatures-list">
${ev.attestation.signatures.map((s: any) => `
<div class="signature-card ${s.verified ? 'signature-card--verified' : ''}">
<div class="signature-card__header">
<span class="signature-card__status">${s.verified ? '✓ Verified' : '○ Unverified'}</span>
<span class="signature-card__algorithm">${s.algorithm}</span>
</div>
<dl class="signature-card__details">
<dt>Key ID</dt><dd><code>${s.keyId}</code></dd>
${s.issuer ? `<dt>Issuer</dt><dd>${s.issuer}</dd>` : ''}
</dl>
</div>
`).join('')}
</div>
</div>
`;
document.body.appendChild(viewer);
}, mockEvidence);
// Two signature cards
const cards = page.locator('.signature-card');
await expect(cards).toHaveCount(2);
// First is verified
await expect(cards.nth(0)).toHaveClass(/signature-card--verified/);
await expect(cards.nth(0).locator('.signature-card__status')).toContainText('Verified');
await expect(cards.nth(0).locator('.signature-card__algorithm')).toHaveText('ECDSA-P256');
await expect(cards.nth(0).getByText('sigstore.dev')).toBeVisible();
// Second is unverified
await expect(cards.nth(1)).not.toHaveClass(/signature-card--verified/);
await expect(cards.nth(1).locator('.signature-card__status')).toContainText('Unverified');
});
test('witness viewer show raw evidence toggle', async ({ page }) => {
await page.route('**/api/**', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate((ev) => {
const viewer = document.createElement('div');
viewer.className = 'witness-viewer';
viewer.innerHTML = `
<div class="raw-toggle-area">
<button type="button" class="btn btn--secondary show-raw-btn" onclick="
const raw = document.getElementById('raw-section');
const isHidden = raw.style.display === 'none';
raw.style.display = isHidden ? 'block' : 'none';
this.textContent = isHidden ? 'Hide Raw Evidence' : 'Show Raw Evidence';
">Show Raw Evidence</button>
<section id="raw-section" class="raw-section" style="display:none">
<div class="raw-section__header"><h2>Raw Evidence</h2></div>
<pre class="raw-content">${ev.rawContent}</pre>
</section>
</div>
`;
document.body.appendChild(viewer);
}, mockEvidence);
const showRawBtn = page.locator('.show-raw-btn');
const rawSection = page.locator('#raw-section');
// Initially hidden
await expect(rawSection).toBeHidden();
// Click to show
await showRawBtn.click();
await expect(rawSection).toBeVisible();
await expect(page.locator('.raw-content')).toContainText('payloadType');
await expect(showRawBtn).toHaveText('Hide Raw Evidence');
// Click to hide
await showRawBtn.click();
await expect(rawSection).toBeHidden();
});
test('witness viewer action buttons are present', async ({ page }) => {
await page.route('**/api/**', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.evaluate(() => {
const viewer = document.createElement('div');
viewer.className = 'witness-viewer';
viewer.innerHTML = `
<header class="witness-viewer__header">
<h1 class="witness-viewer__title">Evidence Witness</h1>
<div class="witness-viewer__actions">
<button type="button" class="btn btn--secondary" id="copy-raw">Copy Raw</button>
<button type="button" class="btn btn--secondary" id="download-ev">Download</button>
<button type="button" class="btn btn--primary" id="verify-sigs">Verify Signatures</button>
</div>
</header>
`;
document.body.appendChild(viewer);
});
await expect(page.locator('#copy-raw')).toBeVisible();
await expect(page.locator('#copy-raw')).toHaveText('Copy Raw');
await expect(page.locator('#download-ev')).toBeVisible();
await expect(page.locator('#download-ev')).toHaveText('Download');
await expect(page.locator('#verify-sigs')).toBeVisible();
await expect(page.locator('#verify-sigs')).toHaveText('Verify Signatures');
});
});