pushProtocol.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. import {
  3. getGroupPushSubscriptions,
  4. getPushCapabilities,
  5. getPushSystemStatus,
  6. savePushSystemConfig,
  7. setGroupPushSubscription,
  8. } from '@/api/push'
  9. import { clearApiSecurityContext, setApiCsrfToken } from '@/api/client'
  10. afterEach(() => {
  11. clearApiSecurityContext()
  12. vi.unstubAllGlobals()
  13. })
  14. describe('push client protocol', () => {
  15. it('uses the versioned push API and protects subscription mutations', async () => {
  16. const calls: Array<{ path: string; init?: RequestInit }> = []
  17. vi.stubGlobal('fetch', vi.fn(async (path: string, init?: RequestInit) => {
  18. calls.push({ path, init })
  19. const payload = path === '/api/app/push/v1/capabilities'
  20. ? { protocolVersion: 1, enabled: true, events: [], maximumSummaryCharacters: 180 }
  21. : path === '/api/app/push/v1/group-subscriptions'
  22. ? { subscriptions: [{ roomId: 'room-1', enabled: true }] }
  23. : path === '/api/app/system/push-status'
  24. ? {
  25. configured: true,
  26. healthy: true,
  27. registration_count: 1,
  28. pending_count: 0,
  29. source: 'file',
  30. editable: true,
  31. key_file: '/srv/secrets/AuthKey_TEST.p8',
  32. key_id: 'KEY123',
  33. team_id: 'TEAM123',
  34. environments: ['development', 'production'],
  35. warnings: [{ code: 'apns_key_permissions', message: '建议调整为 0600', actual_mode: '0644', recommended_mode: '0600' }],
  36. }
  37. : path === '/api/app/system/push-config'
  38. ? {
  39. configured: true,
  40. healthy: true,
  41. registrationCount: 1,
  42. pendingCount: 0,
  43. source: 'file',
  44. editable: true,
  45. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  46. keyId: 'KEY123',
  47. teamId: 'TEAM123',
  48. topic: 'cn.samien.yaoyao.hermes',
  49. environments: ['development', 'production'],
  50. warnings: [],
  51. }
  52. : { subscription: { roomId: 'room-1', enabled: false } }
  53. return new Response(JSON.stringify(payload), {
  54. status: 200,
  55. headers: { 'content-type': 'application/json' },
  56. })
  57. }))
  58. setApiCsrfToken('csrf-push')
  59. await expect(getPushCapabilities()).resolves.toMatchObject({ protocolVersion: 1, enabled: true })
  60. await expect(getGroupPushSubscriptions()).resolves.toEqual(new Set(['room-1']))
  61. await expect(setGroupPushSubscription('room-1', false)).resolves.toBe(false)
  62. await expect(getPushSystemStatus()).resolves.toMatchObject({
  63. configured: true,
  64. registrationCount: 1,
  65. source: 'file',
  66. editable: true,
  67. managementAvailable: true,
  68. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  69. environments: ['development', 'production'],
  70. warnings: [{ code: 'apns_key_permissions', actualMode: '0644', recommendedMode: '0600' }],
  71. })
  72. await expect(savePushSystemConfig({
  73. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  74. keyId: 'KEY123',
  75. teamId: 'TEAM123',
  76. topic: 'cn.samien.yaoyao.hermes',
  77. environments: ['development', 'production'],
  78. })).resolves.toMatchObject({ configured: true, source: 'file', editable: true })
  79. expect(calls.map(call => call.path)).toEqual([
  80. '/api/app/push/v1/capabilities',
  81. '/api/app/push/v1/group-subscriptions',
  82. '/api/app/push/v1/group-subscriptions/room-1',
  83. '/api/app/system/push-status',
  84. '/api/app/system/push-config',
  85. ])
  86. expect(new Headers(calls[2]!.init?.headers).get('X-CSRF-Token')).toBe('csrf-push')
  87. expect(new Headers(calls[4]!.init?.headers).get('X-CSRF-Token')).toBe('csrf-push')
  88. expect(calls[4]!.init?.body).toBe(JSON.stringify({
  89. keyFile: '/srv/secrets/AuthKey_TEST.p8',
  90. keyId: 'KEY123',
  91. teamId: 'TEAM123',
  92. topic: 'cn.samien.yaoyao.hermes',
  93. environments: ['development', 'production'],
  94. }))
  95. })
  96. it('keeps an old status response read-only instead of guessing its configuration source', async () => {
  97. vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
  98. configured: true,
  99. healthy: true,
  100. registrationCount: 2,
  101. pendingCount: 0,
  102. topic: 'cn.samien.yaoyao.hermes',
  103. }), { status: 200, headers: { 'content-type': 'application/json' } })))
  104. await expect(getPushSystemStatus()).resolves.toMatchObject({
  105. configured: true,
  106. source: 'none',
  107. editable: false,
  108. managementAvailable: false,
  109. environments: [],
  110. warnings: [],
  111. })
  112. })
  113. it('normalizes startup configuration errors as the visible push error', async () => {
  114. vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
  115. configured: false,
  116. healthy: false,
  117. source: 'environment',
  118. editable: false,
  119. configuration_error: 'APNs key is invalid',
  120. registration_count: 1,
  121. pending_count: 0,
  122. }), { status: 200, headers: { 'content-type': 'application/json' } })))
  123. await expect(getPushSystemStatus()).resolves.toMatchObject({
  124. configured: false,
  125. source: 'environment',
  126. lastError: 'APNs key is invalid',
  127. })
  128. })
  129. })