vdi-cloud/console/e2e/administrators.spec.ts

113 lines
8.4 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
// Isolated browser fixtures; no live administrator passwords or roles are modified.
async function fixture(page: Page, role = 'super_admin') {
const calls: { method: string; path: string; body: Record<string, unknown> }[] = []
const me = { id: 'self-admin', username: 'root', displayName: '平台管理员', role, enabled: true }
const accounts = [me, { id: 'other-admin', username: 'operations', displayName: '运维人员', role: 'operator', enabled: true }]
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'admin-fixture'))
await page.route('**/api/v1/**', async route => {
const r = route.request(), path = new URL(r.url()).pathname.replace('/api/v1', ''), method = r.method()
const list = (items: unknown[]) => route.fulfill({ json: { items, page: 1, pageSize: 100, total: items.length, requestId: 'admin-fixture' } })
if (method !== 'GET') {
const body = r.postData() ? r.postDataJSON() as Record<string, unknown> : {}; calls.push({ method, path, body })
if (path === '/administrators') {
if (accounts.some(a => a.username === body.username)) { await route.fulfill({ status: 409, json: { code: 'account_conflict', message: '管理员账号已存在', requestId: 'fixture' } }); return }
const a = { id: 'new-admin', username: String(body.username), displayName: String(body.displayName), role: String(body.role), enabled: true };accounts.push(a)
await route.fulfill({ status: 201, json: a }); return
}
if (path.startsWith('/administrators/') && method === 'PUT') { const a = accounts.find(a => path.endsWith(a.id)); if (a) Object.assign(a, body);await route.fulfill({ json: a }); return }
if (path.endsWith('/password') && path.startsWith('/administrators/')) { await route.fulfill({ status: 204 }); return }
if (path === '/auth/password') {
if (body.currentPassword !== 'Correct-old-password') { await route.fulfill({ status: 400, json: { code: 'current_password_invalid', message: '原密码不正确', requestId: 'fixture' } }); return }
await route.fulfill({ status: 204 }); return
}
}
if (path === '/auth/me') { await route.fulfill({ json: me }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, computeProvider: 'proxmox', terminalTransport: 'https-poll', capabilities: ['administrators'] } }); return }
if (path === '/overview') { await route.fulfill({ json: { terminalCount: 0, onlineTerminals: 0, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return }
if (path === '/administrators') { await list(accounts); return }
if (['/desktops', '/terminals', '/users', '/resources', '/tasks', '/audit-logs'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: '测试接口未定义', requestId: 'fixture' } })
})
return calls
}
async function openAccounts(page: Page) { await page.goto('/settings'); await page.getByRole('button', { name: '管理员账号', exact: true }).click(); await expect(page.getByRole('heading', { name: '管理员账号', exact: true })).toBeVisible() }
test('新增管理员校验密码确认,账号持久化字段正确', async ({ page }, testInfo) => {
const calls = await fixture(page)
await page.setViewportSize({ width: 1440, height: 1000 })
await openAccounts(page)
await page.screenshot({ path: testInfo.outputPath('administrators-dark.png'), fullPage: true })
await page.getByRole('button', { name: '新增管理员', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '新增管理员' })
await dialog.getByRole('textbox', { name: '管理员账号', exact: true }).fill('finance')
await dialog.getByRole('textbox', { name: '管理员显示名称' }).fill('财务只读人员')
await dialog.getByLabel('管理员初始密码', { exact: true }).fill('Finance-test-2026')
await expect(dialog.getByRole('button', { name: '保存管理员' })).toBeDisabled()
await dialog.getByLabel('确认管理员初始密码', { exact: true }).fill('Finance-test-2026')
await dialog.getByRole('button', { name: '保存管理员' }).click()
await expect(dialog).not.toBeVisible()
await expect(page.getByText('finance', { exact: true })).toBeVisible()
expect(calls.find(c => c.path === '/administrators')?.body).toEqual({ username: 'finance', displayName: '财务只读人员', role: 'viewer', password: 'Finance-test-2026' })
await page.getByRole('button', { name: '切换浅色主题' }).click()
await page.screenshot({ path: testInfo.outputPath('administrators-light.png'), fullPage: true })
await page.setViewportSize({ width: 390, height: 844 })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: testInfo.outputPath('administrators-mobile.png'), fullPage: true })
})
test('编辑本人禁止角色和停用,取消其他账号停用不提交', async ({ page }) => {
const calls = await fixture(page)
await openAccounts(page)
await page.getByRole('row').filter({ hasText: 'root' }).getByRole('button', { name: '编辑', exact: true }).click()
let dialog = page.getByRole('dialog', { name: '编辑管理员' })
await expect(dialog.getByRole('combobox')).toBeDisabled()
await expect(dialog.getByRole('switch')).toBeDisabled()
await dialog.getByRole('button', { name: '取消', exact: true }).click()
await page.getByRole('row').filter({ hasText: 'operations' }).getByRole('button', { name: '编辑', exact: true }).click()
dialog = page.getByRole('dialog', { name: '编辑管理员' })
await dialog.locator('.el-switch').click()
await dialog.getByRole('button', { name: '保存管理员' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '取消', exact: true }).click()
expect(calls.filter(c => c.method === 'PUT')).toHaveLength(0)
await dialog.getByRole('button', { name: '保存管理员' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click()
await expect(dialog).not.toBeVisible()
expect(calls.find(c => c.method === 'PUT')?.body.enabled).toBe(false)
})
test('重置其他账号密码需要确认,取消不提交', async ({ page }) => {
const calls = await fixture(page)
await openAccounts(page)
await page.getByRole('button', { name: '重置密码', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '重置密码 · operations' })
await dialog.getByLabel('重置后的新密码', { exact: true }).fill('Reset-test-2026')
await dialog.getByLabel('确认重置后的新密码', { exact: true }).fill('Reset-test-2026')
await dialog.getByRole('button', { name: '重置密码', exact: true }).click()
await page.locator('.el-message-box').getByRole('button', { name: '取消', exact: true }).click()
expect(calls.filter(c => c.path.endsWith('/password'))).toHaveLength(0)
await dialog.getByRole('button', { name: '重置密码', exact: true }).click()
await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click()
await expect(dialog).not.toBeVisible()
expect(calls.find(c => c.path === '/administrators/other-admin/password')?.body).toEqual({ password: 'Reset-test-2026' })
})
test('只读人员可修改本人密码,错误原密码不退出,成功后清理登录', async ({ page }) => {
await fixture(page, 'viewer')
await page.goto('/settings')
await expect(page.getByText('无访问权限', { exact: true })).toBeVisible()
await page.getByRole('button', { name: '修改密码', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '修改本人密码' })
await dialog.getByLabel('原密码', { exact: true }).fill('Incorrect-old-password')
await dialog.getByLabel('新密码', { exact: true }).fill('Updated-test-2026')
await dialog.getByLabel('确认新密码', { exact: true }).fill('Updated-test-2026')
await dialog.getByRole('button', { name: '保存并重新登录' }).click()
await expect(dialog.getByText(/原密码不正确/)).toBeVisible()
await dialog.getByLabel('原密码', { exact: true }).fill('Correct-old-password')
await dialog.getByRole('button', { name: '保存并重新登录' }).click()
await expect(page).toHaveURL(/\/login$/)
await expect(page.getByRole('heading', { name: '登录管理控制台' })).toBeVisible()
expect(await page.evaluate(() => sessionStorage.getItem('vdi.accessToken'))).toBeNull()
})