// 管理员随机临时密码与登录入口回归:管理员重置 → 用户密码登录 → 强制改密; // 登录页不再提供一次性 Key 登录 tab。Key 的分发/管理 API 与非登录页流程保留。 // 驱动:任意 auth-on 实例(PLAYWRIGHT_BASE_URL + E2E_PLAIN_BASE; // scripts/e2e.sh 第 4 阶段顺路覆盖,或对隔离实例直连——见 Dev 任务卡 // feature/svc-login-recovery-flow 的真实浏览器验收记录)。 import { test, expect, type Page } from '@playwright/test' const ADMIN = process.env.E2E_ADMIN || 'e2e-admin' const ADMIN_BOOT = process.env.E2E_ADMIN_PASSWORD || 'E2eAdmin#2026' const ADMIN_PW = process.env.E2E_ADMIN_PASSWORD2 || 'E2eAdmin#2026b' const USER = process.env.E2E_LOGIN_KEY_USER || 'e2e-user' const USER_PW = process.env.E2E_LOGIN_KEY_PASSWORD2 || 'E2eUser#2026b' test.skip(!(process.env.PLAYWRIGHT_BASE_URL && process.env.E2E_PLAIN_BASE), 'set PLAYWRIGHT_BASE_URL + E2E_PLAIN_BASE (auth-on instance required)') async function passwordLogin(page: Page, owner: string, password: string) { await page.goto('/') await page.getByPlaceholder('由管理员分配').fill(owner) await page.getByPlaceholder('初始密码或常用密码').fill(password) await page.getByRole('button', { name: '登录', exact: true }).click() } /** 管理员登录;初始密码(must-change)则先完成受限设密再以新密码重登。 * 初始密码单次有效:已跑过本用例的环境(boot 已被改掉)自动改用新密码。 */ async function adminLogin(page: Page) { await passwordLogin(page, ADMIN, ADMIN_BOOT) // 门标题与提交按钮同名(设置新密码):用 heading 角色唯一定位, // getByText 双命中会触发 strict-mode 抛错被 catch 吞掉、误判 gated=false。 let gate = page.getByRole('heading', { name: '设置新密码' }) let gated = await gate.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (!gated) { // boot 密码失败(此前运行已完成改密):换正式密码重登。 const loginErr = await page.locator('.lg-err').waitFor({ state: 'visible', timeout: 3_000 }).then(() => true).catch(() => false) if (!loginErr) throw new Error('admin login neither gated nor errored') await passwordLogin(page, ADMIN, ADMIN_PW) gate = page.getByRole('heading', { name: '设置新密码' }) gated = await gate.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) } if (gated) { const pw = page.locator('.lg-card input[type="password"]') await pw.nth(0).fill(ADMIN_PW) await pw.nth(1).fill(ADMIN_PW) 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() await passwordLogin(page, ADMIN, ADMIN_PW) } await expect(page.locator('.tsb-tool', { hasText: '代理' })).toBeVisible({ timeout: 15_000 }) } test('管理员随机重置:临时密码登录后强制改密;登录页不提供 Key 登录', async ({ page }) => { // 全流程含多次 argon2 登录与两道门,30s 默认预算不够。 test.setTimeout(120_000) // 1) 管理员进入用户管理,为目标用户生成随机临时密码(明文仅显示一次)。 await adminLogin(page) await page.locator('.tsb-user').click() await page.getByRole('button', { name: '系统管理…' }).click() const zone = page.getByRole('dialog', { name: '系统管理' }) await expect(zone).toBeVisible() const row = zone.locator('tr', { hasText: USER }) await row.getByRole('button', { name: '重置临时密码' }).click() const temporaryPassword = zone.locator('input[aria-label="一次性密码"]') await expect(temporaryPassword).toBeVisible({ timeout: 10_000 }) const password = await temporaryPassword.inputValue() expect(password.length).toBeGreaterThan(0) // 2) 管理员退出系统管理与登录态,回到登录页。 //(Key 面板内也有「关闭」;退出全屏层要点头部的「返回工作台」。) await zone.getByRole('button', { name: '返回工作台' }).click() await page.locator('.tsb-user').click() await page.getByRole('button', { name: '退出登录' }).click() await expect(page.getByRole('button', { name: '登录', exact: true })).toBeVisible({ timeout: 10_000 }) // 3) 用户使用临时密码登录后进入强制改密门。 await passwordLogin(page, USER, password) const gate = page.locator('.lg-card') await expect(gate.getByText(`账号 ${USER}`)).toBeVisible({ timeout: 10_000 }) await expect(gate.locator('input[autocomplete="current-password"]')).toHaveCount(1) const pw = gate.locator('input[type="password"]') await pw.nth(0).fill(password) await pw.nth(1).fill(USER_PW) await pw.nth(2).fill(USER_PW) await gate.getByRole('button', { name: '设置新密码', exact: true }).click() // 4) 成功页:提示用新密码登录,唯一出口回登录页。 await expect(page.getByRole('heading', { name: '密码已更新' })).toBeVisible({ timeout: 10_000 }) await page.getByRole('button', { name: '返回登录', exact: true }).click() await expect(page.getByRole('button', { name: '登录', exact: true })).toBeVisible() // 5) 新密码登录进工作台。 await passwordLogin(page, USER, USER_PW) await expect(page.locator('.tsb-tool', { hasText: '代理' })).toBeVisible({ timeout: 15_000 }) // 6) 登录页不再提供一次性 Key 登录入口。 await page.locator('.tsb-user').click() await page.getByRole('button', { name: '退出登录' }).click() await expect(page.getByRole('button', { name: '登录', exact: true })).toBeVisible({ timeout: 10_000 }) await expect(page.getByRole('tab', { name: '一次性登录 Key' })).toHaveCount(0) await expect(page.getByPlaceholder('alk_…')).toHaveCount(0) })