Files
kaidi/frontend/e2e/system-update.e2e.ts
T
2026-08-17 23:03:03 +08:00

229 lines
9.3 KiB
TypeScript

import type { Page, Route } from '@playwright/test';
import { expect, test } from '@playwright/test';
const permissions = ['admin:user:view', 'admin:user:create', 'admin:update:view', 'admin:update:execute'];
function envelope(data: unknown) {
return { data, requestId: '01M00000000000000000000090' };
}
function session() {
return {
authenticated: true,
user: {
publicId: '01M00000000000000000000001',
username: 'admin',
displayName: '系统管理员',
departmentName: '信息中心',
mustChangePassword: false,
},
roles: [{ code: 'SYSTEM_ADMIN', name: '系统管理员', workbenchRoute: '/governance/settings' }],
activeRole: 'SYSTEM_ADMIN',
permissions,
};
}
function updateView(state: string, checked: boolean, enabled = true, recoveryPending = false) {
const busy = [
'DOWNLOAD_QUEUED',
'INSTALL_QUEUED',
'VERIFYING',
'DOWNLOADING',
'BACKING_UP',
'INSTALLING',
'RUNNING',
].includes(state);
return {
enabled,
currentVersion: '1.0.0-preview.1',
latestVersion: checked ? '1.0.0-preview.2' : null,
updateAvailable: checked && !['CURRENT', 'SUCCEEDED'].includes(state),
state,
message: !enabled
? '在线更新未配置'
: recoveryPending
? '回滚未完成,更新服务将重试'
: state === 'DOWNLOAD_QUEUED'
? '下载请求已排队'
: state === 'INSTALL_QUEUED'
? '安装请求已排队'
: state === 'READY'
? '更新包已下载并通过签名校验'
: state === 'SUCCEEDED'
? '新版本已通过健康检查'
: checked
? '发现新版本'
: '尚未执行在线更新',
releaseNotes: checked ? 'Preview update' : null,
publishedAt: checked ? '2026-08-16T00:00:00Z' : null,
checkedAt: checked ? '2026-08-16T00:01:00Z' : null,
statusUpdatedAt: null,
allowedActions:
!enabled || recoveryPending || busy
? []
: state === 'READY'
? ['CHECK', 'INSTALL']
: checked
? ['CHECK', 'DOWNLOAD']
: ['CHECK'],
};
}
async function installFixture(
page: Page,
downloadBodies: unknown[],
installBodies: unknown[],
enabled = true,
recoveryPending = false,
completeAfterInstall = false,
initialState?: string,
completeBusyAfterFirstRead = false,
) {
let checked = recoveryPending || Boolean(initialState);
let state = recoveryPending ? 'FAILED' : initialState || 'IDLE';
const history: Array<Record<string, unknown>> = [];
const recordHistory = (actionCode: string, targetVersion = '1.0.0-preview.2') => {
history.unshift({
publicId: `01M00000000000000000000${String(history.length + 1).padStart(3, '0')}`,
eventSequence: history.length + 1,
requestId: '01M00000000000000000000092',
username: 'admin',
activeRole: 'SYSTEM_ADMIN',
actionCode,
objectType: 'SYSTEM_UPDATE',
objectPublicId: 'SYSTEM_UPDATE',
resultCode: 'SUCCESS',
reason: actionCode === 'SYSTEM_UPDATE_REQUEST' ? '管理员确认立即重启' : null,
beforeJson: null,
afterJson: JSON.stringify({ targetVersion }),
occurredAt: '2026-08-16T00:02:00Z',
allowedActions: [],
});
};
await page.route('**/api/v1/**', async (route: Route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(session()) });
if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(session().user) });
if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(session().roles) });
if (pathname === '/api/v1/auth/csrf') {
return route.fulfill({
headers: { 'set-cookie': 'XSRF-TOKEN=update-csrf; Path=/; SameSite=Lax' },
json: envelope({ token: 'update-csrf' }),
});
}
if (pathname === '/api/v1/admin/users') {
return route.fulfill({
json: {
data: [],
meta: { page: 1, size: 20, totalElements: 0, totalPages: 0 },
requestId: '01M00000000000000000000091',
},
});
}
if (pathname === '/api/v1/admin/system-update' && request.method() === 'GET') {
const response = updateView(state, checked, enabled, recoveryPending);
if (completeBusyAfterFirstRead && state === 'INSTALLING') state = 'SUCCEEDED';
return route.fulfill({ json: envelope(response) });
}
if (pathname === '/api/v1/admin/system-update/check' && request.method() === 'POST') {
checked = true;
recordHistory('SYSTEM_UPDATE_CHECK');
return route.fulfill({ json: envelope(updateView(state, checked, enabled, recoveryPending)) });
}
if (pathname === '/api/v1/admin/system-update/download' && request.method() === 'POST') {
downloadBodies.push(request.postDataJSON());
recordHistory('SYSTEM_UPDATE_DOWNLOAD_REQUEST');
state = 'READY';
return route.fulfill({ json: envelope(updateView('DOWNLOAD_QUEUED', true, enabled)) });
}
if (pathname === '/api/v1/admin/system-update/install' && request.method() === 'POST') {
installBodies.push(request.postDataJSON());
recordHistory('SYSTEM_UPDATE_REQUEST');
const queued = updateView('INSTALL_QUEUED', true, enabled);
state = completeAfterInstall ? 'SUCCEEDED' : 'INSTALL_QUEUED';
return route.fulfill({ json: envelope(queued) });
}
if (pathname === '/api/v1/audit/logs' && request.method() === 'GET') {
return route.fulfill({
json: {
data: history,
meta: { page: 1, size: 20, totalElements: history.length, totalPages: history.length ? 1 : 0 },
requestId: '01M00000000000000000000093',
},
});
}
if (request.method() === 'GET') return route.fulfill({ json: envelope([]) });
return route.fulfill({ status: 204 });
});
}
test('system administrator checks and queues a signed online update', async ({ page }) => {
const downloadBodies: unknown[] = [];
const installBodies: unknown[] = [];
await installFixture(page, downloadBodies, installBodies);
await page.goto('/governance/update');
await expect(page.locator('h1.page-title-sr-only')).toHaveText('系统更新');
await expect(page.locator('.version-item').filter({ hasText: '当前版本' })).toContainText('1.0.0-preview.1');
await page.getByRole('button', { name: '获取更新', exact: true }).click();
await expect(page.locator('.version-item').filter({ hasText: '最新版本' })).toContainText('1.0.0-preview.2');
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toBeVisible({ timeout: 5_000 });
expect(downloadBodies).toEqual([{ version: '1.0.0-preview.2' }]);
await expect(page.getByText('下载更新包', { exact: true })).toBeVisible();
await page.getByRole('button', { name: '立即重启', exact: true }).click();
await expect(page.getByText('安装请求已排队', { exact: true })).toBeVisible();
expect(installBodies).toEqual([{ version: '1.0.0-preview.2', reason: '管理员确认立即重启' }]);
});
test('brand logo returns the active identity to its workbench', async ({ page }) => {
await installFixture(page, [], []);
await page.goto('/governance/update');
await page.locator('.brand-logo').click();
await expect(page).toHaveURL(/\/governance\/settings$/);
await expect(page.getByText('选择工作身份', { exact: true })).toHaveCount(0);
});
test('disabled online updates expose status without an executable check action', async ({ page }) => {
await installFixture(page, [], [], false);
await page.goto('/governance/update');
await expect(page.getByText('在线更新未配置', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '获取更新', exact: true })).toBeDisabled();
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toHaveCount(0);
});
test('rollback recovery keeps the failure visible without an install action', async ({ page }) => {
await installFixture(page, [], [], true, true);
await page.goto('/governance/update');
await expect(page.getByText('回滚未完成,更新服务将重试', { exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '立即重启', exact: true })).toHaveCount(0);
});
test('successful installation starts the ten-second automatic refresh countdown', async ({ page }) => {
await installFixture(page, [], [], true, false, true);
await page.goto('/governance/update');
await page.getByRole('button', { name: '获取更新', exact: true }).click();
await page.getByRole('button', { name: '立即重启', exact: true }).click({ timeout: 5_000 });
await expect(page.getByText('新版本已通过健康检查,页面将在 10 秒后自动刷新。', { exact: true })).toBeVisible({
timeout: 5_000,
});
});
test('reloading during installation resumes polling and starts the refresh countdown', async ({ page }) => {
await installFixture(page, [], [], true, false, false, 'INSTALLING', true);
await page.goto('/governance/update');
await expect(page.getByText('安装中', { exact: true })).toBeVisible();
await expect(page.getByText('新版本已通过健康检查,页面将在 10 秒后自动刷新。', { exact: true })).toBeVisible({
timeout: 6_000,
});
});