Concelier: - Register Topology.Read, Topology.Manage, Topology.Admin authorization policies mapped to OrchRead/OrchOperate/PlatformContextRead/IntegrationWrite scopes. Previously these policies were referenced by endpoints but never registered, causing System.InvalidOperationException on every topology API call. Gateway routes: - Simplified targets/environments routes (removed specific sub-path routes, use catch-all patterns instead) - Changed environments base route to JobEngine (where CRUD lives) - Changed to ReverseProxy type for all topology routes KNOWN ISSUE (not yet fixed): - ReverseProxy routes don't forward the gateway's identity envelope to Concelier. The regions/targets/bindings endpoints return 401 because hasPrincipal=False — the gateway authenticates the user but doesn't pass the identity to the backend via ReverseProxy. Microservice routes use Valkey transport which includes envelope headers. Topology endpoints need either: (a) Valkey transport registration in Concelier, or (b) Concelier configured to accept raw bearer tokens on ReverseProxy paths. This is an architecture-level fix. Journey findings collected so far: - Integration wizard (Harbor + GitHub App): works end-to-end - Advisory Check All: fixed (parallel individual checks) - Mirror domain creation: works, generate-immediately fails silently - Topology wizard Step 1 (Region): blocked by auth passthrough issue - Topology wizard Step 2 (Environment): POST to JobEngine needs verify - User ID resolution: raw hashes shown everywhere Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
896 lines
34 KiB
TypeScript
896 lines
34 KiB
TypeScript
/**
|
|
* Mirror Client Setup Wizard — E2E Tests
|
|
*
|
|
* Verifies the 4-step wizard for configuring Stella Ops as a mirror consumer,
|
|
* including connection testing, domain discovery, signature auto-detection,
|
|
* mode selection, pre-flight checks, and integration with the dashboard and catalog.
|
|
*
|
|
* Sprint: SPRINT_20260315_008_Concelier_mirror_client_setup_wizard (MCS-005)
|
|
*/
|
|
|
|
import { test, expect } from './fixtures/auth.fixture';
|
|
import { navigateAndWait } from './helpers/nav.helper';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock API responses for deterministic E2E
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const MOCK_MIRROR_TEST_SUCCESS = {
|
|
reachable: true,
|
|
statusCode: 200,
|
|
message: 'OK',
|
|
};
|
|
|
|
const MOCK_MIRROR_TEST_FAILURE = {
|
|
reachable: false,
|
|
statusCode: 0,
|
|
message: 'Connection refused',
|
|
};
|
|
|
|
const MOCK_DISCOVERY_RESPONSE = {
|
|
domains: [
|
|
{
|
|
domainId: 'security-advisories',
|
|
displayName: 'Security Advisories',
|
|
lastGenerated: '2026-03-14T10:00:00Z',
|
|
advisoryCount: 4500,
|
|
bundleSize: 12_400_000,
|
|
exportFormats: ['csaf', 'osv'],
|
|
signed: true,
|
|
},
|
|
{
|
|
domainId: 'vendor-bulletins',
|
|
displayName: 'Vendor Bulletins',
|
|
lastGenerated: '2026-03-13T18:30:00Z',
|
|
advisoryCount: 1200,
|
|
bundleSize: 3_800_000,
|
|
exportFormats: ['csaf'],
|
|
signed: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
const MOCK_SIGNATURE_DETECTION = {
|
|
detected: true,
|
|
algorithm: 'ES256',
|
|
keyId: 'key-01',
|
|
provider: 'built-in',
|
|
};
|
|
|
|
const MOCK_CONSUMER_CONFIG = {
|
|
baseAddress: 'https://mirror.stella-ops.org',
|
|
domainId: 'security-advisories',
|
|
indexPath: '/concelier/exports/index.json',
|
|
httpTimeoutSeconds: 30,
|
|
signature: {
|
|
enabled: true,
|
|
algorithm: 'ES256',
|
|
keyId: 'key-01',
|
|
publicKeyPem: null,
|
|
},
|
|
connected: true,
|
|
lastSync: '2026-03-15T08:00:00Z',
|
|
};
|
|
|
|
const MOCK_MIRROR_CONFIG_MIRROR_MODE = {
|
|
mode: 'Mirror',
|
|
consumerMirrorUrl: 'https://mirror.stella-ops.org',
|
|
consumerConnected: true,
|
|
lastConsumerSync: '2026-03-15T08:00:00Z',
|
|
};
|
|
|
|
const MOCK_MIRROR_CONFIG_DIRECT_MODE = {
|
|
mode: 'Direct',
|
|
consumerMirrorUrl: null,
|
|
consumerConnected: false,
|
|
lastConsumerSync: null,
|
|
};
|
|
|
|
const MOCK_MIRROR_HEALTH = {
|
|
totalDomains: 2,
|
|
freshCount: 1,
|
|
staleCount: 1,
|
|
neverGeneratedCount: 0,
|
|
totalAdvisoryCount: 5700,
|
|
};
|
|
|
|
const MOCK_DOMAIN_LIST = {
|
|
domains: [
|
|
{
|
|
id: 'dom-001',
|
|
domainId: 'security-advisories',
|
|
displayName: 'Security Advisories',
|
|
sourceIds: ['nvd', 'osv', 'ghsa'],
|
|
exportFormat: 'csaf',
|
|
rateLimits: { indexRequestsPerHour: 60, downloadRequestsPerHour: 120 },
|
|
requireAuthentication: false,
|
|
signing: { enabled: true, algorithm: 'ES256', keyId: 'key-01' },
|
|
domainUrl: '/concelier/exports/mirror/security-advisories',
|
|
createdAt: new Date().toISOString(),
|
|
status: 'active',
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
};
|
|
|
|
const MOCK_SOURCE_CATALOG = {
|
|
items: [
|
|
{ id: 'nvd', displayName: 'NVD', category: 'Primary', type: 'Upstream', description: 'NIST National Vulnerability Database', baseEndpoint: 'https://services.nvd.nist.gov', requiresAuth: true, credentialEnvVar: 'NVD_API_KEY', credentialUrl: null, documentationUrl: null, defaultPriority: 10, regions: [], tags: ['cve'], enabledByDefault: true },
|
|
{ id: 'osv', displayName: 'OSV', category: 'Primary', type: 'Upstream', description: 'Open Source Vulnerabilities', baseEndpoint: 'https://api.osv.dev', requiresAuth: false, credentialEnvVar: null, credentialUrl: null, documentationUrl: null, defaultPriority: 20, regions: [], tags: ['ecosystem'], enabledByDefault: true },
|
|
{ id: 'stella-mirror', displayName: 'StellaOps Mirror', category: 'Mirror', type: 'Mirror', description: 'Local offline mirror', baseEndpoint: 'http://mirror.local', requiresAuth: false, credentialEnvVar: null, credentialUrl: null, documentationUrl: null, defaultPriority: 100, regions: [], tags: ['mirror'], enabledByDefault: false },
|
|
],
|
|
totalCount: 3,
|
|
};
|
|
|
|
const MOCK_SOURCE_STATUS = {
|
|
sources: MOCK_SOURCE_CATALOG.items.map((s) => ({
|
|
sourceId: s.id,
|
|
enabled: s.enabledByDefault,
|
|
lastCheck: s.enabledByDefault
|
|
? { sourceId: s.id, status: 'Healthy', checkedAt: new Date().toISOString(), latency: '00:00:00.1500000', isHealthy: true, possibleReasons: [], remediationSteps: [] }
|
|
: null,
|
|
})),
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared mock setup helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function setupErrorCollector(page: import('@playwright/test').Page) {
|
|
const errors: string[] = [];
|
|
page.on('console', (msg) => {
|
|
const text = msg.text();
|
|
if (msg.type() === 'error' && /NG0\d{3,4}/.test(text)) {
|
|
errors.push(text);
|
|
}
|
|
});
|
|
return errors;
|
|
}
|
|
|
|
/** Set up mocks for the mirror client setup wizard page. */
|
|
function setupWizardApiMocks(page: import('@playwright/test').Page) {
|
|
// Mirror test endpoint (connection check)
|
|
page.route('**/api/v1/advisory-sources/mirror/test', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_TEST_SUCCESS),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Consumer discovery endpoint
|
|
page.route('**/api/v1/advisory-sources/mirror/consumer/discover', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_DISCOVERY_RESPONSE),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Consumer signature verification endpoint
|
|
page.route('**/api/v1/advisory-sources/mirror/consumer/verify-signature', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_SIGNATURE_DETECTION),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Consumer config GET/PUT
|
|
page.route('**/api/v1/advisory-sources/mirror/consumer', (route) => {
|
|
const method = route.request().method();
|
|
if (method === 'GET') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_CONSUMER_CONFIG),
|
|
});
|
|
} else if (method === 'PUT') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ ...MOCK_CONSUMER_CONFIG, connected: true }),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Mirror config
|
|
page.route('**/api/v1/advisory-sources/mirror/config', (route) => {
|
|
const method = route.request().method();
|
|
if (method === 'GET') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_CONFIG_DIRECT_MODE),
|
|
});
|
|
} else if (method === 'PUT') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_CONFIG_MIRROR_MODE),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Mirror health summary
|
|
page.route('**/api/v1/advisory-sources/mirror/health', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_HEALTH),
|
|
});
|
|
});
|
|
|
|
// Mirror domains
|
|
page.route('**/api/v1/advisory-sources/mirror/domains', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_DOMAIN_LIST),
|
|
});
|
|
});
|
|
|
|
// Mirror import endpoint
|
|
page.route('**/api/v1/advisory-sources/mirror/import', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ success: true, exportsImported: 3, totalSize: 15_000_000, errors: [], warnings: [] }),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Mirror import status
|
|
page.route('**/api/v1/advisory-sources/mirror/import/status', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ status: 'idle', lastResult: null }),
|
|
});
|
|
});
|
|
|
|
// Catch-all for integration/security endpoints
|
|
page.route('**/api/v2/security/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], table: [] }) });
|
|
});
|
|
|
|
page.route('**/api/v2/integrations/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
|
|
page.route('**/api/v1/integrations/providers', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Set up mocks for catalog and dashboard pages that show mirror integration. */
|
|
function setupCatalogDashboardMocks(page: import('@playwright/test').Page) {
|
|
page.route('**/api/v1/advisory-sources/catalog', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SOURCE_CATALOG) });
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/status', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SOURCE_STATUS) });
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/check', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ totalChecked: 3, healthyCount: 2, failedCount: 0 }) });
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/*/check', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ sourceId: 'nvd', status: 'Healthy', checkedAt: new Date().toISOString(), latency: '00:00:00.0850000', isHealthy: true, possibleReasons: [], remediationSteps: [] }),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/*/check-result', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ sourceId: 'nvd', status: 'Healthy', checkedAt: new Date().toISOString(), latency: '00:00:00.1000000', isHealthy: true, possibleReasons: [], remediationSteps: [] }),
|
|
});
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/batch-enable', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ results: [] }) });
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/batch-disable', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ results: [] }) });
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources?*', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], totalCount: 0, dataAsOf: new Date().toISOString() }) });
|
|
});
|
|
|
|
page.route('**/api/v1/advisory-sources/summary', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ totalSources: 2, healthySources: 2, warningSources: 0, staleSources: 0, unavailableSources: 0, disabledSources: 1, conflictingSources: 0, dataAsOf: new Date().toISOString() }) });
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Mirror Client Setup Wizard
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe('Mirror Client Setup Wizard', () => {
|
|
test('renders wizard with 4 steps and step indicators', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// Verify the wizard page loaded with relevant content
|
|
expect(body.toLowerCase()).toContain('mirror');
|
|
|
|
// Verify step indicators exist — the wizard has 4 steps
|
|
// Step 1: Connect, Step 2: Signature, Step 3: Schedule/Mode, Step 4: Review
|
|
const stepIndicators = page.locator('[class*="step"], [class*="wizard-step"], [data-step]');
|
|
const stepCount = await stepIndicators.count();
|
|
|
|
// If step indicators are rendered as distinct elements, expect at least 4
|
|
// Otherwise verify step labels in the page text
|
|
if (stepCount >= 4) {
|
|
expect(stepCount).toBeGreaterThanOrEqual(4);
|
|
} else {
|
|
// Check for step-related text (wizard should show step descriptions)
|
|
const hasStepContent =
|
|
body.toLowerCase().includes('connect') ||
|
|
body.toLowerCase().includes('step 1') ||
|
|
body.toLowerCase().includes('connection');
|
|
expect(hasStepContent).toBeTruthy();
|
|
}
|
|
|
|
// Verify initial state shows step 1 content (connection form)
|
|
const hasConnectionContent =
|
|
body.toLowerCase().includes('base address') ||
|
|
body.toLowerCase().includes('mirror url') ||
|
|
body.toLowerCase().includes('connect to mirror') ||
|
|
body.toLowerCase().includes('test connection');
|
|
expect(hasConnectionContent).toBeTruthy();
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('connection test shows success feedback with green indicator', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Fill the base address input
|
|
const addressInput = page.locator('input[type="url"], input[type="text"]').filter({ hasText: /mirror/i }).first();
|
|
const urlInput = addressInput.isVisible({ timeout: 3000 }).catch(() => false)
|
|
? addressInput
|
|
: page.locator('input[placeholder*="mirror"], input[placeholder*="http"], input[placeholder*="url" i]').first();
|
|
|
|
if (await urlInput.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await urlInput.fill('https://mirror.stella-ops.org');
|
|
} else {
|
|
// Fallback: fill any visible text input that could be the URL field
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const count = await inputs.count();
|
|
for (let i = 0; i < count; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://mirror.stella-ops.org');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Click "Test Connection" button
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Verify success indicator appears
|
|
const body = await page.locator('body').innerText();
|
|
const hasSuccess =
|
|
body.toLowerCase().includes('ok') ||
|
|
body.toLowerCase().includes('success') ||
|
|
body.toLowerCase().includes('reachable') ||
|
|
body.toLowerCase().includes('connected');
|
|
expect(hasSuccess).toBeTruthy();
|
|
|
|
// Check for green success visual indicator
|
|
const successIndicator = page.locator(
|
|
'[class*="success"], [class*="green"], [class*="check"], [class*="status--connected"]'
|
|
);
|
|
const indicatorCount = await successIndicator.count();
|
|
expect(indicatorCount).toBeGreaterThan(0);
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('connection test shows failure with error message and remediation hint', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
|
|
// Override the mirror test endpoint to return failure
|
|
await page.route('**/api/v1/advisory-sources/mirror/test', (route) => {
|
|
if (route.request().method() === 'POST') {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_TEST_FAILURE),
|
|
});
|
|
} else {
|
|
route.continue();
|
|
}
|
|
});
|
|
|
|
// Set up remaining wizard mocks (excluding mirror/test which is overridden above)
|
|
await page.route('**/api/v1/advisory-sources/mirror/consumer/discover', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DISCOVERY_RESPONSE) });
|
|
});
|
|
await page.route('**/api/v1/advisory-sources/mirror/consumer/verify-signature', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SIGNATURE_DETECTION) });
|
|
});
|
|
await page.route('**/api/v1/advisory-sources/mirror/consumer', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_CONSUMER_CONFIG) });
|
|
});
|
|
await page.route('**/api/v1/advisory-sources/mirror/config', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MIRROR_CONFIG_DIRECT_MODE) });
|
|
});
|
|
await page.route('**/api/v1/advisory-sources/mirror/health', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MIRROR_HEALTH) });
|
|
});
|
|
await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DOMAIN_LIST) });
|
|
});
|
|
await page.route('**/api/v2/security/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
await page.route('**/api/v2/integrations/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
await page.route('**/api/v1/integrations/providers', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) });
|
|
});
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Fill base address
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const inputCount = await inputs.count();
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://unreachable-mirror.example.com');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Click "Test Connection"
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Verify error message appears
|
|
const body = await page.locator('body').innerText();
|
|
const hasError =
|
|
body.toLowerCase().includes('connection refused') ||
|
|
body.toLowerCase().includes('failed') ||
|
|
body.toLowerCase().includes('unreachable') ||
|
|
body.toLowerCase().includes('error');
|
|
expect(hasError).toBeTruthy();
|
|
|
|
// Check for error/red visual indicator
|
|
const errorIndicator = page.locator(
|
|
'[class*="error"], [class*="fail"], [class*="red"], [class*="status--disconnected"], [class*="danger"]'
|
|
);
|
|
const indicatorCount = await errorIndicator.count();
|
|
expect(indicatorCount).toBeGreaterThan(0);
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('domain discovery populates dropdown with domains and advisory counts', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Fill base address and trigger connection test to enable discovery
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const inputCount = await inputs.count();
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://mirror.stella-ops.org');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Trigger connection test (which should auto-trigger discovery on success)
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(3000);
|
|
}
|
|
|
|
// Verify domain dropdown/selector is populated
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// The discovery response contains 2 domains
|
|
const hasDomain1 = body.includes('Security Advisories') || body.includes('security-advisories');
|
|
const hasDomain2 = body.includes('Vendor Bulletins') || body.includes('vendor-bulletins');
|
|
|
|
expect(hasDomain1 || hasDomain2).toBeTruthy();
|
|
|
|
// Verify advisory counts are shown
|
|
const hasAdvisoryCount = body.includes('4500') || body.includes('4,500') || body.includes('1200') || body.includes('1,200');
|
|
if (hasDomain1 || hasDomain2) {
|
|
// If domains rendered, advisory counts should also be visible
|
|
expect(hasAdvisoryCount || hasDomain1).toBeTruthy();
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('signature auto-detection pre-populates algorithm and key ID fields', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Complete step 1: fill address and test connection
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const inputCount = await inputs.count();
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://mirror.stella-ops.org');
|
|
break;
|
|
}
|
|
}
|
|
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// Navigate to step 2 (signature verification)
|
|
const nextBtn = page.locator('button').filter({ hasText: /next|continue|step 2|signature/i }).first();
|
|
if (await nextBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// Verify signature auto-detection results are shown
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// The mock returns algorithm: 'ES256' and keyId: 'key-01'
|
|
const hasAlgorithm = body.includes('ES256');
|
|
const hasKeyId = body.includes('key-01');
|
|
|
|
// At least the algorithm or key ID should be visible after auto-detection
|
|
expect(hasAlgorithm || hasKeyId).toBeTruthy();
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('mode selection Mirror vs Hybrid shows appropriate warnings', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Navigate through to step 3 (sync schedule & mode)
|
|
// Complete step 1
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const inputCount = await inputs.count();
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://mirror.stella-ops.org');
|
|
break;
|
|
}
|
|
}
|
|
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// Navigate forward (step 2 then step 3)
|
|
for (let step = 0; step < 2; step++) {
|
|
const nextBtn = page.locator('button').filter({ hasText: /next|continue/i }).first();
|
|
if (await nextBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
}
|
|
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// Verify mode selection options are present
|
|
const hasMirrorOption = body.toLowerCase().includes('mirror');
|
|
const hasHybridOption = body.toLowerCase().includes('hybrid');
|
|
expect(hasMirrorOption).toBeTruthy();
|
|
|
|
// Select Mirror mode and check for warning about disabling direct sources
|
|
const mirrorRadio = page.locator('input[type="radio"][value="Mirror"], input[type="radio"][value="mirror"], label').filter({ hasText: /^mirror$/i }).first();
|
|
if (await mirrorRadio.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await mirrorRadio.click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
const bodyAfterMirror = await page.locator('body').innerText();
|
|
const hasWarning =
|
|
bodyAfterMirror.toLowerCase().includes('disable') ||
|
|
bodyAfterMirror.toLowerCase().includes('direct sources') ||
|
|
bodyAfterMirror.toLowerCase().includes('warning') ||
|
|
bodyAfterMirror.toLowerCase().includes('supersede');
|
|
expect(hasWarning).toBeTruthy();
|
|
}
|
|
|
|
// Select Hybrid mode and verify no such warning
|
|
const hybridRadio = page.locator('input[type="radio"][value="Hybrid"], input[type="radio"][value="hybrid"], label').filter({ hasText: /hybrid/i }).first();
|
|
if (await hybridRadio.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await hybridRadio.click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// In hybrid mode, the disable-direct-sources warning should not be prominent
|
|
// (the page should still mention hybrid but not warn about disabling)
|
|
const bodyAfterHybrid = await page.locator('body').innerText();
|
|
const hasHybridContent = bodyAfterHybrid.toLowerCase().includes('hybrid');
|
|
expect(hasHybridContent || hasHybridOption).toBeTruthy();
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('pre-flight checks all pass with green checkmarks on review step', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupWizardApiMocks(page);
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror/client-setup', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Navigate through all steps to reach step 4 (review)
|
|
// Step 1: fill address and test
|
|
const inputs = page.locator('input[type="text"], input[type="url"]');
|
|
const inputCount = await inputs.count();
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const input = inputs.nth(i);
|
|
if (await input.isVisible().catch(() => false)) {
|
|
await input.fill('https://mirror.stella-ops.org');
|
|
break;
|
|
}
|
|
}
|
|
|
|
const testBtn = page.locator('button').filter({ hasText: /test connection/i }).first();
|
|
if (await testBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await testBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
// Navigate through steps 2, 3, to step 4
|
|
for (let step = 0; step < 3; step++) {
|
|
const nextBtn = page.locator('button').filter({ hasText: /next|continue|review/i }).first();
|
|
if (await nextBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(2000);
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// Verify review step shows summary and pre-flight checks
|
|
const hasReviewContent =
|
|
body.toLowerCase().includes('review') ||
|
|
body.toLowerCase().includes('summary') ||
|
|
body.toLowerCase().includes('pre-flight') ||
|
|
body.toLowerCase().includes('activate');
|
|
expect(hasReviewContent).toBeTruthy();
|
|
|
|
// Verify pre-flight checks show pass status
|
|
// Checks: mirror reachable, domain exists, signature valid
|
|
const hasPassIndicators =
|
|
body.toLowerCase().includes('reachable') ||
|
|
body.toLowerCase().includes('pass') ||
|
|
body.toLowerCase().includes('valid') ||
|
|
body.toLowerCase().includes('ready');
|
|
|
|
// Check for green checkmark elements
|
|
const greenChecks = page.locator(
|
|
'[class*="check"], [class*="pass"], [class*="success"], [class*="green"], [class*="valid"]'
|
|
);
|
|
const checkCount = await greenChecks.count();
|
|
|
|
expect(hasPassIndicators || checkCount > 0).toBeTruthy();
|
|
|
|
// Verify "Activate" button exists on review step
|
|
const activateBtn = page.locator('button').filter({ hasText: /activate|confirm|save/i }).first();
|
|
if (await activateBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await expect(activateBtn).toBeVisible();
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Dashboard integration — Configure button in consumer panel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe('Mirror Dashboard - Consumer Panel', () => {
|
|
test('shows Configure button in consumer panel when mode is Mirror', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
|
|
// Mock mirror config as Mirror mode with consumer URL
|
|
await page.route('**/api/v1/advisory-sources/mirror/config', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_CONFIG_MIRROR_MODE),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v1/advisory-sources/mirror/health', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_HEALTH),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_DOMAIN_LIST),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v2/security/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
|
|
await page.route('**/api/v2/integrations/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
|
|
await page.route('**/api/v1/integrations/providers', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) });
|
|
});
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources/mirror', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// Verify Mirror mode badge is displayed
|
|
expect(body).toContain('Mirror');
|
|
|
|
// Verify consumer panel is visible (shows when mode is Mirror or Hybrid)
|
|
const hasConsumerPanel =
|
|
body.includes('Consumer Mirror Connection') ||
|
|
body.includes('Mirror URL') ||
|
|
body.includes('Connection Status');
|
|
expect(hasConsumerPanel).toBeTruthy();
|
|
|
|
// Verify consumer panel shows the mirror URL
|
|
expect(body).toContain('https://mirror.stella-ops.org');
|
|
|
|
// Look for a "Configure" button in the consumer panel context
|
|
const configureBtn = page.locator('button').filter({ hasText: /configure/i }).first();
|
|
if (await configureBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await expect(configureBtn).toBeVisible();
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Catalog integration — Connect to Mirror button
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe('Advisory Source Catalog - Mirror Integration', () => {
|
|
test('shows Connect to Mirror button when in Direct mode', async ({ authenticatedPage: page }) => {
|
|
const ngErrors = setupErrorCollector(page);
|
|
await setupCatalogDashboardMocks(page);
|
|
|
|
// Mock mirror config in Direct mode
|
|
await page.route('**/api/v1/advisory-sources/mirror/config', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(MOCK_MIRROR_CONFIG_DIRECT_MODE),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v1/advisory-sources/mirror/health', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ totalDomains: 0, freshCount: 0, staleCount: 0, neverGeneratedCount: 0, totalAdvisoryCount: 0 }),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ domains: [], totalCount: 0 }) });
|
|
});
|
|
|
|
await page.route('**/api/v2/security/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
|
|
await page.route('**/api/v2/integrations/**', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
|
});
|
|
|
|
await page.route('**/api/v1/integrations/providers', (route) => {
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) });
|
|
});
|
|
|
|
await navigateAndWait(page, '/ops/integrations/advisory-vex-sources', { timeout: 30_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
const body = await page.locator('body').innerText();
|
|
|
|
// Verify the catalog page loaded
|
|
expect(body).toContain('Advisory & VEX Source Catalog');
|
|
|
|
// Verify Direct mode badge is shown in mirror context header
|
|
const hasDirectBadge = body.includes('Direct');
|
|
expect(hasDirectBadge).toBeTruthy();
|
|
|
|
// Look for "Connect to Mirror" or "Configure Mirror" link/button
|
|
const connectBtn = page.locator('a, button').filter({ hasText: /connect to mirror|configure mirror/i }).first();
|
|
if (await connectBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await expect(connectBtn).toBeVisible();
|
|
}
|
|
|
|
expect(ngErrors).toHaveLength(0);
|
|
});
|
|
});
|