From 119fff96e8d09c03e406c98d0436f1798455d5c1 Mon Sep 17 00:00:00 2001 From: weichen Date: Fri, 16 Oct 2026 10:47:41 +0000 Subject: [PATCH] Guard the admin create path with a browser e2e scenario Why: the 400 (unknown field name) outage shipped because no automated check drove the UsersPanel create path against the strict backend contract; this scenario pins the request body, the pending notice, the table row state and the CLI handoff hint into the composition suite. Evidence: passing run against the deployed shared dev (SELF, task card). Impact: e2e/ only; no production code. Refs: backend 8dfc223, frontend #94 --- e2e/admin-user-create.playwright.ts | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 e2e/admin-user-create.playwright.ts diff --git a/e2e/admin-user-create.playwright.ts b/e2e/admin-user-create.playwright.ts new file mode 100644 index 0000000..e8df47d --- /dev/null +++ b/e2e/admin-user-create.playwright.ts @@ -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') +})