618d1d796c
Co-Authored-By: Claude Code <noreply@anthropic.com>
152 lines
7.7 KiB
TypeScript
152 lines
7.7 KiB
TypeScript
// 账号治理 + 用户同步 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()
|
||
})
|