Guard the admin create path with a browser e2e scenario #97

Merged
weichen merged 1 commits from fix/admin-create-e2e-guard into main 2026-10-16 11:22:07 +00:00
+83
View File
@@ -0,0 +1,83 @@
// 防再发守卫:管理员建号路径(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 login(page: Page) {
await page.goto('/')
await expect(page.getByPlaceholder('由管理员分配')).toBeVisible()
await page.getByPlaceholder('由管理员分配').fill(OWNER)
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD)
await page.getByRole('button', { name: '登录', exact: true }).click()
// 三种落点:直接进工作台(密码已生效)|强制改密门|初始密码已失效(种子二次运行)→ 改用新密码重登。
const entered = await page.locator('.tsb-user').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false)
if (!entered) {
const gated = await page.getByText('修改密码').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false)
if (gated) {
const pwInputs = page.locator('.lg-card input[type="password"]')
await pwInputs.nth(0).fill(PASSWORD)
await pwInputs.nth(1).fill(PASSWORD2)
await pwInputs.nth(2).fill(PASSWORD2)
await page.getByRole('button', { name: '设置新密码并进入' }).click()
} 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')).toHaveText(
`已创建 ${newOwner}(待初始化):请在服务器执行 user initialize-account 发放激活 Key`,
)
// 契约断言三:用户表出现该账号,状态为未设密码(HTTP 建号不能设密码/授权限)。
const row = page.locator('.ad-table tbody tr', { hasText: newOwner })
await expect(row).toBeVisible()
await expect(row).toContainText('未设密码')
await expect(row).not.toContainText('ADMIN')
// 界面说明与后端流程一致(激活 Key 由服务器本地 CLI 发放)。
await expect(page.locator('.ad-create .ad-hint')).toContainText('待初始化')
await expect(page.locator('.ad-create .ad-hint')).toContainText('user initialize-account')
})