25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

240 satır
8.8 KiB

  1. import type { APIRequestContext, APIResponse, Page, TestInfo } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
  3. import { adminAccessToken, confirmMessageBox, envelopeData } from './helpers'
  4. type JsonRecord = Record<string, unknown>
  5. interface DepartmentNode extends JsonRecord {
  6. id: string
  7. name: string
  8. children: DepartmentNode[]
  9. }
  10. interface LoginRoleCase {
  11. code: 'administrative' | 'teacher'
  12. label: string
  13. displayName: string
  14. username: string
  15. password: string
  16. department: DepartmentNode
  17. organizationPath: DepartmentNode[]
  18. }
  19. interface CreatedUser {
  20. id: string
  21. username: string
  22. dataVersion: number
  23. }
  24. test.use({ trace: 'off', video: 'off' })
  25. const suffix = runPrefix.replace(/[^A-Za-z0-9]/g, '').slice(-10).toLowerCase() || 'manual'
  26. const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object'
  27. ? value as JsonRecord
  28. : {}
  29. const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value)
  30. ? value.map(asRecord)
  31. : []
  32. async function body(response: APIResponse) {
  33. return response.json().catch(() => ({}))
  34. }
  35. function pageRecords(value: unknown) {
  36. const data = envelopeData(value)
  37. const root = asRecord(data)
  38. const items = root.records ?? root.items
  39. return Array.isArray(items) ? items.map(asRecord) : asRecords(data)
  40. }
  41. function normalizeDepartment(value: unknown): DepartmentNode {
  42. const item = asRecord(value)
  43. return {
  44. ...item,
  45. id: String(item.id ?? ''),
  46. name: String(item.name ?? ''),
  47. children: Array.isArray(item.children) ? item.children.map(normalizeDepartment) : [],
  48. }
  49. }
  50. async function createRoleUser(
  51. request: APIRequestContext,
  52. headers: Record<string, string>,
  53. role: LoginRoleCase,
  54. ) {
  55. const rolesResponse = await request.get('/api/auth/v1/roles/all', { headers })
  56. expect(rolesResponse.status(), '读取正式角色目录').toBe(200)
  57. const rolesData = envelopeData(await body(rolesResponse))
  58. const roles = Array.isArray(rolesData) ? asRecords(rolesData) : pageRecords(rolesData)
  59. const targetRole = roles.find((item) => String(item.code ?? item.roleCode) === role.code && item.enabled !== false)
  60. expect(targetRole, `测试环境必须存在启用的 ${role.code} 角色`).toBeTruthy()
  61. const response = await request.post('/api/auth/v1/users', {
  62. headers,
  63. data: {
  64. username: role.username,
  65. displayName: role.displayName,
  66. departmentId: Number(role.department.id),
  67. roleIds: [Number(targetRole!.id)],
  68. defaultRoleId: Number(targetRole!.id),
  69. enabled: true,
  70. password: role.password,
  71. remark: `${runPrefix} ${role.label}身份真实登录测试,结束自动删除`,
  72. version: 0,
  73. },
  74. })
  75. expect(response.status(), `创建临时${role.label}账号`).toBe(200)
  76. const created = asRecord(envelopeData(await body(response)))
  77. return {
  78. id: String(created.id ?? ''),
  79. username: role.username,
  80. dataVersion: Number(created.dataVersion ?? created.version ?? 0),
  81. } satisfies CreatedUser
  82. }
  83. async function completeInitialPassword(
  84. request: APIRequestContext,
  85. role: LoginRoleCase,
  86. user: CreatedUser,
  87. ) {
  88. const login = await request.post('/api/auth/v1/auth/login', {
  89. data: {
  90. roleCode: role.code,
  91. userId: Number(user.id),
  92. departmentId: Number(role.department.id),
  93. password: role.password,
  94. rememberMe: false,
  95. },
  96. })
  97. expect(login.status(), `${role.label}初始密码登录`).toBe(200)
  98. const loginData = asRecord(envelopeData(await body(login)))
  99. const accessToken = String(loginData.accessToken ?? asRecord(loginData.tokens).accessToken ?? '')
  100. expect(accessToken).not.toBe('')
  101. const changed = await request.put('/api/auth/v1/auth/initial-password', {
  102. headers: { Authorization: `Bearer ${accessToken}` },
  103. data: { newPassword: role.password.replace('Start', 'Ready') },
  104. })
  105. expect(changed.status(), `${role.label}完成首次改密`).toBe(200)
  106. role.password = role.password.replace('Start', 'Ready')
  107. }
  108. async function selectVisibleOption(page: Page, text: string) {
  109. const dropdown = page.locator('.el-select-dropdown:visible').last()
  110. await expect(dropdown).toBeVisible()
  111. const option = dropdown.locator('.el-select-dropdown__item').filter({ hasText: text }).first()
  112. await expect(option).toBeVisible()
  113. await option.click()
  114. }
  115. async function loginRoleInBrowser(page: Page, role: LoginRoleCase, testInfo: TestInfo) {
  116. await page.goto('/login')
  117. await page.locator(`[data-role-code="${role.code}"]`).click()
  118. const organizationSelects = page.locator('.login-role-fields > .login-field .el-select:not(.login-identity-picker)')
  119. for (let index = 0; index < role.organizationPath.length; index += 1) {
  120. await organizationSelects.nth(index).click()
  121. await selectVisibleOption(page, role.organizationPath[index].name)
  122. }
  123. const identityPicker = page.locator('.login-identity-picker')
  124. await expect(identityPicker).toBeEnabled()
  125. await identityPicker.click()
  126. await selectVisibleOption(page, role.displayName)
  127. await page.locator('input[name="password"]').fill(role.password)
  128. const loginResponse = page.waitForResponse((response) => (
  129. response.request().method() === 'POST'
  130. && new URL(response.url()).pathname === '/api/auth/v1/auth/login'
  131. ))
  132. await page.getByRole('button', { name: '登录系统' }).click()
  133. expect((await loginResponse).status(), `${role.label}浏览器登录`).toBe(200)
  134. await expect(page).toHaveURL(/\/overview(?:\?|$)/)
  135. await expect(page.locator('.app-shell')).toBeVisible()
  136. await expect(page.locator('.user-menu-trigger')).toContainText(role.displayName)
  137. await captureScreenshot(page, testInfo, `${role.code}-real-account-login`)
  138. await page.locator('.user-menu-trigger').click()
  139. await page.getByRole('menuitem', { name: /退出登录/ }).click()
  140. await confirmMessageBox(page, '退出登录')
  141. await expect(page).toHaveURL(/\/login(?:\?|$)/)
  142. }
  143. async function cleanupUser(
  144. request: APIRequestContext,
  145. headers: Record<string, string>,
  146. user: CreatedUser,
  147. ) {
  148. const lookup = await request.get('/api/auth/v1/users', {
  149. headers,
  150. params: { keyword: user.username, page: 1, size: 100 },
  151. })
  152. if (lookup.status() !== 200) return false
  153. const current = pageRecords(await body(lookup)).find((item) => String(item.username ?? '') === user.username)
  154. if (!current) return true
  155. const deleted = await request.delete(`/api/auth/v1/users/${encodeURIComponent(String(current.id))}`, {
  156. headers,
  157. data: { version: Number(current.dataVersion ?? current.version ?? 0) },
  158. })
  159. return [200, 204, 404].includes(deleted.status())
  160. }
  161. test('行政和教员必须通过组织与账号选择完成真实登录', async ({ page, request }, testInfo) => {
  162. const token = await adminAccessToken(request)
  163. const headers = { Authorization: `Bearer ${token}` }
  164. const contextResponse = await request.get('/api/auth/v1/auth/login-context')
  165. expect(contextResponse.status()).toBe(200)
  166. const context = asRecord(envelopeData(await body(contextResponse)))
  167. const roots = (Array.isArray(context.departments) ? context.departments : []).map(normalizeDepartment)
  168. const administrativeDepartment = roots.find((item) => item.id && item.name)
  169. const teacherRoot = roots.find((item) => item.children.length > 0)
  170. const teacherDepartment = teacherRoot?.children.find((item) => item.id && item.name)
  171. expect(administrativeDepartment, '行政登录需要至少一个一级部门').toBeTruthy()
  172. expect(teacherRoot && teacherDepartment, '教员登录需要至少一个二级部门').toBeTruthy()
  173. const roles: LoginRoleCase[] = [
  174. {
  175. code: 'administrative',
  176. label: '行政',
  177. displayName: `${runPrefix}-行政登录员`,
  178. username: `ape_adm_${suffix}`.slice(0, 32),
  179. password: `Start@Aa1${suffix.slice(-4)}`,
  180. department: administrativeDepartment!,
  181. organizationPath: [administrativeDepartment!],
  182. },
  183. {
  184. code: 'teacher',
  185. label: '教员',
  186. displayName: `${runPrefix}-教员登录员`,
  187. username: `ape_tch_${suffix}`.slice(0, 32),
  188. password: `Start@Bb2${suffix.slice(-4)}`,
  189. department: teacherDepartment!,
  190. organizationPath: [teacherRoot!, teacherDepartment!],
  191. },
  192. ]
  193. const created: Array<{ role: LoginRoleCase; user: CreatedUser }> = []
  194. const cleanup: Array<{ role: string; result: 'CLEANED' | 'CLEANUP_FAILED' }> = []
  195. try {
  196. for (const role of roles) {
  197. const user = await createRoleUser(request, headers, role)
  198. created.push({ role, user })
  199. await completeInitialPassword(request, role, user)
  200. await loginRoleInBrowser(page, role, testInfo)
  201. }
  202. } finally {
  203. for (const item of [...created].reverse()) {
  204. cleanup.push({
  205. role: item.role.code,
  206. result: await cleanupUser(request, headers, item.user) ? 'CLEANED' : 'CLEANUP_FAILED',
  207. })
  208. }
  209. await attachJson(testInfo, 'role-login-cleanup', cleanup)
  210. }
  211. expect(cleanup.every((item) => item.result === 'CLEANED'), '临时身份账号必须全部清理').toBeTruthy()
  212. })