選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

232 行
12 KiB

  1. import type { APIRequestContext, Locator, Page } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
  3. import {
  4. adminAccessToken,
  5. chooseSelectOption,
  6. confirmMessageBox,
  7. envelopeData,
  8. fillFormItem,
  9. loginAsAdmin,
  10. tableRowByText,
  11. visibleDialog,
  12. } from './helpers'
  13. test.describe.configure({ mode: 'serial' })
  14. test.use({ trace: 'off', video: 'off' })
  15. const suffix = runPrefix.replace(/[^A-Za-z0-9]/g, '').slice(-14).toLowerCase()
  16. const data = {
  17. departmentName: `${runPrefix}-测试部门`,
  18. departmentCode: `E2E_${suffix.toUpperCase()}`.slice(0, 32),
  19. roleName: `${runPrefix}-测试角色`,
  20. roleCode: `e2e_${suffix}`.slice(0, 48),
  21. username: `e2e_${suffix}`.slice(0, 32),
  22. displayName: `${runPrefix}-学员`,
  23. editedDisplayName: `${runPrefix}-已编辑学员`,
  24. initialPassword: `Init@${suffix.slice(-6)}Aa1`,
  25. finalPassword: `Final@${suffix.slice(-5)}Bb2`,
  26. }
  27. const auth = (token: string) => ({ Authorization: `Bearer ${token}` })
  28. const record = (value: unknown) => value && typeof value === 'object' ? value as Record<string, unknown> : {}
  29. const records = (value: unknown) => {
  30. const root = record(envelopeData(value))
  31. const items = root.records ?? root.items
  32. return Array.isArray(items) ? items.map(record) : Array.isArray(envelopeData(value)) ? (envelopeData(value) as unknown[]).map(record) : []
  33. }
  34. async function json(response: Awaited<ReturnType<APIRequestContext['get']>>) {
  35. return response.json().catch(() => ({}))
  36. }
  37. async function cleanupCreatedData(request: APIRequestContext, token: string) {
  38. const result: Record<string, string> = {}
  39. const userList = await request.get('/api/auth/v1/users', { headers: auth(token), params: { keyword: data.username, size: 100 } })
  40. const user = records(await json(userList)).find((item) => String(item.username) === data.username)
  41. if (user) {
  42. const response = await request.delete(`/api/auth/v1/users/${String(user.id)}`, {
  43. headers: auth(token), data: { version: Number(user.version ?? user.dataVersion ?? 0) },
  44. })
  45. result.user = `${response.status()}`
  46. }
  47. const roleList = await request.get('/api/auth/v1/roles', { headers: auth(token), params: { keyword: data.roleCode, size: 100 } })
  48. const role = records(await json(roleList)).find((item) => String(item.code) === data.roleCode)
  49. if (role) {
  50. const response = await request.delete(`/api/auth/v1/roles/${String(role.id)}`, {
  51. headers: auth(token), data: { version: Number(role.version ?? role.dataVersion ?? 0) },
  52. })
  53. result.role = `${response.status()}`
  54. }
  55. const departmentsResponse = await request.get('/api/auth/v1/departments', { headers: auth(token) })
  56. const departmentValue = envelopeData(await json(departmentsResponse))
  57. const departments = Array.isArray(departmentValue) ? departmentValue.map(record) : []
  58. const department = departments.find((item) => String(item.code) === data.departmentCode)
  59. if (department) {
  60. const response = await request.post(`/api/auth/v1/departments/${String(department.id)}/migrate-and-delete`, {
  61. headers: auth(token), data: { version: Number(department.version ?? department.dataVersion ?? 0) },
  62. })
  63. result.department = `${response.status()}`
  64. }
  65. return result
  66. }
  67. async function fillInputByPlaceholder(page: Page, placeholder: string, value: string) {
  68. const input = page.getByPlaceholder(placeholder)
  69. await expect(input).toBeVisible()
  70. await input.fill(value)
  71. }
  72. async function logout(page: Page) {
  73. await page.locator('.user-menu-trigger').click()
  74. await page.getByRole('menuitem', { name: /退出登录/ }).click()
  75. await confirmMessageBox(page, '退出登录')
  76. await expect(page).toHaveURL(/\/login(?:\?|$)/)
  77. }
  78. async function selectStudentIdentity(page: Page) {
  79. await page.locator('[data-role-code="student"]').click()
  80. const picker = page.locator('.login-identity-picker')
  81. await expect(picker).toBeVisible()
  82. await picker.click()
  83. const option = page.locator('.login-identity-popper .el-select-dropdown__item').filter({ hasText: data.editedDisplayName }).first()
  84. await expect(option).toBeVisible()
  85. await option.click()
  86. }
  87. test('IAM 真实生命周期:部门、角色、用户、授权、首次改密和逆序清理', async ({ page, request }, testInfo) => {
  88. test.slow()
  89. const token = await adminAccessToken(request)
  90. await cleanupCreatedData(request, token)
  91. await loginAsAdmin(page)
  92. let primaryError: unknown
  93. let cleanup: Record<string, string> = {}
  94. try {
  95. await test.step('通过部门管理页面创建并持久化测试部门', async () => {
  96. await page.goto('/organization/departments')
  97. await page.getByRole('button', { name: '新增部门', exact: true }).click()
  98. const dialog = visibleDialog(page)
  99. await fillFormItem(dialog, '部门名称', data.departmentName)
  100. await fillFormItem(dialog, '部门编码', data.departmentCode)
  101. await fillFormItem(dialog, '部门职责', `${runPrefix} E2E 自动化部门,运行结束后删除`)
  102. await dialog.getByRole('button', { name: '创建部门' }).click()
  103. await fillInputByPlaceholder(page, '请输入部门、编码或负责人', data.departmentCode)
  104. await page.getByRole('button', { name: '搜索', exact: true }).click()
  105. await expect(tableRowByText(page, data.departmentName)).toBeVisible()
  106. await captureScreenshot(page, testInfo, '01-department-created')
  107. })
  108. await test.step('通过角色管理页面创建、编辑并持久化自定义角色', async () => {
  109. await page.goto('/organization/roles')
  110. await page.getByRole('button', { name: '新增角色', exact: true }).click()
  111. const dialog = visibleDialog(page)
  112. await fillFormItem(dialog, '角色名称', data.roleName)
  113. await fillFormItem(dialog, '角色编码', data.roleCode)
  114. await fillFormItem(dialog, '界面简称', '测')
  115. await fillFormItem(dialog, '角色说明', `${runPrefix} 创建测试`)
  116. await dialog.getByRole('button', { name: '创建角色' }).click()
  117. await fillInputByPlaceholder(page, '请输入角色名称、编码或说明', data.roleCode)
  118. await page.getByRole('button', { name: '搜索', exact: true }).click()
  119. const row = tableRowByText(page, data.roleName)
  120. await expect(row).toBeVisible()
  121. await row.getByRole('button', { name: '编辑' }).click()
  122. const editDialog = visibleDialog(page)
  123. await fillFormItem(editDialog, '角色说明', `${runPrefix} 已编辑并保存`)
  124. await editDialog.getByRole('button', { name: '保存修改' }).click()
  125. await expect(tableRowByText(page, '已编辑并保存')).toBeVisible()
  126. await captureScreenshot(page, testInfo, '02-role-created-edited')
  127. })
  128. await test.step('在权限页给临时角色保存一项真实权限', async () => {
  129. await page.goto('/organization/permissions')
  130. const roleButton = page.locator('.role-selector > button').filter({ hasText: data.roleName })
  131. await expect(roleButton).toBeVisible()
  132. await roleButton.click()
  133. const uncheckedPermission = page.locator('.permission-item[aria-checked="false"]').first()
  134. await expect(uncheckedPermission).toBeVisible()
  135. await uncheckedPermission.click()
  136. const save = page.getByRole('button', { name: '保存权限配置' })
  137. await expect(save).toBeEnabled()
  138. await save.click()
  139. await expect(page.getByText(/权限已保存并立即生效/).last()).toBeVisible()
  140. await captureScreenshot(page, testInfo, '03-permission-saved')
  141. })
  142. await test.step('通过用户管理页面创建学员账号并编辑姓名', async () => {
  143. await page.goto('/organization/users')
  144. await page.getByRole('button', { name: '新增用户', exact: true }).click()
  145. const dialog = visibleDialog(page)
  146. await fillFormItem(dialog, '姓名', data.displayName)
  147. await fillFormItem(dialog, '登录账号', data.username)
  148. await chooseSelectOption(page, dialog, '所属部门', data.departmentName)
  149. await fillFormItem(dialog, '初始密码', data.initialPassword)
  150. const studentRole = dialog.locator('.el-checkbox').filter({ hasText: '学员' }).first()
  151. await expect(studentRole).toBeVisible()
  152. await studentRole.click()
  153. await chooseSelectOption(page, dialog, '登录默认身份', '学员')
  154. await dialog.getByRole('button', { name: '创建用户' }).click()
  155. await fillInputByPlaceholder(page, '请输入姓名、账号、部门或角色', data.username)
  156. await page.getByRole('button', { name: '搜索', exact: true }).click()
  157. const row = tableRowByText(page, data.username)
  158. await expect(row).toBeVisible()
  159. await row.getByRole('button', { name: '编辑' }).click()
  160. const editDialog = visibleDialog(page)
  161. await fillFormItem(editDialog, '姓名', data.editedDisplayName)
  162. await editDialog.getByRole('button', { name: '保存修改' }).click()
  163. await expect(tableRowByText(page, data.editedDisplayName)).toBeVisible()
  164. await captureScreenshot(page, testInfo, '04-user-created-edited')
  165. })
  166. await test.step('学员按身份选择登录并完成首次强制改密', async () => {
  167. await logout(page)
  168. await selectStudentIdentity(page)
  169. await page.locator('input[name="password"]').fill(data.initialPassword)
  170. await page.getByRole('button', { name: '登录系统' }).click()
  171. await expect(page).toHaveURL(/\/change-password(?:\?|$)/)
  172. await expect(page.getByRole('heading', { name: '设置新密码' })).toBeVisible()
  173. await page.getByPlaceholder('请输入新密码').fill(data.finalPassword)
  174. await page.getByPlaceholder('请再次输入').fill(data.finalPassword)
  175. await captureScreenshot(page, testInfo, '05-initial-password-form', [page.locator('input[type="password"]')])
  176. await page.getByRole('button', { name: '保存并进入平台' }).click()
  177. await expect(page).toHaveURL(/\/overview(?:\?|$)/)
  178. await expect(page.locator('.user-menu-trigger')).toContainText(data.editedDisplayName)
  179. await captureScreenshot(page, testInfo, '06-student-login-success')
  180. await page.goto('/organization/users')
  181. await expect(page).toHaveURL(/\/overview\?forbidden=1$/)
  182. await expect(page.getByRole('heading', { name: '用户管理' })).toHaveCount(0)
  183. await captureScreenshot(page, testInfo, '07-student-system-route-forbidden')
  184. await logout(page)
  185. await loginAsAdmin(page)
  186. })
  187. await test.step('列表筛选、停启入口及重置密码两种模式可以交互', async () => {
  188. await page.goto('/organization/users')
  189. await fillInputByPlaceholder(page, '请输入姓名、账号、部门或角色', data.username)
  190. await page.getByRole('button', { name: '搜索', exact: true }).click()
  191. const row = tableRowByText(page, data.username)
  192. await row.getByRole('button', { name: '重置密码' }).click()
  193. const dialog = visibleDialog(page)
  194. await expect(dialog.getByRole('radio', { name: /系统重置/ })).toBeChecked()
  195. await dialog.getByText('手动重置', { exact: true }).click()
  196. await expect(dialog.getByPlaceholder('请输入符合全部规则的新密码')).toBeVisible()
  197. await captureScreenshot(page, testInfo, '07-password-reset-modes', [dialog.locator('input[type="password"]')])
  198. await dialog.getByRole('button', { name: '取消' }).click()
  199. })
  200. } catch (error) {
  201. primaryError = error
  202. } finally {
  203. cleanup = await cleanupCreatedData(request, token).catch((error) => ({ cleanupError: String(error) }))
  204. await attachJson(testInfo, 'cleanup-result', cleanup)
  205. }
  206. if (primaryError) throw primaryError
  207. expect(cleanup.user, '用户应完成清理').toMatch(/^2/)
  208. expect(cleanup.role, '角色应完成清理').toMatch(/^2/)
  209. expect(cleanup.department, '部门应完成清理').toMatch(/^2/)
  210. })