This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
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('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,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user