Files
photo-live/apps/admin/e2e/users.spec.ts
T
2026-09-20 20:26:53 +08:00

152 lines
7.7 KiB
TypeScript
Raw 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.
// 账号治理 + 用户同步 e2e(真实后端 + compose 基础设施):
// 账号页:种子用户可见 → 前缀过滤 → 自我保护(自身行角色禁改)→
// 重置密码(生成密码一次性展示/复制)→ 新密码 API 登录 →
// 停用(确认框)→ 该用户既有 token 即刻 401 → 状态桶过滤联动;
// 同步页:手动补拉(mock 源默认健康场景)→ 状态刷新(终态+计数+水位)→
// 运行历史行点击 → 失败明细抽屉(成功批次空明细——admin e2e 无法
// 触达后端 mock provider 注入坏行场景,失败明细非空链路由 API 侧
// e2e 覆盖,此处断言空抽屉 + 真实批次计数)。
// 治理操作用独立种子账号 gov-e2e(与 albums 套件的 staff-e2e 隔离,互不影响)。
// 选择器一律 data-test(test.use testIdAttribute),与文案解耦。
import { expect, test, type Page } from '@playwright/test'
import { adminCredentials, loginAsAdmin, seedFixture } from './helpers'
test.use({ testIdAttribute: 'data-test', permissions: ['clipboard-read', 'clipboard-write'] })
const GOV_ACCOUNT = 'gov-e2e'
test.beforeAll(() => {
// 幂等种子治理目标账号(占位密码不可登录;本套件经重置密码赋予真实密码)
seedFixture('staff', GOV_ACCOUNT, 'Gov E2E')
})
/** 进入账号管理页(已登录态) */
async function gotoUsers(page: Page): Promise<void> {
await page.goto('/#/system/users')
await expect(page.getByTestId('users-table')).toBeVisible()
}
/** 账号表内定位目标行 */
function userRow(page: Page, account: string) {
return page.getByTestId('users-table').locator('tr', { hasText: account })
}
test('账号页:种子用户可见 → 前缀过滤 → 自我保护(自身行角色禁改)', async ({ page }) => {
await loginAsAdmin(page)
await gotoUsers(page)
const { account } = adminCredentials()
await expect(userRow(page, account)).toBeVisible()
await expect(userRow(page, GOV_ACCOUNT)).toBeVisible()
// 前缀过滤:查 gov-e2e → 目标行保留、admin 行消失
await page.getByTestId('users-query-input').fill(GOV_ACCOUNT)
await page.getByTestId('users-search-btn').click()
await expect(userRow(page, GOV_ACCOUNT)).toBeVisible()
await expect(userRow(page, account)).toHaveCount(0)
// 自我保护:admin 自身行的编辑对话框中角色选择禁用(服务端 400 的 UI 前置)
await page.getByTestId('users-query-input').fill('')
await page.getByTestId('users-search-btn').click()
await userRow(page, account).getByTestId('user-edit-btn').click()
await expect(page.getByTestId('user-edit-dialog')).toBeVisible()
// ElSelect 禁用态落在内层 .el-select__wrapper 的 is-disabled class
//(根 div 与 toBeDisabled 均不适配)
await expect(page.getByTestId('user-edit-role').locator('.el-select__wrapper')).toHaveClass(
/is-disabled/
)
await page.getByTestId('user-edit-cancel').click()
await expect(page.getByTestId('user-edit-dialog')).toBeHidden()
})
test('重置密码(生成)→ 新密码可登录 → 停用 → 既有 token 401 → 状态过滤联动', async ({ page }) => {
await loginAsAdmin(page)
await gotoUsers(page)
// 定位治理目标行(过滤收敛到单行)
await page.getByTestId('users-query-input').fill(GOV_ACCOUNT)
await page.getByTestId('users-search-btn').click()
const govRow = userRow(page, GOV_ACCOUNT)
await expect(govRow).toBeVisible()
// 上一轮运行可能已停用(锁定用户无法登录)——先解锁恢复基线,保证幂等重跑
if (await govRow.getByTestId('user-unlock-btn').isVisible()) {
await govRow.getByTestId('user-unlock-btn').click()
await page.locator('.el-message-box').getByRole('button', { name: '确定' }).click()
await expect(govRow.getByTestId('user-deactivate-btn')).toBeVisible({ timeout: 20_000 })
}
// ── 重置密码:生成模式(默认)→ 一次性展示 + 复制 ──
await govRow.getByTestId('user-reset-btn').click()
await expect(page.getByTestId('user-reset-dialog')).toBeVisible()
await page.getByTestId('user-reset-submit').click()
await expect(page.getByTestId('user-reset-result')).toBeVisible()
const newPassword = ((await page.getByTestId('user-reset-password').textContent()) ?? '').trim()
expect(newPassword.length).toBeGreaterThanOrEqual(8)
await page.getByTestId('user-reset-copy').click()
// 127.0.0.1 是安全上下文:剪贴板内容与展示一致
const clipped = await page.evaluate(() => navigator.clipboard.readText())
expect(clipped).toBe(newPassword)
await page.getByTestId('user-reset-close').click()
await expect(page.getByTestId('user-reset-dialog')).toBeHidden()
// ── 新密码 API 登录(重置已撤销占位凭据语义)──
const loginRes = await page.request.post('/v1/auth/password', {
data: { account: GOV_ACCOUNT, password: newPassword }
})
expect(loginRes.ok()).toBeTruthy()
const { sessionToken } = (await loginRes.json()) as { sessionToken: string }
const authed = { Authorization: `Bearer ${sessionToken}` }
const before = await page.request.get('/v1/albums', { headers: authed })
expect(before.status()).toBe(200)
// ── 停用(确认框:会话撤销警告)→ 行内切换为「解锁」──
await govRow.getByTestId('user-deactivate-btn').click()
await page.locator('.el-message-box').getByRole('button', { name: '确定' }).click()
await expect(govRow.getByTestId('user-unlock-btn')).toBeVisible({ timeout: 20_000 })
// 停用即撤会话:该用户既有 token 即刻 401
const after = await page.request.get('/v1/albums', { headers: authed })
expect(after.status()).toBe(401)
// ── 状态桶过滤联动:locked + gov 前缀 → 可见;locked + staff-e2e → 空 ──
await page.getByTestId('users-status-select').click()
await page.locator('.el-select-dropdown__item:visible', { hasText: '本地锁定' }).click()
await page.getByTestId('users-search-btn').click()
await expect(userRow(page, GOV_ACCOUNT)).toBeVisible()
await page.getByTestId('users-query-input').fill('staff-e2e')
await page.getByTestId('users-search-btn').click()
await expect(page.getByTestId('users-table').locator('tbody tr')).toHaveCount(0)
})
test('同步页:手动补拉 → 终态计数刷新 → 运行历史行点击打开失败明细抽屉', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('/#/system/user-sync')
await expect(page.getByTestId('sync-status-card')).toBeVisible()
await expect(page.getByTestId('sync-runs-table')).toBeVisible()
// 手动补拉(202 受理 → 页内轮询至批次终态;idle 是受理前的初值,须等到
// 终态标签而非「非 running」——受理与批次落账之间有秒级窗口)
await page.getByTestId('sync-trigger-btn').click()
await expect
.poll(async () => page.getByTestId('sync-state-tag').getAttribute('data-state'), {
timeout: 30_000
})
.toMatch(/^(success|partial)$/)
// mock 源默认健康场景:首轮 added=3 / 已有水位时增量幂等,终态均应为 success
// 状态刷新:最近批次计数 chips 可见、源水位已提交(非 '-')
await expect(page.getByTestId('sync-counts')).toBeVisible()
const watermark = (await page.getByTestId('sync-source-version').textContent()) ?? ''
expect(watermark.trim()).not.toBe('-')
// 运行历史:首行为本手动批次
const firstRun = page.getByTestId('sync-runs-table').locator('tbody tr').first()
await expect(firstRun).toContainText('手动')
// 行点击 → 失败明细抽屉(成功批次 → 空明细)
await firstRun.click()
await expect(page.getByTestId('sync-errors-drawer')).toBeVisible()
await expect(page.getByTestId('sync-errors-empty').first()).toBeVisible()
})