systemManagementDialog.test.ts 8.8 KB

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