test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -0,0 +1,762 @@
// -----------------------------------------------------------------------------
// doctor-registry.spec.ts
// Sprint: SPRINT_0127_001_0002_oci_registry_compatibility
// Tasks: REG-UI-01
// Description: E2E tests for Doctor Registry UI components
// -----------------------------------------------------------------------------
import { expect, test, type Page } from '@playwright/test';
/**
* E2E Tests for Doctor Registry UI Components
* Task REG-UI-01: Registry health card, capability matrix, check details
*/
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 doctor:read',
audience: 'https://doctor.local',
},
apiBaseUrls: {
authority: 'https://authority.local',
doctor: 'https://doctor.local',
},
quickstartMode: true,
};
// Mock Doctor report with registry check results
const mockDoctorReport = {
runId: 'run-registry-001',
status: 'completed',
startedAt: '2026-01-27T10:00:00Z',
completedAt: '2026-01-27T10:01:30Z',
durationMs: 90000,
summary: {
passed: 4,
info: 1,
warnings: 2,
failed: 1,
skipped: 0,
total: 8,
},
overallSeverity: 'fail',
results: [
// Harbor Registry - healthy
{
checkId: 'integration.registry.v2-endpoint',
pluginId: 'integration.registry',
category: 'integration',
severity: 'pass',
diagnosis: 'V2 endpoint accessible and responding correctly',
evidence: {
description: 'Registry V2 API probe results',
data: {
registry_url: 'https://harbor.example.com',
registry_name: 'Harbor Production',
status_code: '200',
response_time_ms: '45',
server_header: 'Harbor',
},
},
durationMs: 150,
executedAt: '2026-01-27T10:00:05Z',
},
{
checkId: 'integration.registry.auth-config',
pluginId: 'integration.registry',
category: 'integration',
severity: 'pass',
diagnosis: 'Authentication configured correctly',
evidence: {
description: 'Authentication validation results',
data: {
registry_url: 'https://harbor.example.com',
auth_method: 'bearer',
token_valid: 'true',
},
},
durationMs: 85,
executedAt: '2026-01-27T10:00:10Z',
},
{
checkId: 'integration.registry.referrers-api',
pluginId: 'integration.registry',
category: 'integration',
severity: 'pass',
diagnosis: 'OCI 1.1 Referrers API fully supported',
evidence: {
description: 'Referrers API probe results',
data: {
registry_url: 'https://harbor.example.com',
referrers_supported: 'true',
api_version: 'OCI 1.1',
},
},
durationMs: 200,
executedAt: '2026-01-27T10:00:15Z',
},
// Generic OCI Registry - degraded (no referrers API)
{
checkId: 'integration.registry.v2-endpoint',
pluginId: 'integration.registry',
category: 'integration',
severity: 'pass',
diagnosis: 'V2 endpoint accessible',
evidence: {
description: 'Registry V2 API probe results',
data: {
registry_url: 'https://registry.example.com',
registry_name: 'Generic OCI Registry',
status_code: '200',
response_time_ms: '120',
},
},
durationMs: 180,
executedAt: '2026-01-27T10:00:30Z',
},
{
checkId: 'integration.registry.referrers-api',
pluginId: 'integration.registry',
category: 'integration',
severity: 'warn',
diagnosis: 'Referrers API not supported, using tag-based fallback',
evidence: {
description: 'Referrers API probe results',
data: {
registry_url: 'https://registry.example.com',
referrers_supported: 'false',
fallback_required: 'true',
http_status: '404',
},
},
likelyCauses: [
'Registry does not support OCI Distribution Spec 1.1',
'Referrers API endpoint not implemented',
],
remediation: {
requiresBackup: false,
steps: [
{
order: 1,
description: 'Upgrade registry to a version supporting OCI 1.1',
command: 'helm upgrade registry oci-registry --version 2.0.0',
commandType: 'shell',
},
],
},
durationMs: 250,
executedAt: '2026-01-27T10:00:45Z',
},
// Broken Registry - unhealthy
{
checkId: 'integration.registry.v2-endpoint',
pluginId: 'integration.registry',
category: 'integration',
severity: 'fail',
diagnosis: 'V2 endpoint unreachable - connection refused',
evidence: {
description: 'Registry V2 API probe results',
data: {
registry_url: 'https://broken.example.com',
registry_name: 'Broken Registry',
error: 'Connection refused',
error_code: 'ECONNREFUSED',
},
},
likelyCauses: [
'Registry service is not running',
'Firewall blocking connection',
'Incorrect registry URL',
],
remediation: {
requiresBackup: false,
steps: [
{
order: 1,
description: 'Verify registry service is running',
command: 'docker ps | grep registry',
commandType: 'shell',
},
{
order: 2,
description: 'Check firewall rules',
command: 'iptables -L -n | grep 5000',
commandType: 'shell',
},
],
},
durationMs: 3000,
executedAt: '2026-01-27T10:01:00Z',
},
// Capability check - info severity
{
checkId: 'integration.registry.capabilities',
pluginId: 'integration.registry',
category: 'integration',
severity: 'info',
diagnosis: 'Registry capability matrix generated',
evidence: {
description: 'OCI capability probe results',
data: {
registry_url: 'https://harbor.example.com',
supports_chunked_upload: 'true',
supports_cross_repo_mount: 'true',
supports_manifest_delete: 'true',
supports_blob_delete: 'true',
capability_score: '6/7',
},
},
durationMs: 500,
executedAt: '2026-01-27T10:01:15Z',
},
// TLS certificate warning
{
checkId: 'integration.registry.tls-cert',
pluginId: 'integration.registry',
category: 'integration',
severity: 'warn',
diagnosis: 'TLS certificate expires in 14 days',
evidence: {
description: 'TLS certificate validation results',
data: {
registry_url: 'https://registry.example.com',
expires_at: '2026-02-10T00:00:00Z',
days_remaining: '14',
issuer: "Let's Encrypt",
},
},
likelyCauses: ['Certificate renewal not configured', 'Certbot job failed'],
remediation: {
requiresBackup: false,
steps: [
{
order: 1,
description: 'Renew certificate',
command: 'certbot renew --quiet',
commandType: 'shell',
},
],
},
durationMs: 100,
executedAt: '2026-01-27T10:01:20Z',
},
],
};
const mockPlugins = {
plugins: [
{
pluginId: 'integration.registry',
displayName: 'Registry Integration',
category: 'integration',
version: '1.0.0',
checkCount: 5,
},
],
total: 1,
};
const mockChecks = {
checks: [
{
checkId: 'integration.registry.v2-endpoint',
name: 'V2 Endpoint Check',
description: 'Verify OCI registry V2 API endpoint accessibility',
pluginId: 'integration.registry',
category: 'integration',
defaultSeverity: 'fail',
tags: ['registry', 'oci', 'connectivity'],
estimatedDurationMs: 5000,
},
{
checkId: 'integration.registry.auth-config',
name: 'Authentication Config',
description: 'Validate registry authentication configuration',
pluginId: 'integration.registry',
category: 'integration',
defaultSeverity: 'fail',
tags: ['registry', 'oci', 'auth'],
estimatedDurationMs: 3000,
},
{
checkId: 'integration.registry.referrers-api',
name: 'Referrers API Support',
description: 'Detect OCI 1.1 Referrers API support',
pluginId: 'integration.registry',
category: 'integration',
defaultSeverity: 'warn',
tags: ['registry', 'oci', 'referrers', 'oci-1.1'],
estimatedDurationMs: 5000,
},
{
checkId: 'integration.registry.capabilities',
name: 'Capability Probe',
description: 'Probe registry OCI capabilities',
pluginId: 'integration.registry',
category: 'integration',
defaultSeverity: 'info',
tags: ['registry', 'oci', 'capabilities'],
estimatedDurationMs: 10000,
},
{
checkId: 'integration.registry.tls-cert',
name: 'TLS Certificate',
description: 'Validate TLS certificate validity',
pluginId: 'integration.registry',
category: 'integration',
defaultSeverity: 'warn',
tags: ['registry', 'tls', 'security'],
estimatedDurationMs: 2000,
},
],
total: 5,
};
test.describe('REG-UI-01: Doctor Registry Health Card', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
await setupDoctorMocks(page);
});
test('registry health panel displays after doctor run', async ({ page }) => {
await page.goto('/doctor');
// Wait for doctor page to load
await expect(page.getByRole('heading', { name: /doctor/i })).toBeVisible({ timeout: 10000 });
// Registry health section should be visible after results load
const registrySection = page.locator('text=/registry.*health|configured.*registries/i');
if ((await registrySection.count()) > 0) {
await expect(registrySection.first()).toBeVisible({ timeout: 10000 });
}
});
test('registry cards show health indicators', async ({ page }) => {
await page.goto('/doctor');
// Look for health status indicators (healthy/degraded/unhealthy)
const healthIndicators = page.locator(
'text=/healthy|degraded|unhealthy|pass|warn|fail/i'
);
if ((await healthIndicators.count()) > 0) {
await expect(healthIndicators.first()).toBeVisible({ timeout: 10000 });
}
});
test('registry cards display registry names', async ({ page }) => {
await page.goto('/doctor');
// Check for registry names from mock data
const harborRegistry = page.getByText(/harbor.*production|harbor\.example\.com/i);
if ((await harborRegistry.count()) > 0) {
await expect(harborRegistry.first()).toBeVisible({ timeout: 10000 });
}
});
test('clicking registry card shows details', async ({ page }) => {
await page.goto('/doctor');
// Find and click a registry card
const registryCard = page.locator('[class*="registry-card"], [class*="health-card"]').first();
if (await registryCard.isVisible({ timeout: 5000 }).catch(() => false)) {
await registryCard.click();
// Details panel should appear
const detailsPanel = page.locator(
'[class*="details"], [class*="check-details"], [class*="registry-details"]'
);
if ((await detailsPanel.count()) > 0) {
await expect(detailsPanel.first()).toBeVisible({ timeout: 5000 });
}
}
});
});
test.describe('REG-UI-01: Doctor Registry Capability Matrix', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
await setupDoctorMocks(page);
});
test('capability matrix displays after doctor run', async ({ page }) => {
await page.goto('/doctor');
// Look for capability matrix
const capabilityMatrix = page.locator(
'text=/capability.*matrix|oci.*capabilities/i, [class*="capability-matrix"]'
);
if ((await capabilityMatrix.count()) > 0) {
await expect(capabilityMatrix.first()).toBeVisible({ timeout: 10000 });
}
});
test('capability matrix shows OCI features', async ({ page }) => {
await page.goto('/doctor');
// Check for OCI capability names
const ociFeatures = [
/v2.*endpoint|v2.*api/i,
/referrers.*api|referrers/i,
/chunked.*upload/i,
/manifest.*delete/i,
];
for (const feature of ociFeatures) {
const featureElement = page.locator(`text=${feature.source}`);
if ((await featureElement.count()) > 0) {
// At least one OCI feature should be visible
break;
}
}
});
test('capability matrix shows supported/unsupported indicators', async ({ page }) => {
await page.goto('/doctor');
// Look for checkmark/x indicators or supported/unsupported text
const indicators = page.locator(
'text=/supported|unsupported|partial|✓|✗|yes|no/i'
);
if ((await indicators.count()) > 0) {
await expect(indicators.first()).toBeVisible({ timeout: 10000 });
}
});
test('capability rows are expandable', async ({ page }) => {
await page.goto('/doctor');
// Find expandable capability row
const expandableRow = page.locator(
'[class*="capability-row"], [class*="expandable"], tr[class*="capability"]'
).first();
if (await expandableRow.isVisible({ timeout: 5000 }).catch(() => false)) {
await expandableRow.click();
// Description should appear
const description = page.locator('[class*="description"], [class*="expanded"]');
if ((await description.count()) > 0) {
await expect(description.first()).toBeVisible({ timeout: 3000 });
}
}
});
});
test.describe('REG-UI-01: Doctor Registry Check Details', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
await setupDoctorMocks(page);
});
test('check results display for registry checks', async ({ page }) => {
await page.goto('/doctor');
// Look for check results
const checkResults = page.locator(
'[class*="check-result"], [class*="check-item"], text=/integration\.registry/i'
);
if ((await checkResults.count()) > 0) {
await expect(checkResults.first()).toBeVisible({ timeout: 10000 });
}
});
test('check results show severity indicators', async ({ page }) => {
await page.goto('/doctor');
// Look for severity badges/icons
const severityIndicators = page.locator(
'[class*="severity"], [class*="pass"], [class*="warn"], [class*="fail"]'
);
if ((await severityIndicators.count()) > 0) {
await expect(severityIndicators.first()).toBeVisible({ timeout: 10000 });
}
});
test('expanding check shows evidence', async ({ page }) => {
await page.goto('/doctor');
// Find and click a check result
const checkResult = page.locator(
'[class*="check-result"], [class*="check-item"]'
).first();
if (await checkResult.isVisible({ timeout: 5000 }).catch(() => false)) {
await checkResult.click();
// Evidence section should appear
const evidence = page.locator(
'[class*="evidence"], text=/evidence|registry_url|status_code/i'
);
if ((await evidence.count()) > 0) {
await expect(evidence.first()).toBeVisible({ timeout: 5000 });
}
}
});
test('failed checks show remediation steps', async ({ page }) => {
await page.goto('/doctor');
// Look for failed check
const failedCheck = page.locator('[class*="fail"], [class*="severity-fail"]').first();
if (await failedCheck.isVisible({ timeout: 5000 }).catch(() => false)) {
await failedCheck.click();
// Remediation should be visible
const remediation = page.locator(
'text=/remediation|steps|command|verify|check/i'
);
if ((await remediation.count()) > 0) {
await expect(remediation.first()).toBeVisible({ timeout: 5000 });
}
}
});
test('evidence displays key-value pairs', async ({ page }) => {
await page.goto('/doctor');
// Find and expand a check
const checkResult = page.locator('[class*="check-result"], [class*="check-item"]').first();
if (await checkResult.isVisible({ timeout: 5000 }).catch(() => false)) {
await checkResult.click();
// Evidence data should show key-value pairs
const evidenceKeys = ['registry_url', 'status_code', 'response_time'];
for (const key of evidenceKeys) {
const keyElement = page.locator(`text=/${key}/i`);
if ((await keyElement.count()) > 0) {
// At least one evidence key should be present
break;
}
}
}
});
});
test.describe('REG-UI-01: Doctor Registry Integration', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
await setupDoctorMocks(page);
});
test('running doctor shows registry checks in progress', async ({ page }) => {
// Mock SSE for progress updates
await page.route('**/api/doctor/runs/*/progress*', (route) =>
route.fulfill({
status: 200,
contentType: 'text/event-stream',
body: `data: {"eventType":"check-started","checkId":"integration.registry.v2-endpoint","completed":0,"total":5}\n\n`,
})
);
await page.goto('/doctor');
// Click run button if visible
const runButton = page.getByRole('button', { name: /run|check|quick|normal|full/i });
if (await runButton.first().isVisible({ timeout: 5000 }).catch(() => false)) {
// Don't actually run - just verify button exists
await expect(runButton.first()).toBeEnabled();
}
});
test('registry filter shows only registry checks', async ({ page }) => {
await page.goto('/doctor');
// Look for category filter
const categoryFilter = page.locator(
'select[id*="category"], [class*="filter"] select, [class*="category-filter"]'
);
if (await categoryFilter.isVisible({ timeout: 5000 }).catch(() => false)) {
// Select integration category
await categoryFilter.selectOption({ label: /integration/i });
// Should filter to registry checks
const registryChecks = page.locator('text=/integration\.registry/i');
if ((await registryChecks.count()) > 0) {
await expect(registryChecks.first()).toBeVisible({ timeout: 5000 });
}
}
});
test('severity filter highlights failed registry checks', async ({ page }) => {
await page.goto('/doctor');
// Look for severity filter
const failFilter = page.locator(
'input[type="checkbox"][id*="fail"], label:has-text("fail") input, [class*="severity-fail"] input'
);
if (await failFilter.first().isVisible({ timeout: 5000 }).catch(() => false)) {
await failFilter.first().check();
// Should show only failed checks
const failedChecks = page.locator('[class*="severity-fail"], [class*="fail"]');
if ((await failedChecks.count()) > 0) {
await expect(failedChecks.first()).toBeVisible({ timeout: 5000 });
}
}
});
test('health summary shows correct counts', async ({ page }) => {
await page.goto('/doctor');
// Look for health summary counts
const summarySection = page.locator('[class*="summary"], [class*="health-summary"]');
if (await summarySection.isVisible({ timeout: 5000 }).catch(() => false)) {
// Should show counts from mock data
// 1 healthy (Harbor), 1 degraded (Generic OCI), 1 unhealthy (Broken)
const healthyCount = page.locator('text=/healthy.*[0-9]|[0-9].*healthy/i');
const unhealthyCount = page.locator('text=/unhealthy.*[0-9]|[0-9].*unhealthy/i');
if ((await healthyCount.count()) > 0) {
await expect(healthyCount.first()).toBeVisible();
}
if ((await unhealthyCount.count()) > 0) {
await expect(unhealthyCount.first()).toBeVisible();
}
}
});
});
// Helper functions
async function setupBasicMocks(page: Page) {
page.on('console', (message) => {
if (message.type() === 'error') {
console.log('[browser:error]', message.text());
}
});
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')) {
return route.abort();
}
return route.fulfill({ status: 400, body: 'blocked' });
});
}
async function setupAuthenticatedSession(page: Page) {
const mockToken = {
access_token: 'mock-doctor-access-token',
id_token: 'mock-id-token',
token_type: 'Bearer',
expires_in: 3600,
scope: 'openid profile email doctor:read',
};
await page.addInitScript((tokenData) => {
(window as any).__stellaopsTestSession = {
isAuthenticated: true,
accessToken: tokenData.access_token,
idToken: tokenData.id_token,
expiresAt: Date.now() + tokenData.expires_in * 1000,
};
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);
}
async function setupDoctorMocks(page: Page) {
// Mock Doctor plugins list
await page.route('**/api/doctor/plugins*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPlugins),
})
);
// Mock Doctor checks list
await page.route('**/api/doctor/checks*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockChecks),
})
);
// Mock start run
await page.route('**/api/doctor/runs', (route) => {
if (route.request().method() === 'POST') {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ runId: mockDoctorReport.runId }),
});
}
return route.continue();
});
// Mock get run result
await page.route('**/api/doctor/runs/*', (route) => {
if (route.request().method() === 'GET') {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockDoctorReport),
});
}
return route.continue();
});
// Mock latest report endpoint
await page.route('**/api/doctor/reports/latest*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockDoctorReport),
})
);
// Mock dashboard data if Doctor is on dashboard
await page.route('**/api/dashboard*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
doctor: {
lastRun: mockDoctorReport.completedAt,
summary: mockDoctorReport.summary,
},
}),
})
);
}