systemManagementDialog.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { nextTick } from 'vue'
  4. import SystemManagementDialog from '@/components/app/SystemManagementDialog.vue'
  5. import { listUsers } from '@/api/admin'
  6. import { getPushSystemStatus, savePushSystemConfig, type PushSystemStatus } from '@/api/push'
  7. vi.mock('@/api/admin', () => ({
  8. createUser: vi.fn(async () => undefined),
  9. deleteUser: vi.fn(async () => undefined),
  10. listUsers: vi.fn(async () => []),
  11. setUpstreamCredentials: vi.fn(async () => undefined),
  12. updateUser: vi.fn(async () => undefined),
  13. }))
  14. vi.mock('@/api/push', () => ({
  15. getPushSystemStatus: vi.fn(),
  16. savePushSystemConfig: vi.fn(),
  17. }))
  18. function pushStatus(overrides: Partial<PushSystemStatus> = {}): PushSystemStatus {
  19. return {
  20. configured: false,
  21. healthy: false,
  22. topic: 'cn.samien.yaoyao.hermes',
  23. registrationCount: 0,
  24. pendingCount: 0,
  25. source: 'none',
  26. editable: true,
  27. managementAvailable: true,
  28. environments: ['development', 'production'],
  29. warnings: [],
  30. ...overrides,
  31. }
  32. }
  33. async function mountOpen(): Promise<VueWrapper> {
  34. const wrapper = mount(SystemManagementDialog, {
  35. attachTo: document.body,
  36. props: { open: false },
  37. global: { stubs: { AppIcon: true } },
  38. })
  39. await wrapper.setProps({ open: true })
  40. await flushPromises()
  41. return wrapper
  42. }
  43. function input(name: string): HTMLInputElement {
  44. return document.querySelector<HTMLInputElement>(`input[name="${name}"]`)!
  45. }
  46. function enter(name: string, value: string) {
  47. const element = input(name)
  48. element.value = value
  49. element.dispatchEvent(new Event('input'))
  50. }
  51. function button(label: string): HTMLButtonElement | undefined {
  52. return [...document.querySelectorAll<HTMLButtonElement>('button')]
  53. .find(element => element.textContent?.includes(label))
  54. }
  55. beforeEach(() => {
  56. vi.mocked(listUsers).mockResolvedValue([])
  57. vi.mocked(getPushSystemStatus).mockResolvedValue(pushStatus())
  58. vi.mocked(savePushSystemConfig).mockResolvedValue(pushStatus({ configured: true, healthy: true, source: 'file' }))
  59. })
  60. afterEach(() => {
  61. document.body.innerHTML = ''
  62. vi.clearAllMocks()
  63. })
  64. describe('System management APNs configuration', () => {
  65. it('sends a server-local path and both environments through validation and enablement', async () => {
  66. vi.mocked(getPushSystemStatus).mockResolvedValueOnce(pushStatus({ environments: [] }))
  67. vi.mocked(savePushSystemConfig).mockResolvedValueOnce(pushStatus({
  68. configured: true,
  69. healthy: true,
  70. source: 'file',
  71. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  72. keyId: 'KEY123',
  73. teamId: 'TEAM123',
  74. warnings: [{
  75. code: 'apns_key_permissions',
  76. message: 'APNs 已启用,但密钥文件权限为 0644;建议调整为 0600,此提示不会影响当前推送。',
  77. actualMode: '0644',
  78. recommendedMode: '0600',
  79. }],
  80. }))
  81. const wrapper = await mountOpen()
  82. enter('push-key-file', ' /srv/secrets/AuthKey_TEST.p8 ')
  83. enter('push-key-id', ' KEY123 ')
  84. enter('push-team-id', ' TEAM123 ')
  85. enter('push-topic', ' cn.samien.yaoyao.hermes ')
  86. await nextTick()
  87. const save = button('验证并启用')!
  88. expect(save.disabled).toBe(false)
  89. save.click()
  90. await flushPromises()
  91. expect(savePushSystemConfig).toHaveBeenCalledWith({
  92. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  93. keyId: 'KEY123',
  94. teamId: 'TEAM123',
  95. topic: 'cn.samien.yaoyao.hermes',
  96. environments: ['development', 'production'],
  97. })
  98. expect(document.body.textContent).toContain('Sandbox 与 Production 验证通过,APNs 已启用')
  99. expect(document.querySelector('.push-warning')?.textContent).toContain('权限为 0644')
  100. expect(button('验证并启用')?.disabled).toBe(false)
  101. wrapper.unmount()
  102. })
  103. it('shows environment-managed configuration as read-only', async () => {
  104. vi.mocked(getPushSystemStatus).mockResolvedValueOnce(pushStatus({
  105. configured: true,
  106. healthy: true,
  107. source: 'environment',
  108. editable: false,
  109. keyFile: '/Users/test/.hermes/secrets/AuthKey_TEST.p8',
  110. keyId: 'KEY123',
  111. teamId: 'TEAM123',
  112. }))
  113. const wrapper = await mountOpen()
  114. expect(document.body.textContent).toContain('由服务环境变量管理,Web 仅可查看')
  115. expect(input('push-key-file').readOnly).toBe(true)
  116. expect(document.querySelector<HTMLFieldSetElement>('.push-config-form fieldset')?.disabled).toBe(true)
  117. expect(button('验证并启用')).toBeUndefined()
  118. document.querySelector<HTMLFormElement>('.push-config-form')!.dispatchEvent(new Event('submit'))
  119. await nextTick()
  120. expect(savePushSystemConfig).not.toHaveBeenCalled()
  121. wrapper.unmount()
  122. })
  123. it('surfaces an invalid startup configuration instead of calling it unconfigured', async () => {
  124. vi.mocked(getPushSystemStatus).mockResolvedValueOnce(pushStatus({
  125. configured: false,
  126. healthy: false,
  127. source: 'environment',
  128. editable: false,
  129. lastError: 'APNs key file must contain an ES256 private key',
  130. }))
  131. const wrapper = await mountOpen()
  132. expect(document.body.textContent).toContain('APNs 配置无效:APNs key file must contain an ES256 private key')
  133. wrapper.unmount()
  134. })
  135. it('keeps an old status-only response safe and does not expose editing controls', async () => {
  136. vi.mocked(getPushSystemStatus).mockResolvedValueOnce(pushStatus({
  137. configured: true,
  138. healthy: true,
  139. managementAvailable: false,
  140. editable: false,
  141. }))
  142. const wrapper = await mountOpen()
  143. expect(document.body.textContent).toContain('APNs 推送服务正常')
  144. expect(document.body.textContent).toContain('当前服务版本只提供推送状态')
  145. expect(document.querySelector('.push-config-form')).toBeNull()
  146. expect(savePushSystemConfig).not.toHaveBeenCalled()
  147. wrapper.unmount()
  148. })
  149. it('rejects an empty environment set locally and preserves input after a server error', async () => {
  150. vi.mocked(getPushSystemStatus).mockResolvedValueOnce(pushStatus({
  151. source: 'file',
  152. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  153. keyId: 'KEY123',
  154. teamId: 'TEAM123',
  155. environments: ['development'],
  156. }))
  157. const wrapper = await mountOpen()
  158. const development = document.querySelector<HTMLInputElement>('input[type="checkbox"][value="development"]')!
  159. development.checked = false
  160. development.dispatchEvent(new Event('change'))
  161. await nextTick()
  162. document.querySelector<HTMLFormElement>('.push-config-form')!.dispatchEvent(new Event('submit'))
  163. await nextTick()
  164. expect(document.querySelector('[role="alert"]')?.textContent).toContain('至少选择 Sandbox 或 Production')
  165. expect(savePushSystemConfig).not.toHaveBeenCalled()
  166. development.checked = true
  167. development.dispatchEvent(new Event('change'))
  168. await nextTick()
  169. vi.mocked(savePushSystemConfig).mockRejectedValueOnce(new Error('8800 无法读取该密钥文件,请检查路径和服务账户权限'))
  170. button('验证并启用')!.click()
  171. await flushPromises()
  172. expect(document.querySelector('[role="alert"]')?.textContent).toContain('8800 无法读取该密钥文件')
  173. expect(input('push-key-file').value).toBe('/srv/secrets/AuthKey_TEST.p8')
  174. wrapper.unmount()
  175. })
  176. })