277 lines
9.2 KiB
TypeScript
277 lines
9.2 KiB
TypeScript
import { expect, test, type Page, type Route } from '@playwright/test';
|
|
|
|
import type { StubAuthSession } from '../../src/app/testing/auth-fixtures';
|
|
|
|
const operatorSession: StubAuthSession = {
|
|
subjectId: 'release-promotions-e2e-user',
|
|
tenant: 'tenant-default',
|
|
scopes: [
|
|
'admin',
|
|
'ui.read',
|
|
'release:read',
|
|
'release:write',
|
|
'release:publish',
|
|
'orch:read',
|
|
'orch:operate',
|
|
'policy:read',
|
|
'policy:review',
|
|
],
|
|
};
|
|
|
|
const mockConfig = {
|
|
authority: {
|
|
issuer: '/authority',
|
|
clientId: 'stella-ops-ui',
|
|
authorizeEndpoint: '/authority/connect/authorize',
|
|
tokenEndpoint: '/authority/connect/token',
|
|
logoutEndpoint: '/authority/connect/logout',
|
|
redirectUri: 'https://127.0.0.1:4400/auth/callback',
|
|
postLogoutRedirectUri: 'https://127.0.0.1:4400/',
|
|
scope: 'openid profile email ui.read',
|
|
audience: '/gateway',
|
|
dpopAlgorithms: ['ES256'],
|
|
refreshLeewaySeconds: 60,
|
|
},
|
|
apiBaseUrls: {
|
|
authority: '/authority',
|
|
scanner: '/scanner',
|
|
policy: '/policy',
|
|
concelier: '/concelier',
|
|
attestor: '/attestor',
|
|
gateway: '/gateway',
|
|
},
|
|
quickstartMode: true,
|
|
setup: 'complete',
|
|
};
|
|
|
|
const approvalSummary = {
|
|
approvalId: 'apr-001',
|
|
releaseId: 'rel-001',
|
|
releaseName: 'API Gateway',
|
|
releaseVersion: '2.1.0',
|
|
sourceEnvironment: 'stage',
|
|
targetEnvironment: 'production',
|
|
requestedBy: 'alice',
|
|
requestedAt: '2026-03-08T08:30:00Z',
|
|
urgency: 'normal',
|
|
justification: 'Promote the verified API Gateway release to production.',
|
|
status: 'pending',
|
|
currentApprovals: 0,
|
|
requiredApprovals: 2,
|
|
blockers: [],
|
|
};
|
|
|
|
const approvalDetail = {
|
|
...approvalSummary,
|
|
gateResults: [
|
|
{
|
|
gateId: 'gate-policy',
|
|
gateName: 'Policy',
|
|
type: 'policy',
|
|
status: 'passed',
|
|
message: 'Policy checks passed.',
|
|
evaluatedAt: '2026-03-08T08:35:00Z',
|
|
},
|
|
],
|
|
actions: [],
|
|
approvers: [],
|
|
releaseComponents: [{ name: 'api-gateway', version: '2.1.0', digest: 'sha256:api-gateway' }],
|
|
};
|
|
|
|
const createdApproval = {
|
|
id: 'apr-new',
|
|
releaseId: 'rel-001',
|
|
releaseName: 'API Gateway',
|
|
releaseVersion: '2.1.0',
|
|
sourceEnvironment: 'stage',
|
|
targetEnvironment: 'production',
|
|
requestedBy: 'release-promotions-e2e-user',
|
|
requestedAt: '2026-03-08T09:45:00Z',
|
|
urgency: 'normal',
|
|
justification: 'Promote the API Gateway release to production after decisioning review.',
|
|
status: 'pending',
|
|
currentApprovals: 0,
|
|
requiredApprovals: 2,
|
|
gatesPassed: true,
|
|
scheduledTime: null,
|
|
expiresAt: '2026-03-10T09:45:00Z',
|
|
};
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200): Promise<void> {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function setupHarness(page: Page): Promise<void> {
|
|
await page.addInitScript((session) => {
|
|
(window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session;
|
|
}, operatorSession);
|
|
|
|
await page.route('**/api/**', (route) => fulfillJson(route, {}));
|
|
await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig));
|
|
await page.route('**/platform/i18n/*.json', (route) => fulfillJson(route, {}));
|
|
await page.route('**/config.json', (route) => fulfillJson(route, mockConfig));
|
|
await page.route('**/.well-known/openid-configuration', (route) =>
|
|
fulfillJson(route, {
|
|
issuer: 'https://127.0.0.1:4400/authority',
|
|
authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize',
|
|
token_endpoint: 'https://127.0.0.1:4400/authority/connect/token',
|
|
jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json',
|
|
response_types_supported: ['code'],
|
|
subject_types_supported: ['public'],
|
|
id_token_signing_alg_values_supported: ['RS256'],
|
|
}),
|
|
);
|
|
await page.route('**/authority/.well-known/jwks.json', (route) => fulfillJson(route, { keys: [] }));
|
|
await page.route('**/console/branding**', (route) =>
|
|
fulfillJson(route, {
|
|
tenantId: operatorSession.tenant,
|
|
appName: 'Stella Ops',
|
|
logoUrl: null,
|
|
cssVariables: {},
|
|
}),
|
|
);
|
|
await page.route('**/console/profile**', (route) =>
|
|
fulfillJson(route, {
|
|
subjectId: operatorSession.subjectId,
|
|
username: 'release-promotions-e2e',
|
|
displayName: 'Release Promotions E2E',
|
|
tenant: operatorSession.tenant,
|
|
roles: ['release-operator'],
|
|
scopes: operatorSession.scopes,
|
|
}),
|
|
);
|
|
await page.route('**/console/token/introspect**', (route) =>
|
|
fulfillJson(route, {
|
|
active: true,
|
|
tenant: operatorSession.tenant,
|
|
subject: operatorSession.subjectId,
|
|
scopes: operatorSession.scopes,
|
|
}),
|
|
);
|
|
await page.route('**/authority/console/tenants**', (route) =>
|
|
fulfillJson(route, {
|
|
tenants: [
|
|
{
|
|
tenantId: operatorSession.tenant,
|
|
displayName: 'Default Tenant',
|
|
isDefault: true,
|
|
isActive: true,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
await page.route('**/api/v2/context/regions**', (route) =>
|
|
fulfillJson(route, [{ regionId: 'eu-west', displayName: 'EU West', sortOrder: 1, enabled: true }]),
|
|
);
|
|
await page.route('**/api/v2/context/environments**', (route) =>
|
|
fulfillJson(route, [
|
|
{
|
|
environmentId: 'production',
|
|
regionId: 'eu-west',
|
|
environmentType: 'prod',
|
|
displayName: 'Production',
|
|
sortOrder: 1,
|
|
enabled: true,
|
|
},
|
|
]),
|
|
);
|
|
await page.route('**/api/v2/context/preferences**', (route) =>
|
|
fulfillJson(route, {
|
|
tenantId: operatorSession.tenant,
|
|
actorId: operatorSession.subjectId,
|
|
regions: ['eu-west'],
|
|
environments: ['production'],
|
|
timeWindow: '24h',
|
|
stage: 'all',
|
|
updatedAt: '2026-03-08T07:00:00Z',
|
|
updatedBy: operatorSession.subjectId,
|
|
}),
|
|
);
|
|
await page.route(/\/api\/v2\/releases\/approvals(?:\?.*)?$/, (route) => fulfillJson(route, [approvalSummary]));
|
|
await page.route(/\/api\/v1\/approvals\/apr-001$/, (route) => fulfillJson(route, approvalDetail));
|
|
await page.route(/\/api\/v1\/approvals\/apr-new$/, (route) =>
|
|
fulfillJson(route, {
|
|
...approvalDetail,
|
|
...createdApproval,
|
|
gateResults: approvalDetail.gateResults,
|
|
}),
|
|
);
|
|
await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/available-environments$/, (route) =>
|
|
fulfillJson(route, [
|
|
{ id: 'env-stage', name: 'Stage', tier: 'staging' },
|
|
{ id: 'env-production', name: 'Production', tier: 'production' },
|
|
]),
|
|
);
|
|
await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/promotion-preview(?:\?.*)?$/, (route) =>
|
|
fulfillJson(route, {
|
|
releaseId: 'rel-001',
|
|
releaseName: 'API Gateway',
|
|
sourceEnvironment: 'stage',
|
|
targetEnvironment: 'production',
|
|
gateResults: [
|
|
{
|
|
gateId: 'gate-policy',
|
|
gateName: 'Policy',
|
|
type: 'policy',
|
|
status: 'passed',
|
|
message: 'Policy checks passed.',
|
|
evaluatedAt: '2026-03-08T09:40:00Z',
|
|
},
|
|
],
|
|
allGatesPassed: true,
|
|
requiredApprovers: 2,
|
|
estimatedDeployTime: 180,
|
|
warnings: [],
|
|
}),
|
|
);
|
|
await page.route(/\/api\/v1\/release-orchestrator\/releases\/rel-001\/promote$/, async (route) =>
|
|
fulfillJson(route, createdApproval, 201),
|
|
);
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupHarness(page);
|
|
});
|
|
|
|
test('release overview surfaces the canonical promotions page', async ({ page }) => {
|
|
await page.goto('/releases/overview', { waitUntil: 'networkidle' });
|
|
await page.locator('.overview').getByRole('link', { name: 'Promotions' }).click();
|
|
|
|
await expect(page).toHaveURL(/\/releases\/promotions(?:\?.*)?$/);
|
|
await expect(page.getByRole('heading', { name: 'Promotions' })).toBeVisible();
|
|
await expect(page.getByRole('link', { name: 'Create Promotion' })).toBeVisible();
|
|
});
|
|
|
|
test('legacy promotions create alias lands on the canonical wizard and submits a promotion request', async ({ page }) => {
|
|
await page.goto(
|
|
'/release-control/promotions/create?releaseId=rel-001&returnTo=%2Freleases%2Fruns%2Frun-001%2Fgate-decision',
|
|
{ waitUntil: 'networkidle' },
|
|
);
|
|
|
|
await expect(page).toHaveURL(/\/releases\/promotions\/create\?releaseId=rel-001/);
|
|
await expect(page.getByLabel('Release context handoff')).toContainText('rel-001');
|
|
await expect(page.getByRole('heading', { name: 'Select Region and Environment Path' })).toBeVisible();
|
|
|
|
await page.locator('#target-env').selectOption('env-production');
|
|
await expect(page.getByRole('heading', { name: 'Gate Preview' })).toBeVisible();
|
|
await expect(page.getByText('All gates passed')).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: 'Next ->' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Approval Context' })).toBeVisible();
|
|
await page.getByLabel('Justification').fill(
|
|
'Promote the API Gateway release to production after decisioning review.',
|
|
);
|
|
|
|
await page.getByRole('button', { name: 'Next ->' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Launch Promotion' })).toBeVisible();
|
|
await page.getByRole('button', { name: 'Submit Promotion Request' }).click();
|
|
|
|
await expect(page).toHaveURL(/\/releases\/promotions\/apr-new(?:\?.*)?$/);
|
|
await expect(page.getByRole('heading', { name: 'API Gateway' })).toBeVisible();
|
|
await expect(page.getByText('pending', { exact: true })).toBeVisible();
|
|
});
|