Fix setup integration navigation and failure states
This commit is contained in:
@@ -158,6 +158,9 @@ const canonicalRoutes = [
|
||||
'/ops/platform-setup/defaults-guardrails',
|
||||
'/ops/platform-setup/trust-signing',
|
||||
'/setup',
|
||||
'/setup/integrations',
|
||||
'/setup/integrations/advisory-vex-sources',
|
||||
'/setup/integrations/secrets',
|
||||
'/setup/identity-access',
|
||||
'/setup/tenant-branding',
|
||||
'/setup/notifications',
|
||||
@@ -196,6 +199,18 @@ const strictRouteExpectations: Partial<Record<(typeof canonicalRoutes)[number],
|
||||
title: /Trust/i,
|
||||
texts: ['Setup', 'Trust Management'],
|
||||
},
|
||||
'/setup/integrations': {
|
||||
title: /Integrations/i,
|
||||
texts: ['Integrations', 'External system connectors'],
|
||||
},
|
||||
'/setup/integrations/advisory-vex-sources': {
|
||||
title: /Advisory & VEX Sources/i,
|
||||
texts: ['Integrations', 'FeedMirror Integrations'],
|
||||
},
|
||||
'/setup/integrations/secrets': {
|
||||
title: /Secrets/i,
|
||||
texts: ['Integrations', 'RepoSource Integrations'],
|
||||
},
|
||||
'/ops/policy': {
|
||||
title: /Policy/i,
|
||||
texts: ['Policy Governance', 'Risk Budget Overview'],
|
||||
@@ -210,6 +225,69 @@ const strictRouteExpectations: Partial<Record<(typeof canonicalRoutes)[number],
|
||||
},
|
||||
};
|
||||
|
||||
const integrationFixtures = [
|
||||
{
|
||||
integrationId: 'int-reg-1',
|
||||
tenantId: adminSession.tenant,
|
||||
name: 'Harbor Registry',
|
||||
description: 'Primary registry connector.',
|
||||
type: 1,
|
||||
provider: 101,
|
||||
status: 2,
|
||||
baseUrl: 'https://registry.example.test',
|
||||
authRef: 'auth-reg-1',
|
||||
createdAt: '2026-03-01T08:00:00.000Z',
|
||||
createdBy: 'qa-user',
|
||||
modifiedAt: '2026-03-02T08:00:00.000Z',
|
||||
modifiedBy: 'qa-user',
|
||||
lastTestedAt: '2026-03-03T08:00:00.000Z',
|
||||
lastTestSuccess: true,
|
||||
paused: false,
|
||||
consecutiveFailures: 0,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
integrationId: 'int-feed-1',
|
||||
tenantId: adminSession.tenant,
|
||||
name: 'Concelier Mirror',
|
||||
description: 'Advisory and VEX feed mirror.',
|
||||
type: 6,
|
||||
provider: 500,
|
||||
status: 2,
|
||||
baseUrl: 'https://feeds.example.test',
|
||||
authRef: 'auth-feed-1',
|
||||
createdAt: '2026-03-01T08:00:00.000Z',
|
||||
createdBy: 'qa-user',
|
||||
modifiedAt: '2026-03-02T08:00:00.000Z',
|
||||
modifiedBy: 'qa-user',
|
||||
lastTestedAt: '2026-03-03T08:00:00.000Z',
|
||||
lastTestSuccess: true,
|
||||
paused: false,
|
||||
consecutiveFailures: 0,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
integrationId: 'int-secret-1',
|
||||
tenantId: adminSession.tenant,
|
||||
name: 'Vault Secrets Broker',
|
||||
description: 'Secrets and repository source connector.',
|
||||
type: 4,
|
||||
provider: 600,
|
||||
status: 2,
|
||||
baseUrl: 'https://vault.example.test',
|
||||
authRef: 'auth-secret-1',
|
||||
createdAt: '2026-03-01T08:00:00.000Z',
|
||||
createdBy: 'qa-user',
|
||||
modifiedAt: '2026-03-02T08:00:00.000Z',
|
||||
modifiedBy: 'qa-user',
|
||||
lastTestedAt: '2026-03-03T08:00:00.000Z',
|
||||
lastTestSuccess: true,
|
||||
paused: false,
|
||||
consecutiveFailures: 0,
|
||||
version: 1,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function collectNgErrors(page: Page): string[] {
|
||||
const errors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
@@ -891,6 +969,125 @@ async function setupHarness(page: Page): Promise<void> {
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/v1/integrations**', (route) => {
|
||||
const request = route.request();
|
||||
const pathname = new URL(request.url()).pathname;
|
||||
if (!pathname.endsWith('/api/v1/integrations')) {
|
||||
return route.fallback();
|
||||
}
|
||||
|
||||
if (request.method() !== 'GET') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(integrationFixtures[0]),
|
||||
});
|
||||
}
|
||||
|
||||
const requestUrl = new URL(request.url());
|
||||
const typeFilter = requestUrl.searchParams.get('type');
|
||||
const statusFilter = requestUrl.searchParams.get('status');
|
||||
const searchFilter = requestUrl.searchParams.get('search')?.toLowerCase() ?? '';
|
||||
const pageNumber = Number(requestUrl.searchParams.get('page') ?? '1');
|
||||
const pageSize = Number(requestUrl.searchParams.get('pageSize') ?? '20');
|
||||
|
||||
const items = integrationFixtures.filter((integration) => {
|
||||
if (typeFilter && integration.type !== Number(typeFilter)) {
|
||||
return false;
|
||||
}
|
||||
if (statusFilter && integration.status !== Number(statusFilter)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
searchFilter &&
|
||||
!`${integration.name} ${integration.description ?? ''}`.toLowerCase().includes(searchFilter)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: items.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
|
||||
totalCount: items.length,
|
||||
page: pageNumber,
|
||||
pageSize,
|
||||
hasMore: pageNumber * pageSize < items.length,
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/v1/integrations/**', (route) => {
|
||||
const request = route.request();
|
||||
const pathParts = new URL(request.url()).pathname.split('/').filter(Boolean);
|
||||
const integrationsIndex = pathParts.indexOf('integrations');
|
||||
const resourceParts = pathParts.slice(integrationsIndex + 1);
|
||||
const integrationId = resourceParts[0];
|
||||
const action = resourceParts[1];
|
||||
const integration = integrationFixtures.find((candidate) => candidate.integrationId === integrationId);
|
||||
|
||||
if (!integration) {
|
||||
return route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Integration not found' }),
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'health') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
integrationId,
|
||||
status: integration.status,
|
||||
lastTestedAt: integration.lastTestedAt,
|
||||
lastTestSuccess: integration.lastTestSuccess,
|
||||
lastSyncAt: integration.modifiedAt,
|
||||
lastEventAt: integration.modifiedAt,
|
||||
consecutiveFailures: integration.consecutiveFailures,
|
||||
uptimePercentage: 99.9,
|
||||
averageLatencyMs: 42,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'test') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
testedAt: integration.lastTestedAt,
|
||||
latencyMs: 42,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method() === 'DELETE') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method() !== 'GET') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(integration),
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(integration),
|
||||
});
|
||||
});
|
||||
await page.route('**/policy/api/risk/profiles**', (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
return route.fulfill({
|
||||
@@ -1223,6 +1420,7 @@ async function setupHarness(page: Page): Promise<void> {
|
||||
requestUrl.includes('/api/v2/context/') ||
|
||||
requestUrl.includes('/api/v2/security/sbom-explorer') ||
|
||||
requestUrl.includes('/policy/api/') ||
|
||||
requestUrl.includes('/api/v1/integrations') ||
|
||||
requestUrl.includes('/api/v1/trust/') ||
|
||||
requestUrl.includes('/api/v1/audit/') ||
|
||||
requestUrl.includes('/api/v1/authority/quotas') ||
|
||||
@@ -1422,6 +1620,68 @@ test.describe('Pre-alpha key end-user interactions', () => {
|
||||
await expect(page.locator('#main-content')).toContainText('Trust Management');
|
||||
});
|
||||
|
||||
test('setup integrations advisory tile stays on canonical combined source route', async ({ page }) => {
|
||||
await page.goto('/setup/integrations', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#main-content')).toContainText('External system connectors');
|
||||
|
||||
await page.locator('#main-content a[href="/setup/integrations/advisory-vex-sources"]').first().click();
|
||||
await expect(page).toHaveURL(/\/setup\/integrations\/advisory-vex-sources$/);
|
||||
await expect(page.locator('#main-content')).toContainText('FeedMirror Integrations');
|
||||
});
|
||||
|
||||
test('setup integrations unsupported add action opens setup onboarding hub', async ({ page }) => {
|
||||
await page.goto('/setup/integrations/secrets', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#main-content')).toContainText('RepoSource Integrations');
|
||||
|
||||
const addButton = page.locator('#main-content button:has-text("+ Add Integration")').first();
|
||||
await expect(addButton).toBeVisible();
|
||||
await addButton.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/setup\/integrations\/onboarding$/);
|
||||
await expect(page.locator('#main-content')).toContainText('Container Registries');
|
||||
});
|
||||
|
||||
test('setup integration detail and back-link stay under setup routes', async ({ page }) => {
|
||||
await page.goto('/setup/integrations/int-secret-1', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page).toHaveURL(/\/setup\/integrations\/int-secret-1$/);
|
||||
await expect(page.locator('#main-content')).toContainText('Vault Secrets Broker');
|
||||
|
||||
await page.locator('#main-content a:has-text("Back to Integrations")').first().click();
|
||||
await expect(page).toHaveURL(/\/setup\/integrations$/);
|
||||
});
|
||||
|
||||
test('setup host onboarding returns to runtime-hosts list after create', async ({ page }) => {
|
||||
await page.goto('/setup/integrations/onboarding/host', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#main-content')).toContainText('Select Host Provider');
|
||||
|
||||
await page.getByRole('button', { name: /kubernetes/i }).click();
|
||||
await page.getByRole('button', { name: /^next$/i }).click();
|
||||
await page.locator('label[for="auth-offline"]').click();
|
||||
await page.getByRole('button', { name: /^next$/i }).click();
|
||||
await page.locator('#namespaces').fill('production');
|
||||
await page.getByRole('button', { name: /^next$/i }).click();
|
||||
await page.getByRole('button', { name: /^next$/i }).click();
|
||||
await page.getByRole('button', { name: /^next$/i }).click();
|
||||
await page.locator('#name').fill('QA Host');
|
||||
await page.getByRole('button', { name: /create integration/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/setup\/integrations\/runtime-hosts$/);
|
||||
await expect(page.locator('#main-content')).toContainText('RuntimeHost Integrations');
|
||||
await expect(page.locator('#main-content')).not.toContainText('Loading integration details...');
|
||||
});
|
||||
|
||||
test('setup integration detail 404 renders an explicit error state', async ({ page }) => {
|
||||
await page.goto('/setup/integrations/activity', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#main-content')).toContainText('Integration Activity');
|
||||
|
||||
await page.locator('#main-content .integration-link').first().click();
|
||||
|
||||
await expect(page).toHaveURL(/\/setup\/integrations\/int-1$/);
|
||||
await expect(page.locator('#main-content')).toContainText('Integration unavailable');
|
||||
await expect(page.locator('#main-content')).toContainText('Integration not found.');
|
||||
await expect(page.locator('#main-content')).not.toContainText('Loading integration details...');
|
||||
});
|
||||
|
||||
test('sidebar root navigation works for all canonical workspaces', async ({ page }) => {
|
||||
await page.goto('/mission-control/board', { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('aside.sidebar a[href="/releases/overview"]').first().click();
|
||||
|
||||
Reference in New Issue
Block a user