accountSecurityPanel.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { flushPromises, mount } from '@vue/test-utils'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import AccountSecurityDialog from '@/components/app/AccountSecurityDialog.vue'
  4. import AccountSecurityPanel from '@/components/app/AccountSecurityPanel.vue'
  5. const auth = vi.hoisted(() => ({
  6. user: { id: 'admin-1', username: 'owner', role: 'admin' } as { id: string; username: string; role: string; avatar?: string } | undefined,
  7. changeCredentials: vi.fn(async () => undefined),
  8. updateAccountAvatar: vi.fn(async (avatar: string | null) => { if (auth.user) { if (avatar) auth.user.avatar=avatar; else delete auth.user.avatar } }),
  9. logout: vi.fn(async () => undefined),
  10. }))
  11. vi.mock('@/stores/auth', () => ({ useAuthStore: () => auth }))
  12. async function fill(wrapper: ReturnType<typeof mount>, name: string, value: string) {
  13. await wrapper.get<HTMLInputElement>(`input[name="${name}"]`).setValue(value)
  14. }
  15. beforeEach(() => {
  16. auth.user = { id: 'admin-1', username: 'owner', role: 'admin' }
  17. auth.changeCredentials.mockResolvedValue(undefined)
  18. auth.updateAccountAvatar.mockClear()
  19. })
  20. afterEach(() => {
  21. document.body.innerHTML = ''
  22. vi.clearAllMocks()
  23. })
  24. describe('Account security panel', () => {
  25. it('saves the trimmed administrator username and resets its dirty state', async () => {
  26. const wrapper = mount(AccountSecurityPanel, { props: { active: true } })
  27. expect(wrapper.text()).toContain('不会更改 9119 服务账号')
  28. expect(wrapper.emitted('dirty-change')?.at(-1)).toEqual([false])
  29. await fill(wrapper, 'username', ' owner-two ')
  30. await fill(wrapper, 'current-password', 'current-secret')
  31. await fill(wrapper, 'new-password', 'new-secret-123')
  32. await fill(wrapper, 'password-confirmation', 'new-secret-123')
  33. expect(wrapper.text()).toContain('两次输入一致')
  34. expect(wrapper.emitted('dirty-change')?.some(event => event[0] === true)).toBe(true)
  35. await wrapper.get('form').trigger('submit')
  36. await flushPromises()
  37. expect(auth.changeCredentials).toHaveBeenCalledWith({
  38. currentPassword: 'current-secret',
  39. newPassword: 'new-secret-123',
  40. username: 'owner-two',
  41. })
  42. expect(wrapper.emitted('saved')).toHaveLength(1)
  43. expect(wrapper.emitted('dirty-change')?.at(-1)).toEqual([false])
  44. expect(wrapper.get<HTMLInputElement>('input[name="current-password"]').element.value).toBe('')
  45. expect(wrapper.text()).toContain('账号与密码已更新')
  46. })
  47. it('does not expose or submit an administrator username for a regular user', async () => {
  48. auth.user = { id: 'user-1', username: 'member', role: 'user' }
  49. const wrapper = mount(AccountSecurityPanel, { props: { active: true } })
  50. expect(wrapper.find('input[name="username"]').exists()).toBe(false)
  51. await fill(wrapper, 'current-password', 'current-secret')
  52. await fill(wrapper, 'new-password', 'member-secret-123')
  53. await fill(wrapper, 'password-confirmation', 'member-secret-123')
  54. await wrapper.get('form').trigger('submit')
  55. await flushPromises()
  56. expect(auth.changeCredentials).toHaveBeenCalledWith({
  57. currentPassword: 'current-secret',
  58. newPassword: 'member-secret-123',
  59. })
  60. })
  61. it('resets when activated and exposes logout as an independent action', async () => {
  62. const wrapper = mount(AccountSecurityPanel, { props: { active: false } })
  63. await fill(wrapper, 'current-password', 'discard-me')
  64. expect(wrapper.emitted('dirty-change')?.at(-1)).toEqual([true])
  65. await wrapper.setProps({ active: true })
  66. expect(wrapper.get<HTMLInputElement>('input[name="current-password"]').element.value).toBe('')
  67. expect(wrapper.emitted('dirty-change')?.at(-1)).toEqual([false])
  68. await wrapper.get<HTMLButtonElement>('.logout-button').trigger('click')
  69. expect(wrapper.emitted('logout')).toHaveLength(1)
  70. expect(auth.logout).not.toHaveBeenCalled()
  71. })
  72. it('restores the account to its generated initial avatar independently', async () => {
  73. auth.user = { id: 'admin-1', username: 'owner', role: 'admin', avatar: 'data:image/png;base64,aGVsbG8=' }
  74. const wrapper = mount(AccountSecurityPanel, { props: { active: true } })
  75. expect(wrapper.get('.account-initial-avatar img').attributes('src')).toBe(auth.user.avatar)
  76. await wrapper.get('.account-avatar-card button:last-of-type').trigger('click')
  77. await flushPromises()
  78. expect(auth.updateAccountAvatar).toHaveBeenCalledWith(null)
  79. expect(wrapper.get('.account-initial-avatar').text()).toBe('O')
  80. })
  81. it('keeps the legacy dialog wrapper operational', async () => {
  82. const wrapper = mount(AccountSecurityDialog, {
  83. attachTo: document.body,
  84. props: { open: true },
  85. })
  86. expect(document.querySelector('[role="dialog"][aria-label="账号安全"]')).not.toBeNull()
  87. document.querySelector<HTMLButtonElement>('.logout-button')!.click()
  88. await flushPromises()
  89. expect(auth.logout).toHaveBeenCalledOnce()
  90. expect(wrapper.emitted('close')).toHaveLength(1)
  91. wrapper.unmount()
  92. })
  93. })