Files
Agent-System-Frontend/e2e/admin-user-create.playwright.ts
agent-exec-transport dc062820bc
ci / test-and-build (pull_request) Failing after 1s
governance / governance (pull_request) Successful in 16s
agent-exec transport 6b75522dbe048a8b6a6156edfa6f508e/a250410c65cf
2026-10-23 09:56:32 +00:00

104 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 防再发守卫:管理员建号路径(UsersPanel)必须持续符合后端待初始化契约。
// 背景:backend 8dfc223 将 POST /api/admin/users 改为严格 {owner, profile} 解码,
// 前端未跟进导致部署后 UI 建号 400(json: unknown field "name")——单测与 mock
// 都没有覆盖这条组合路径,本场景以真实浏览器把「填用户名 → 创建账号 →
// 确认『待初始化』提示与用户表行」钉进组合验收线。
// 驱动方:Dev 仓 scripts/e2e.sh——构建 RDM 二进制 + frontend dist,起 serve(auth on)
// 与 vite preview;种子账号 e2e-auditor 为基座管理员(bootstrap-admin)。
// 环境契约(缺省值与 e2e.sh 一致;PLAYWRIGHT_BASE_URL 未设置时整组跳过):
// PLAYWRIGHT_BASE_URL / E2E_OWNER / E2E_PASSWORD / E2E_PASSWORD2
import { test, expect, type Page } from '@playwright/test'
const OWNER = process.env.E2E_OWNER || 'e2e-auditor'
const PASSWORD = process.env.E2E_PASSWORD || 'E2eAudit#2026'
const PASSWORD2 = process.env.E2E_PASSWORD2 || 'E2eAudit#2026b'
test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL (scripts/e2e.sh drives this suite)')
async function passwordLogin(page: Page, password: string) {
await expect(page.getByPlaceholder('由管理员分配')).toBeVisible()
await page.getByPlaceholder('由管理员分配').fill(OWNER)
await page.getByPlaceholder('初始密码或常用密码').fill(password)
await page.getByRole('button', { name: '登录', exact: true }).click()
}
/** 强制改密门:设新密码后服务端销毁会话,回到登录页。 */
async function completePasswordGate(page: Page) {
// 门模式异步加载:current_required 渲染 #forced-current,其余模式渲染受限设密提示。
const current = page.locator('#forced-current')
await expect(current.or(page.locator('.lg-card .lg-hint'))).toBeVisible()
if (await current.isVisible()) await current.fill(PASSWORD)
// 「新密码(≥8 位)」「确认新密码」label 未关联输入框,getByLabel 无法命中,按门内顺序定位。
const pw = page.locator('.lg-card input[type="password"]:not(#forced-current)')
await pw.nth(0).fill(PASSWORD2)
await pw.nth(1).fill(PASSWORD2)
await page.getByRole('button', { name: '设置新密码', exact: true }).click()
await expect(page.getByRole('heading', { name: '密码已更新' })).toBeVisible({ timeout: 10_000 })
await page.getByRole('button', { name: '返回登录', exact: true }).click()
}
async function login(page: Page) {
await page.goto('/')
await passwordLogin(page, PASSWORD)
// 三种落点:直接进工作台(密码已生效)|强制改密门 → 设新密码后重登|初始密码已失效(种子二次运行)→ 改用新密码重登。
const entered = await page.locator('.tsb-user').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false)
if (!entered) {
// 门标题与提交按钮同名(设置新密码):用 heading 角色唯一定位。
const gated = await page.getByRole('heading', { name: '设置新密码' }).waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false)
if (gated) {
await completePasswordGate(page)
await passwordLogin(page, PASSWORD2)
} else {
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2)
await page.getByRole('button', { name: '登录', exact: true }).click()
}
}
await expect(page.locator('.tsb-user')).toBeVisible({ timeout: 15_000 })
}
test('管理员建号:UI 请求新契约并给出待初始化提示', async ({ page }) => {
await login(page)
// 账户菜单 → 系统管理…(全屏独立视图)→ 用户管理分区。
await page.locator('.tsb-user').click()
const adminEntry = page.getByText('系统管理…', { exact: true })
await expect(adminEntry).toBeVisible()
await adminEntry.click()
await expect(page.locator('.az')).toBeVisible()
const usersSection = page.getByText('用户管理', { exact: true }).first()
if (await usersSection.count()) await usersSection.click()
// 建号表单:用户名每次运行唯一(重复运行不因 409 假红),显示名可空。
const newOwner = `e2e-ui-${Date.now().toString(36)}`
const userInput = page.getByPlaceholder('用户名(登录名)')
await expect(userInput).toBeVisible()
await userInput.fill(newOwner)
await page.getByPlaceholder('显示名(可空)').fill('组合验收')
// 创建 → 契约断言一:请求体是严格 {owner, profile:{name}},无 password/isAdmin。
const createBody = page.waitForRequest(
req => req.url().endsWith('/api/admin/users') && req.method() === 'POST',
).then(req => req.postDataJSON())
await page.getByRole('button', { name: '创建账号', exact: true }).click()
const body = await createBody
expect(body).toEqual({ owner: newOwner, profile: { name: '组合验收' } })
// 契约断言二:页面创建后账号可直接使用,不再提示 CLI 初始化。
await expect(page.locator('.ad-notice')).toContainText(`已创建 ${newOwner}`)
// 契约断言三:请求不带密码时服务端生成一次性初始密码(响应 initialPassword,
// 账号 hasPassword=true 且 mustChangePassword=true)——页面一次性交付该密码,
// 用户表该账号状态为待改密而非未设密码,且未被授予管理员。
await expect(page.locator('.ad-reset .ad-hint')).toContainText(`初始密码仅显示一次,请立即安全交付给 ${newOwner}`)
await expect(page.locator('.ad-reset input[aria-label="一次性密码"]')).not.toHaveValue('')
const row = page.locator('.ad-table tbody tr', { hasText: newOwner })
await expect(row).toBeVisible()
await expect(row).toContainText('待改密')
await expect(row).not.toContainText('未设密码')
await expect(row).not.toContainText('ADMIN')
// 界面说明与后端流程一致(激活 Key 由服务器本地 CLI 发放)。
await expect(page.locator('.ad-create .ad-hint').filter({ hasText: '初始密码' })).toContainText('初始密码')
await expect(page.locator('.ad-create .ad-hint').filter({ hasText: 'user initialize-account' })).toHaveCount(0)
})