You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

556 regels
24 KiB

  1. import type { APIRequestContext, APIResponse, Page, TestInfo } from '@playwright/test'
  2. import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures'
  3. import {
  4. adminAccessToken,
  5. confirmMessageBox,
  6. envelopeData,
  7. loginAsAdmin,
  8. tableRowByText,
  9. visibleDialog,
  10. } from './helpers'
  11. type JsonRecord = Record<string, unknown>
  12. type Headers = Record<string, string>
  13. interface PasswordActivity {
  14. module: string
  15. action: string
  16. result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED'
  17. httpStatus?: number
  18. resourceId?: string
  19. detail?: string
  20. }
  21. interface LoginSession {
  22. accessToken: string
  23. refreshToken: string
  24. mustChangePassword: boolean
  25. }
  26. interface StatusResponse {
  27. status: () => number
  28. }
  29. const suffix = runPrefix.replace(/[^A-Za-z0-9]/g, '').slice(-12).toLowerCase() || 'manual'
  30. const testUser = {
  31. username: `ape_pwd_${suffix}`.slice(0, 32),
  32. displayName: `${runPrefix}-密码安全学员`,
  33. creationPassword: `Start@Aa1${suffix.slice(-4)}`,
  34. baselinePassword: `Base@Bb2${suffix.slice(-4)}`,
  35. manualPassword: `Manual@Cc3${suffix.slice(-4)}`,
  36. afterManualPassword: `After@Dd4${suffix.slice(-4)}`,
  37. afterSystemPassword: `System@Ee5${suffix.slice(-4)}`,
  38. profilePassword: `Final@Ff6${suffix.slice(-4)}`,
  39. wrongCurrentPassword: `Wrong@Gg7${suffix.slice(-4)}`,
  40. mismatchPassword: `Match@Hh8${suffix.slice(-4)}`,
  41. mismatchConfirmation: `Other@Ii9${suffix.slice(-4)}`,
  42. }
  43. const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object'
  44. ? value as JsonRecord
  45. : {}
  46. const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value)
  47. ? value.map(asRecord)
  48. : []
  49. const auth = (token: string): Headers => ({ Authorization: `Bearer ${token}` })
  50. async function responseJson(response: APIResponse) {
  51. return response.json().catch(() => ({}))
  52. }
  53. function assertStatus(response: StatusResponse, expected: number | number[], label: string) {
  54. const allowed = Array.isArray(expected) ? expected : [expected]
  55. expect(allowed, `${label}:HTTP ${response.status()}`).toContain(response.status())
  56. }
  57. function recordPass(
  58. activities: PasswordActivity[],
  59. module: string,
  60. action: string,
  61. response: StatusResponse,
  62. resourceId?: string,
  63. detail?: string,
  64. ) {
  65. activities.push({ module, action, result: 'PASS', httpStatus: response.status(), resourceId, detail })
  66. }
  67. function pageRecords(value: unknown) {
  68. const data = envelopeData(value)
  69. const root = asRecord(data)
  70. const items = root.records ?? root.items
  71. return Array.isArray(items) ? items.map(asRecord) : asRecords(data)
  72. }
  73. async function findUser(request: APIRequestContext, headers: Headers) {
  74. const response = await request.get('/api/auth/v1/users', {
  75. headers,
  76. params: { keyword: testUser.username, page: 1, size: 100 },
  77. })
  78. if (response.status() !== 200) return null
  79. return pageRecords(await responseJson(response))
  80. .find((item) => String(item.username ?? '') === testUser.username) ?? null
  81. }
  82. async function cleanupUser(
  83. request: APIRequestContext,
  84. headers: Headers,
  85. activities: PasswordActivity[],
  86. cleanupErrors: string[],
  87. ) {
  88. try {
  89. let user = await findUser(request, headers)
  90. if (!user) return
  91. let response = await request.delete(`/api/auth/v1/users/${encodeURIComponent(String(user.id))}`, {
  92. headers,
  93. data: { version: Number(user.dataVersion ?? user.version ?? 0) },
  94. })
  95. if (response.status() === 409) {
  96. user = await findUser(request, headers)
  97. if (!user) return
  98. response = await request.delete(`/api/auth/v1/users/${encodeURIComponent(String(user.id))}`, {
  99. headers,
  100. data: { version: Number(user.dataVersion ?? user.version ?? 0) },
  101. })
  102. }
  103. if (![200, 204, 404].includes(response.status())) {
  104. throw new Error(`HTTP ${response.status()}`)
  105. }
  106. activities.push({
  107. module: '账号安全',
  108. action: '临时用户逆序清理',
  109. result: 'CLEANED',
  110. httpStatus: response.status(),
  111. resourceId: String(user.id),
  112. })
  113. } catch (error) {
  114. const detail = `临时用户清理失败:${error instanceof Error ? error.message : String(error)}`
  115. cleanupErrors.push(detail)
  116. activities.push({ module: '账号安全', action: '临时用户逆序清理', result: 'CLEANUP_FAILED', detail })
  117. }
  118. }
  119. async function createTemporaryStudent(
  120. request: APIRequestContext,
  121. headers: Headers,
  122. activities: PasswordActivity[],
  123. ) {
  124. const departmentsResponse = await request.get('/api/auth/v1/departments', { headers })
  125. assertStatus(departmentsResponse, 200, '读取可用部门')
  126. const departments = asRecords(envelopeData(await responseJson(departmentsResponse)))
  127. const department = departments.find((item) => item.enabled !== false && item.status !== 'disabled')
  128. expect(department, '测试环境必须至少存在一个启用部门').toBeTruthy()
  129. const rolesResponse = await request.get('/api/auth/v1/roles/all', { headers })
  130. assertStatus(rolesResponse, 200, '读取可用角色')
  131. const rolesValue = envelopeData(await responseJson(rolesResponse))
  132. const roles = Array.isArray(rolesValue) ? asRecords(rolesValue) : pageRecords(rolesValue)
  133. const studentRole = roles.find((item) => String(item.code ?? item.roleCode) === 'student' && item.enabled !== false)
  134. expect(studentRole, '测试环境必须存在启用的学员角色').toBeTruthy()
  135. const response = await request.post('/api/auth/v1/users', {
  136. headers,
  137. data: {
  138. username: testUser.username,
  139. displayName: testUser.displayName,
  140. departmentId: Number(department!.id),
  141. roleIds: [Number(studentRole!.id)],
  142. defaultRoleId: Number(studentRole!.id),
  143. enabled: true,
  144. password: testUser.creationPassword,
  145. remark: `${runPrefix} 账号安全真实提交测试,运行结束自动删除`,
  146. version: 0,
  147. },
  148. })
  149. assertStatus(response, 200, '创建临时学员账号')
  150. const created = asRecord(envelopeData(await responseJson(response)))
  151. const userId = String(created.id ?? '')
  152. expect(userId).not.toBe('')
  153. recordPass(activities, '账号安全', '管理员创建唯一临时学员账号', response, userId)
  154. return userId
  155. }
  156. async function formalStudentLogin(
  157. request: APIRequestContext,
  158. userId: string,
  159. password: string,
  160. expectedStatus = 200,
  161. ): Promise<{ response: APIResponse; session: LoginSession | null }> {
  162. const response = await request.post('/api/auth/v1/auth/login', {
  163. data: {
  164. userId: Number(userId),
  165. roleCode: 'student',
  166. password,
  167. rememberMe: false,
  168. },
  169. })
  170. assertStatus(response, expectedStatus, expectedStatus === 200 ? '学员身份登录' : '旧密码应被拒绝')
  171. if (response.status() !== 200) return { response, session: null }
  172. const data = asRecord(envelopeData(await responseJson(response)))
  173. const user = asRecord(data.user)
  174. const session = {
  175. accessToken: String(data.accessToken ?? ''),
  176. refreshToken: String(data.refreshToken ?? ''),
  177. mustChangePassword: Boolean(user.mustChangePassword ?? data.mustChangePassword),
  178. }
  179. expect(session.accessToken).not.toBe('')
  180. expect(session.refreshToken).not.toBe('')
  181. return { response, session }
  182. }
  183. async function completeInitialPassword(
  184. request: APIRequestContext,
  185. session: LoginSession,
  186. newPassword: string,
  187. ) {
  188. const response = await request.put('/api/auth/v1/auth/initial-password', {
  189. headers: auth(session.accessToken),
  190. data: { newPassword },
  191. })
  192. assertStatus(response, 200, '完成首次登录改密')
  193. const data = asRecord(envelopeData(await responseJson(response)))
  194. const user = asRecord(data.user)
  195. expect(Boolean(user.mustChangePassword)).toBeFalsy()
  196. const replacement: LoginSession = {
  197. accessToken: String(data.accessToken ?? ''),
  198. refreshToken: String(data.refreshToken ?? ''),
  199. mustChangePassword: false,
  200. }
  201. expect(replacement.accessToken).not.toBe('')
  202. return { response, session: replacement }
  203. }
  204. async function expectSessionRevoked(request: APIRequestContext, session: LoginSession, label: string) {
  205. const response = await request.get('/api/auth/v1/auth/me', { headers: auth(session.accessToken) })
  206. assertStatus(response, 401, label)
  207. return response
  208. }
  209. async function logoutCurrentUser(page: Page) {
  210. await page.locator('.user-menu-trigger').click()
  211. await page.getByRole('menuitem', { name: '退出登录' }).click()
  212. await confirmMessageBox(page, '退出登录')
  213. await expect(page).toHaveURL(/\/login(?:\?|$)/)
  214. }
  215. async function selectStudentOnLogin(page: Page) {
  216. await page.locator('[data-role-code="student"]').click()
  217. const picker = page.locator('.login-identity-picker')
  218. await expect(picker).toBeVisible()
  219. await expect(picker).not.toHaveClass(/is-disabled/)
  220. await picker.click()
  221. const option = page.locator('.login-identity-popper .el-select-dropdown__item')
  222. .filter({ hasText: testUser.displayName })
  223. .first()
  224. await expect(option).toBeVisible()
  225. await option.click()
  226. }
  227. async function loginStudentInBrowser(page: Page, password: string, expectedPath: RegExp) {
  228. await page.goto('/login')
  229. await selectStudentOnLogin(page)
  230. await page.locator('input[name="password"]').fill(password)
  231. const loginResponsePromise = page.waitForResponse((response) => (
  232. response.request().method() === 'POST'
  233. && new URL(response.url()).pathname === '/api/auth/v1/auth/login'
  234. ))
  235. await page.getByRole('button', { name: '登录系统' }).click()
  236. const response = await loginResponsePromise
  237. assertStatus(response, 200, '浏览器学员身份登录')
  238. await expect(page).toHaveURL(expectedPath)
  239. return response
  240. }
  241. async function completeInitialPasswordInBrowser(
  242. page: Page,
  243. testInfo: TestInfo,
  244. newPassword: string,
  245. screenshotLabel: string,
  246. ) {
  247. await expect(page.getByRole('heading', { name: '设置新密码' })).toBeVisible()
  248. await page.getByPlaceholder('请输入新密码').fill(newPassword)
  249. await page.getByPlaceholder('请再次输入').fill(newPassword)
  250. await captureScreenshot(page, testInfo, screenshotLabel, [
  251. page.locator('input[autocomplete="new-password"]'),
  252. ])
  253. const changedResponsePromise = page.waitForResponse((response) => (
  254. response.request().method() === 'PUT'
  255. && new URL(response.url()).pathname === '/api/auth/v1/auth/initial-password'
  256. ))
  257. await page.getByRole('button', { name: '保存并进入平台' }).click()
  258. const response = await changedResponsePromise
  259. assertStatus(response, 200, '浏览器完成首次改密')
  260. await expect(page).toHaveURL(/\/overview(?:\?|$)/)
  261. return response
  262. }
  263. async function openUserRow(page: Page) {
  264. await page.goto('/organization/users')
  265. const keyword = page.getByPlaceholder('请输入姓名、账号、部门或角色')
  266. await expect(keyword).toBeVisible()
  267. await keyword.fill(testUser.username)
  268. await page.getByRole('button', { name: '搜索', exact: true }).click()
  269. const row = tableRowByText(page, testUser.username)
  270. await expect(row).toBeVisible()
  271. return row
  272. }
  273. async function resetPasswordFromUserPage(
  274. page: Page,
  275. testInfo: TestInfo,
  276. userId: string,
  277. mode: 'MANUAL' | 'SYSTEM',
  278. manualPassword?: string,
  279. ) {
  280. const row = await openUserRow(page)
  281. await row.getByRole('button', { name: '重置密码' }).click()
  282. const resetDialog = visibleDialog(page)
  283. await expect(resetDialog.getByRole('heading', { name: '重置登录密码' })).toBeVisible()
  284. if (mode === 'MANUAL') {
  285. await resetDialog.getByText('手动重置', { exact: true }).click()
  286. const input = resetDialog.locator([
  287. 'input[data-testid="manual-reset-password-input"]',
  288. '[data-testid="manual-reset-password-input"] input',
  289. ].join(', ')).first()
  290. await input.fill(manualPassword ?? '')
  291. await expect(resetDialog.getByTestId('confirm-password-reset')).toBeEnabled()
  292. } else {
  293. await expect(resetDialog.getByTestId('password-reset-system-panel')).toBeVisible()
  294. }
  295. const resetResponsePromise = page.waitForResponse((response) => (
  296. response.request().method() === 'PUT'
  297. && new URL(response.url()).pathname === `/api/auth/v1/users/${userId}/password`
  298. ))
  299. await resetDialog.getByTestId('confirm-password-reset').click()
  300. const response = await resetResponsePromise
  301. assertStatus(response, 200, mode === 'MANUAL' ? '手动重置登录密码' : '系统随机重置登录密码')
  302. const resultDialog = visibleDialog(page)
  303. await expect(resultDialog.getByRole('heading', { name: '密码重置成功' })).toBeVisible()
  304. await expect(resultDialog.getByTestId('password-result-mode'))
  305. .toContainText(mode === 'MANUAL' ? '手动设置' : '系统生成')
  306. const temporaryPassword = (await resultDialog.getByTestId('generated-password').textContent())?.trim() ?? ''
  307. expect(temporaryPassword).not.toBe('')
  308. if (mode === 'MANUAL') expect(temporaryPassword).toBe(manualPassword)
  309. await captureScreenshot(
  310. page,
  311. testInfo,
  312. mode === 'MANUAL' ? '01-manual-reset-success-masked' : '04-system-reset-success-masked',
  313. [resultDialog.getByTestId('password-result-secret')],
  314. )
  315. await resultDialog.getByRole('button', { name: '我已妥善保存' }).click()
  316. await expect(resultDialog).toBeHidden()
  317. return { response, temporaryPassword }
  318. }
  319. async function attachLifecycleReport(
  320. testInfo: TestInfo,
  321. activities: PasswordActivity[],
  322. cleanupErrors: string[],
  323. ) {
  324. await attachJson(testInfo, '密码重置与个人中心生命周期及清理', {
  325. runId,
  326. runPrefix,
  327. activities,
  328. cleanupComplete: cleanupErrors.length === 0,
  329. cleanupErrors,
  330. security: '测试密码、一次性临时密码、访问令牌和刷新令牌仅保存在测试进程内存中;截图已遮罩敏感控件,报告未记录敏感值。',
  331. })
  332. }
  333. test.describe.configure({ mode: 'serial' })
  334. test.use({ trace: 'off', video: 'off' })
  335. test('手动与系统密码重置、首次改密、会话撤销和个人中心改密真实闭环', async ({ page, request }, testInfo) => {
  336. test.setTimeout(180_000)
  337. const activities: PasswordActivity[] = []
  338. const cleanupErrors: string[] = []
  339. const adminToken = await adminAccessToken(request)
  340. const adminHeaders = auth(adminToken)
  341. let userId = ''
  342. let primaryError: unknown
  343. await cleanupUser(request, adminHeaders, activities, cleanupErrors)
  344. try {
  345. userId = await createTemporaryStudent(request, adminHeaders, activities)
  346. const initialLogin = await formalStudentLogin(request, userId, testUser.creationPassword)
  347. expect(initialLogin.session?.mustChangePassword).toBeTruthy()
  348. recordPass(activities, '账号安全', '初始密码建立受限会话', initialLogin.response, userId)
  349. const baseline = await completeInitialPassword(request, initialLogin.session!, testUser.baselinePassword)
  350. recordPass(activities, '账号安全', '建立重置前正常密码与会话', baseline.response, userId)
  351. const baselineSession = baseline.session
  352. await loginAsAdmin(page)
  353. const manualReset = await resetPasswordFromUserPage(
  354. page,
  355. testInfo,
  356. userId,
  357. 'MANUAL',
  358. testUser.manualPassword,
  359. )
  360. recordPass(activities, '用户管理', '页面真实提交手动密码重置', manualReset.response, userId)
  361. const baselineRevoked = await expectSessionRevoked(request, baselineSession, '手动重置后旧会话必须失效')
  362. recordPass(activities, '账号安全', '手动重置撤销旧会话', baselineRevoked, userId)
  363. const oldBaselineLogin = await formalStudentLogin(request, userId, testUser.baselinePassword, 401)
  364. recordPass(activities, '账号安全', '手动重置后旧密码被拒绝', oldBaselineLogin.response, userId)
  365. const manualLogin = await formalStudentLogin(request, userId, manualReset.temporaryPassword)
  366. expect(manualLogin.session?.mustChangePassword).toBeTruthy()
  367. recordPass(activities, '账号安全', '手动临时密码仅建立首次改密会话', manualLogin.response, userId)
  368. await logoutCurrentUser(page)
  369. const manualBrowserLogin = await loginStudentInBrowser(page, manualReset.temporaryPassword, /\/change-password(?:\?|$)/)
  370. recordPass(activities, '登录与首次改密', '手动临时密码从身份选择页登录', manualBrowserLogin, userId)
  371. const manualInitialChange = await completeInitialPasswordInBrowser(
  372. page,
  373. testInfo,
  374. testUser.afterManualPassword,
  375. '02-manual-reset-first-change-masked',
  376. )
  377. recordPass(activities, '登录与首次改密', '手动重置后页面完成首次改密', manualInitialChange, userId)
  378. const manualTemporarySessionRevoked = await expectSessionRevoked(
  379. request,
  380. manualLogin.session!,
  381. '首次改密后临时密码会话必须失效',
  382. )
  383. recordPass(activities, '账号安全', '手动临时密码会话在首次改密后失效', manualTemporarySessionRevoked, userId)
  384. await captureScreenshot(page, testInfo, '03-manual-reset-normal-session-restored')
  385. const beforeSystemReset = await formalStudentLogin(request, userId, testUser.afterManualPassword)
  386. expect(beforeSystemReset.session?.mustChangePassword).toBeFalsy()
  387. await logoutCurrentUser(page)
  388. await loginAsAdmin(page)
  389. const systemReset = await resetPasswordFromUserPage(page, testInfo, userId, 'SYSTEM')
  390. recordPass(activities, '用户管理', '页面真实提交系统随机密码重置', systemReset.response, userId)
  391. const beforeSystemSessionRevoked = await expectSessionRevoked(
  392. request,
  393. beforeSystemReset.session!,
  394. '系统重置后旧会话必须失效',
  395. )
  396. recordPass(activities, '账号安全', '系统随机重置撤销旧会话', beforeSystemSessionRevoked, userId)
  397. const oldManualLogin = await formalStudentLogin(request, userId, testUser.afterManualPassword, 401)
  398. recordPass(activities, '账号安全', '系统随机重置后旧密码被拒绝', oldManualLogin.response, userId)
  399. const systemLogin = await formalStudentLogin(request, userId, systemReset.temporaryPassword)
  400. expect(systemLogin.session?.mustChangePassword).toBeTruthy()
  401. recordPass(activities, '账号安全', '系统随机临时密码仅建立首次改密会话', systemLogin.response, userId)
  402. await logoutCurrentUser(page)
  403. const systemBrowserLogin = await loginStudentInBrowser(page, systemReset.temporaryPassword, /\/change-password(?:\?|$)/)
  404. recordPass(activities, '登录与首次改密', '系统随机临时密码从身份选择页登录', systemBrowserLogin, userId)
  405. const systemInitialChange = await completeInitialPasswordInBrowser(
  406. page,
  407. testInfo,
  408. testUser.afterSystemPassword,
  409. '05-system-reset-first-change-masked',
  410. )
  411. recordPass(activities, '登录与首次改密', '系统随机重置后页面完成首次改密', systemInitialChange, userId)
  412. const systemTemporarySessionRevoked = await expectSessionRevoked(
  413. request,
  414. systemLogin.session!,
  415. '系统首次改密后临时密码会话必须失效',
  416. )
  417. recordPass(activities, '账号安全', '系统临时密码会话在首次改密后失效', systemTemporarySessionRevoked, userId)
  418. await test.step('个人中心验证错误原密码、弱密码和确认不一致,再真实成功改密', async () => {
  419. await page.goto('/profile')
  420. await expect(page.getByRole('heading', { name: '个人中心' })).toBeVisible()
  421. const form = page.locator('.profile-password-form')
  422. const currentPassword = form.locator('input[autocomplete="current-password"]')
  423. const newPasswords = form.locator('input[autocomplete="new-password"]')
  424. const newPassword = newPasswords.nth(0)
  425. const confirmation = newPasswords.nth(1)
  426. const submit = form.getByRole('button', { name: '确认修改' })
  427. const masks = [currentPassword, newPasswords]
  428. await currentPassword.fill(testUser.wrongCurrentPassword)
  429. await newPassword.fill(testUser.profilePassword)
  430. await confirmation.fill(testUser.profilePassword)
  431. const wrongResponsePromise = page.waitForResponse((response) => (
  432. response.request().method() === 'PUT'
  433. && new URL(response.url()).pathname === '/api/auth/v1/auth/password'
  434. ))
  435. await submit.click()
  436. const wrongResponse = await wrongResponsePromise
  437. assertStatus(wrongResponse, 400, '个人中心错误原密码应被服务端拒绝')
  438. await expect(page.locator('.el-message').filter({ hasText: '原密码错误' }).last()).toBeVisible()
  439. recordPass(activities, '个人中心', '错误原密码真实提交并被服务端拒绝', wrongResponse, userId)
  440. await captureScreenshot(page, testInfo, '06-profile-wrong-current-password-masked', masks)
  441. let passwordRequestCount = 0
  442. const countPasswordRequest = (requestValue: { method: () => string; url: () => string }) => {
  443. if (requestValue.method() === 'PUT' && new URL(requestValue.url()).pathname === '/api/auth/v1/auth/password') {
  444. passwordRequestCount += 1
  445. }
  446. }
  447. page.on('request', countPasswordRequest)
  448. await currentPassword.fill(testUser.afterSystemPassword)
  449. await newPassword.fill('weak123')
  450. await confirmation.fill('weak123')
  451. const beforeWeak = passwordRequestCount
  452. await submit.click()
  453. await expect(page.locator('.el-message').filter({ hasText: '新密码至少 8 位' }).last()).toBeVisible()
  454. expect(passwordRequestCount, '弱密码应在页面校验阶段拦截').toBe(beforeWeak)
  455. activities.push({ module: '个人中心', action: '弱密码提交在页面校验阶段拦截', result: 'PASS', resourceId: userId })
  456. await captureScreenshot(page, testInfo, '07-profile-weak-password-masked', masks)
  457. await currentPassword.fill(testUser.afterSystemPassword)
  458. await newPassword.fill(testUser.mismatchPassword)
  459. await confirmation.fill(testUser.mismatchConfirmation)
  460. const beforeMismatch = passwordRequestCount
  461. await submit.click()
  462. await expect(page.locator('.el-message').filter({ hasText: '两次输入的新密码不一致' }).last()).toBeVisible()
  463. expect(passwordRequestCount, '确认密码不一致应在页面校验阶段拦截').toBe(beforeMismatch)
  464. activities.push({ module: '个人中心', action: '确认密码不一致在页面校验阶段拦截', result: 'PASS', resourceId: userId })
  465. await captureScreenshot(page, testInfo, '08-profile-confirmation-mismatch-masked', masks)
  466. const separateSession = await formalStudentLogin(request, userId, testUser.afterSystemPassword)
  467. expect(separateSession.session?.mustChangePassword).toBeFalsy()
  468. await currentPassword.fill(testUser.afterSystemPassword)
  469. await newPassword.fill(testUser.profilePassword)
  470. await confirmation.fill(testUser.profilePassword)
  471. const successResponsePromise = page.waitForResponse((response) => (
  472. response.request().method() === 'PUT'
  473. && new URL(response.url()).pathname === '/api/auth/v1/auth/password'
  474. ))
  475. await submit.click()
  476. const successResponse = await successResponsePromise
  477. assertStatus(successResponse, 200, '个人中心成功修改密码')
  478. await expect(page).toHaveURL(/\/login(?:\?|$)/)
  479. recordPass(activities, '个人中心', '正确原密码真实提交并修改成功', successResponse, userId)
  480. page.off('request', countPasswordRequest)
  481. const separateSessionRevoked = await expectSessionRevoked(
  482. request,
  483. separateSession.session!,
  484. '个人中心改密后其他会话必须失效',
  485. )
  486. recordPass(activities, '账号安全', '个人中心改密撤销其他会话', separateSessionRevoked, userId)
  487. const previousLogin = await formalStudentLogin(request, userId, testUser.afterSystemPassword, 401)
  488. recordPass(activities, '账号安全', '个人中心改密后旧密码被拒绝', previousLogin.response, userId)
  489. const restoredLogin = await formalStudentLogin(request, userId, testUser.profilePassword)
  490. expect(restoredLogin.session?.mustChangePassword).toBeFalsy()
  491. recordPass(activities, '账号安全', '个人中心新密码恢复正常可登录状态', restoredLogin.response, userId)
  492. const browserRestoredLogin = await loginStudentInBrowser(page, testUser.profilePassword, /\/overview(?:\?|$)/)
  493. recordPass(activities, '登录与首次改密', '最终新密码从身份选择页正常登录', browserRestoredLogin, userId)
  494. await captureScreenshot(page, testInfo, '09-profile-password-login-restored')
  495. })
  496. } catch (error) {
  497. primaryError = error
  498. } finally {
  499. await cleanupUser(request, adminHeaders, activities, cleanupErrors)
  500. await attachLifecycleReport(testInfo, activities, cleanupErrors)
  501. }
  502. if (primaryError) throw primaryError
  503. expect(cleanupErrors, '临时用户必须完成无残留清理').toEqual([])
  504. expect(await findUser(request, adminHeaders), '临时用户清理后不应残留').toBeNull()
  505. })