|
- import type { APIRequestContext, APIResponse, Page, TestInfo } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
- import { adminAccessToken, confirmMessageBox, envelopeData } from './helpers'
-
- type JsonRecord = Record<string, unknown>
-
- interface DepartmentNode extends JsonRecord {
- id: string
- name: string
- children: DepartmentNode[]
- }
-
- interface LoginRoleCase {
- code: 'administrative' | 'teacher'
- label: string
- displayName: string
- username: string
- password: string
- department: DepartmentNode
- organizationPath: DepartmentNode[]
- }
-
- interface CreatedUser {
- id: string
- username: string
- dataVersion: number
- }
-
- test.use({ trace: 'off', video: 'off' })
-
- const suffix = runPrefix.replace(/[^A-Za-z0-9]/g, '').slice(-10).toLowerCase() || 'manual'
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object'
- ? value as JsonRecord
- : {}
-
- const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value)
- ? value.map(asRecord)
- : []
-
- async function body(response: APIResponse) {
- return response.json().catch(() => ({}))
- }
-
- function pageRecords(value: unknown) {
- const data = envelopeData(value)
- const root = asRecord(data)
- const items = root.records ?? root.items
- return Array.isArray(items) ? items.map(asRecord) : asRecords(data)
- }
-
- function normalizeDepartment(value: unknown): DepartmentNode {
- const item = asRecord(value)
- return {
- ...item,
- id: String(item.id ?? ''),
- name: String(item.name ?? ''),
- children: Array.isArray(item.children) ? item.children.map(normalizeDepartment) : [],
- }
- }
-
- async function createRoleUser(
- request: APIRequestContext,
- headers: Record<string, string>,
- role: LoginRoleCase,
- ) {
- const rolesResponse = await request.get('/api/auth/v1/roles/all', { headers })
- expect(rolesResponse.status(), '读取正式角色目录').toBe(200)
- const rolesData = envelopeData(await body(rolesResponse))
- const roles = Array.isArray(rolesData) ? asRecords(rolesData) : pageRecords(rolesData)
- const targetRole = roles.find((item) => String(item.code ?? item.roleCode) === role.code && item.enabled !== false)
- expect(targetRole, `测试环境必须存在启用的 ${role.code} 角色`).toBeTruthy()
-
- const response = await request.post('/api/auth/v1/users', {
- headers,
- data: {
- username: role.username,
- displayName: role.displayName,
- departmentId: Number(role.department.id),
- roleIds: [Number(targetRole!.id)],
- defaultRoleId: Number(targetRole!.id),
- enabled: true,
- password: role.password,
- remark: `${runPrefix} ${role.label}身份真实登录测试,结束自动删除`,
- version: 0,
- },
- })
- expect(response.status(), `创建临时${role.label}账号`).toBe(200)
- const created = asRecord(envelopeData(await body(response)))
- return {
- id: String(created.id ?? ''),
- username: role.username,
- dataVersion: Number(created.dataVersion ?? created.version ?? 0),
- } satisfies CreatedUser
- }
-
- async function completeInitialPassword(
- request: APIRequestContext,
- role: LoginRoleCase,
- user: CreatedUser,
- ) {
- const login = await request.post('/api/auth/v1/auth/login', {
- data: {
- roleCode: role.code,
- userId: Number(user.id),
- departmentId: Number(role.department.id),
- password: role.password,
- rememberMe: false,
- },
- })
- expect(login.status(), `${role.label}初始密码登录`).toBe(200)
- const loginData = asRecord(envelopeData(await body(login)))
- const accessToken = String(loginData.accessToken ?? asRecord(loginData.tokens).accessToken ?? '')
- expect(accessToken).not.toBe('')
-
- const changed = await request.put('/api/auth/v1/auth/initial-password', {
- headers: { Authorization: `Bearer ${accessToken}` },
- data: { newPassword: role.password.replace('Start', 'Ready') },
- })
- expect(changed.status(), `${role.label}完成首次改密`).toBe(200)
- role.password = role.password.replace('Start', 'Ready')
- }
-
- async function selectVisibleOption(page: Page, text: string) {
- const dropdown = page.locator('.el-select-dropdown:visible').last()
- await expect(dropdown).toBeVisible()
- const option = dropdown.locator('.el-select-dropdown__item').filter({ hasText: text }).first()
- await expect(option).toBeVisible()
- await option.click()
- }
-
- async function loginRoleInBrowser(page: Page, role: LoginRoleCase, testInfo: TestInfo) {
- await page.goto('/login')
- await page.locator(`[data-role-code="${role.code}"]`).click()
-
- const organizationSelects = page.locator('.login-role-fields > .login-field .el-select:not(.login-identity-picker)')
- for (let index = 0; index < role.organizationPath.length; index += 1) {
- await organizationSelects.nth(index).click()
- await selectVisibleOption(page, role.organizationPath[index].name)
- }
-
- const identityPicker = page.locator('.login-identity-picker')
- await expect(identityPicker).toBeEnabled()
- await identityPicker.click()
- await selectVisibleOption(page, role.displayName)
- await page.locator('input[name="password"]').fill(role.password)
-
- const loginResponse = page.waitForResponse((response) => (
- response.request().method() === 'POST'
- && new URL(response.url()).pathname === '/api/auth/v1/auth/login'
- ))
- await page.getByRole('button', { name: '登录系统' }).click()
- expect((await loginResponse).status(), `${role.label}浏览器登录`).toBe(200)
- await expect(page).toHaveURL(/\/overview(?:\?|$)/)
- await expect(page.locator('.app-shell')).toBeVisible()
- await expect(page.locator('.user-menu-trigger')).toContainText(role.displayName)
- await captureScreenshot(page, testInfo, `${role.code}-real-account-login`)
-
- await page.locator('.user-menu-trigger').click()
- await page.getByRole('menuitem', { name: /退出登录/ }).click()
- await confirmMessageBox(page, '退出登录')
- await expect(page).toHaveURL(/\/login(?:\?|$)/)
- }
-
- async function cleanupUser(
- request: APIRequestContext,
- headers: Record<string, string>,
- user: CreatedUser,
- ) {
- const lookup = await request.get('/api/auth/v1/users', {
- headers,
- params: { keyword: user.username, page: 1, size: 100 },
- })
- if (lookup.status() !== 200) return false
- const current = pageRecords(await body(lookup)).find((item) => String(item.username ?? '') === user.username)
- if (!current) return true
- const deleted = await request.delete(`/api/auth/v1/users/${encodeURIComponent(String(current.id))}`, {
- headers,
- data: { version: Number(current.dataVersion ?? current.version ?? 0) },
- })
- return [200, 204, 404].includes(deleted.status())
- }
-
- test('行政和教员必须通过组织与账号选择完成真实登录', async ({ page, request }, testInfo) => {
- const token = await adminAccessToken(request)
- const headers = { Authorization: `Bearer ${token}` }
- const contextResponse = await request.get('/api/auth/v1/auth/login-context')
- expect(contextResponse.status()).toBe(200)
- const context = asRecord(envelopeData(await body(contextResponse)))
- const roots = (Array.isArray(context.departments) ? context.departments : []).map(normalizeDepartment)
- const administrativeDepartment = roots.find((item) => item.id && item.name)
- const teacherRoot = roots.find((item) => item.children.length > 0)
- const teacherDepartment = teacherRoot?.children.find((item) => item.id && item.name)
- expect(administrativeDepartment, '行政登录需要至少一个一级部门').toBeTruthy()
- expect(teacherRoot && teacherDepartment, '教员登录需要至少一个二级部门').toBeTruthy()
-
- const roles: LoginRoleCase[] = [
- {
- code: 'administrative',
- label: '行政',
- displayName: `${runPrefix}-行政登录员`,
- username: `ape_adm_${suffix}`.slice(0, 32),
- password: `Start@Aa1${suffix.slice(-4)}`,
- department: administrativeDepartment!,
- organizationPath: [administrativeDepartment!],
- },
- {
- code: 'teacher',
- label: '教员',
- displayName: `${runPrefix}-教员登录员`,
- username: `ape_tch_${suffix}`.slice(0, 32),
- password: `Start@Bb2${suffix.slice(-4)}`,
- department: teacherDepartment!,
- organizationPath: [teacherRoot!, teacherDepartment!],
- },
- ]
- const created: Array<{ role: LoginRoleCase; user: CreatedUser }> = []
- const cleanup: Array<{ role: string; result: 'CLEANED' | 'CLEANUP_FAILED' }> = []
-
- try {
- for (const role of roles) {
- const user = await createRoleUser(request, headers, role)
- created.push({ role, user })
- await completeInitialPassword(request, role, user)
- await loginRoleInBrowser(page, role, testInfo)
- }
- } finally {
- for (const item of [...created].reverse()) {
- cleanup.push({
- role: item.role.code,
- result: await cleanupUser(request, headers, item.user) ? 'CLEANED' : 'CLEANUP_FAILED',
- })
- }
- await attachJson(testInfo, 'role-login-cleanup', cleanup)
- }
-
- expect(cleanup.every((item) => item.result === 'CLEANED'), '临时身份账号必须全部清理').toBeTruthy()
- })
|