rtm-manager-adapter.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
  2. import { RtmManagerAdapter } from '../../src/managers/rtm-manager-adapter'
  3. import { SttError } from '../../src/core/stt-error'
  4. import AgoraRTM from 'agora-rtm'
  5. // 模拟全局fetch
  6. global.fetch = vi.fn()
  7. // 模拟fetch响应
  8. const mockFetchResponse = (data: any) => {
  9. return {
  10. ok: true,
  11. json: async () => ({ data }),
  12. }
  13. }
  14. describe('RtmManagerAdapter', () => {
  15. let manager: RtmManagerAdapter
  16. const mockAppId = 'test-app-id'
  17. const mockCertificate = 'test-certificate'
  18. beforeEach(() => {
  19. manager = new RtmManagerAdapter(mockAppId, mockCertificate)
  20. })
  21. afterEach(async () => {
  22. if (manager.isJoined) {
  23. await manager.destroy()
  24. }
  25. })
  26. describe('join', () => {
  27. it('should join successfully with valid config', async () => {
  28. const config = {
  29. channel: 'test-channel',
  30. userId: 'test-user',
  31. userName: 'Test User',
  32. }
  33. // 模拟fetch返回token
  34. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  35. await manager.join(config)
  36. expect(manager.isJoined).toBe(true)
  37. expect(manager.config).toEqual(config)
  38. expect(manager.userId).toBe('test-user')
  39. expect(manager.channel).toBe('test-channel')
  40. expect(manager.userList).toHaveLength(1)
  41. expect(manager.userList[0]).toEqual({
  42. userId: 'test-user',
  43. userName: 'Test User',
  44. })
  45. // 验证 RTM 客户端被正确创建(包含token)
  46. expect(AgoraRTM.RTM).toHaveBeenCalledWith(mockAppId, 'test-user', { token: 'test-token' })
  47. })
  48. it('should throw error when config is invalid', async () => {
  49. const config = {
  50. channel: '',
  51. userId: '',
  52. userName: '',
  53. }
  54. await expect(manager.join(config)).rejects.toThrow(SttError)
  55. await expect(manager.join(config)).rejects.toThrow(
  56. 'Missing required configuration parameters'
  57. )
  58. })
  59. it('should throw error when already joined', async () => {
  60. const config = {
  61. channel: 'test-channel',
  62. userId: 'test-user',
  63. userName: 'Test User',
  64. }
  65. // 模拟fetch返回token
  66. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  67. await manager.join(config)
  68. await expect(manager.join(config)).rejects.toThrow(SttError)
  69. await expect(manager.join(config)).rejects.toThrow(
  70. 'RTM manager is already joined to a channel'
  71. )
  72. })
  73. it('should throw error when appId is not provided', async () => {
  74. const managerWithoutAppId = new RtmManagerAdapter()
  75. const config = {
  76. channel: 'test-channel',
  77. userId: 'test-user',
  78. userName: 'Test User',
  79. }
  80. await expect(managerWithoutAppId.join(config)).rejects.toThrow(SttError)
  81. await expect(managerWithoutAppId.join(config)).rejects.toThrow(
  82. 'App ID is required for RTM connection'
  83. )
  84. })
  85. it('should emit connected event', async () => {
  86. const connectedHandler = vi.fn()
  87. manager.on('connected', connectedHandler)
  88. const config = {
  89. channel: 'test-channel',
  90. userId: 'test-user',
  91. userName: 'Test User',
  92. }
  93. // 模拟fetch返回token
  94. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  95. await manager.join(config)
  96. expect(connectedHandler).toHaveBeenCalledTimes(1)
  97. expect(connectedHandler).toHaveBeenCalledWith({
  98. channel: 'test-channel',
  99. userId: 'test-user',
  100. })
  101. })
  102. it('should setup event listeners correctly', async () => {
  103. const config = {
  104. channel: 'test-channel',
  105. userId: 'test-user',
  106. userName: 'Test User',
  107. }
  108. // 模拟fetch返回token
  109. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  110. await manager.join(config)
  111. // 获取创建的 RTM 客户端实例
  112. const mockRTM = vi.mocked(AgoraRTM.RTM)
  113. const mockClient = mockRTM.mock.results[0].value
  114. expect(mockClient.addEventListener).toHaveBeenCalledWith('status', expect.any(Function))
  115. expect(mockClient.addEventListener).toHaveBeenCalledWith('presence', expect.any(Function))
  116. expect(mockClient.addEventListener).toHaveBeenCalledWith('storage', expect.any(Function))
  117. })
  118. })
  119. describe('updateSttData', () => {
  120. beforeEach(async () => {
  121. // 模拟fetch返回token
  122. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  123. await manager.join({
  124. channel: 'test-channel',
  125. userId: 'test-user',
  126. userName: 'Test User',
  127. })
  128. })
  129. it('should update STT data successfully', async () => {
  130. const updatingHandler = vi.fn()
  131. const updatedHandler = vi.fn()
  132. manager.on('metadataUpdating', updatingHandler)
  133. manager.on('metadataUpdated', updatedHandler)
  134. const data = {
  135. status: 'start',
  136. taskId: 'test-task-id',
  137. token: 'test-token',
  138. startTime: Date.now(),
  139. duration: 3600000,
  140. }
  141. await manager.updateSttData(data)
  142. expect(updatingHandler).toHaveBeenCalledTimes(1)
  143. expect(updatingHandler).toHaveBeenCalledWith({ data })
  144. expect(updatedHandler).toHaveBeenCalledTimes(1)
  145. expect(updatedHandler).toHaveBeenCalledWith({ data })
  146. // 验证存储方法被正确调用
  147. const mockRTM = vi.mocked(AgoraRTM.RTM)
  148. const mockClient = mockRTM.mock.results[0].value
  149. expect(mockClient.storage.setChannelMetadata).toHaveBeenCalledWith(
  150. 'test-channel',
  151. 'MESSAGE',
  152. expect.arrayContaining([
  153. { key: 'status', value: expect.any(String) },
  154. { key: 'taskId', value: expect.any(String) },
  155. { key: 'token', value: expect.any(String) },
  156. { key: 'startTime', value: expect.any(String) },
  157. { key: 'duration', value: expect.any(String) },
  158. ])
  159. )
  160. })
  161. it('should throw error when not joined', async () => {
  162. const unjoinedManager = new RtmManagerAdapter(mockAppId)
  163. const data = {
  164. status: 'start',
  165. taskId: 'test-task-id',
  166. }
  167. await expect(unjoinedManager.updateSttData(data)).rejects.toThrow(SttError)
  168. await expect(unjoinedManager.updateSttData(data)).rejects.toThrow(
  169. 'RTM manager must be joined to a channel before updating STT data'
  170. )
  171. })
  172. it('should handle empty data gracefully', async () => {
  173. // 重置存储方法的调用记录
  174. const mockRTM = vi.mocked(AgoraRTM.RTM)
  175. const mockClient = mockRTM.mock.results[0].value
  176. mockClient.storage.setChannelMetadata.mockClear()
  177. await manager.updateSttData({})
  178. // 验证存储方法没有被调用(因为数据为空)
  179. expect(mockClient.storage.setChannelMetadata).not.toHaveBeenCalled()
  180. })
  181. })
  182. describe('updateLanguages', () => {
  183. beforeEach(async () => {
  184. // 模拟fetch返回token
  185. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  186. await manager.join({
  187. channel: 'test-channel',
  188. userId: 'test-user',
  189. userName: 'Test User',
  190. })
  191. })
  192. it('should update languages successfully', async () => {
  193. const updatingHandler = vi.fn()
  194. const updatedHandler = vi.fn()
  195. manager.on('languagesUpdating', updatingHandler)
  196. manager.on('languagesUpdated', updatedHandler)
  197. const languages = [
  198. { source: 'en-US', target: ['zh-CN', 'ja-JP'] },
  199. { source: 'zh-CN', target: ['en-US'] },
  200. ]
  201. await manager.updateLanguages(languages)
  202. expect(updatingHandler).toHaveBeenCalledTimes(1)
  203. expect(updatingHandler).toHaveBeenCalledWith({ languages })
  204. expect(updatedHandler).toHaveBeenCalledTimes(1)
  205. expect(updatedHandler).toHaveBeenCalledWith({ languages })
  206. // 验证 updateSttData 被正确调用
  207. const mockRTM = vi.mocked(AgoraRTM.RTM)
  208. const mockClient = mockRTM.mock.results[0].value
  209. expect(mockClient.storage.setChannelMetadata).toHaveBeenCalledWith(
  210. 'test-channel',
  211. 'MESSAGE',
  212. expect.arrayContaining([
  213. { key: 'transcribe1', value: expect.any(String) },
  214. { key: 'translate1List', value: expect.any(String) },
  215. { key: 'transcribe2', value: expect.any(String) },
  216. { key: 'translate2List', value: expect.any(String) },
  217. ])
  218. )
  219. })
  220. it('should throw error when not joined', async () => {
  221. const unjoinedManager = new RtmManagerAdapter(mockAppId)
  222. const languages = [{ source: 'en-US' }]
  223. await expect(unjoinedManager.updateLanguages(languages)).rejects.toThrow(SttError)
  224. await expect(unjoinedManager.updateLanguages(languages)).rejects.toThrow(
  225. 'RTM manager must be joined to a channel before updating languages'
  226. )
  227. })
  228. it('should handle empty languages array', async () => {
  229. await manager.updateLanguages([])
  230. // 验证 updateSttData 被调用但数据为空
  231. const mockRTM = vi.mocked(AgoraRTM.RTM)
  232. const mockClient = mockRTM.mock.results[0].value
  233. expect(mockClient.storage.setChannelMetadata).toHaveBeenCalledWith(
  234. 'test-channel',
  235. 'MESSAGE',
  236. expect.arrayContaining([
  237. { key: 'transcribe1', value: '""' },
  238. { key: 'translate1List', value: '[]' },
  239. { key: 'transcribe2', value: '""' },
  240. { key: 'translate2List', value: '[]' },
  241. ])
  242. )
  243. })
  244. })
  245. describe('acquireLock', () => {
  246. beforeEach(async () => {
  247. // 模拟fetch返回token
  248. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  249. await manager.join({
  250. channel: 'test-channel',
  251. userId: 'test-user',
  252. userName: 'Test User',
  253. })
  254. })
  255. it('should acquire lock successfully', async () => {
  256. const acquiringHandler = vi.fn()
  257. const acquiredHandler = vi.fn()
  258. manager.on('lockAcquiring', acquiringHandler)
  259. manager.on('lockAcquired', acquiredHandler)
  260. await manager.acquireLock()
  261. expect(acquiringHandler).toHaveBeenCalledTimes(1)
  262. expect(acquiredHandler).toHaveBeenCalledTimes(1)
  263. // 验证锁方法被正确调用
  264. const mockRTM = vi.mocked(AgoraRTM.RTM)
  265. const mockClient = mockRTM.mock.results[0].value
  266. expect(mockClient.lock.acquireLock).toHaveBeenCalledWith(
  267. 'test-channel',
  268. 'MESSAGE',
  269. 'lock_stt'
  270. )
  271. })
  272. it('should throw error when not joined', async () => {
  273. const unjoinedManager = new RtmManagerAdapter(mockAppId)
  274. await expect(unjoinedManager.acquireLock()).rejects.toThrow(SttError)
  275. await expect(unjoinedManager.acquireLock()).rejects.toThrow(
  276. 'RTM manager must be joined to a channel before acquiring lock'
  277. )
  278. })
  279. })
  280. describe('releaseLock', () => {
  281. beforeEach(async () => {
  282. // 模拟fetch返回token
  283. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  284. await manager.join({
  285. channel: 'test-channel',
  286. userId: 'test-user',
  287. userName: 'Test User',
  288. })
  289. })
  290. it('should release lock successfully', async () => {
  291. const releasingHandler = vi.fn()
  292. const releasedHandler = vi.fn()
  293. manager.on('lockReleasing', releasingHandler)
  294. manager.on('lockReleased', releasedHandler)
  295. await manager.releaseLock()
  296. expect(releasingHandler).toHaveBeenCalledTimes(1)
  297. expect(releasedHandler).toHaveBeenCalledTimes(1)
  298. // 验证锁方法被正确调用
  299. const mockRTM = vi.mocked(AgoraRTM.RTM)
  300. const mockClient = mockRTM.mock.results[0].value
  301. expect(mockClient.lock.releaseLock).toHaveBeenCalledWith(
  302. 'test-channel',
  303. 'MESSAGE',
  304. 'lock_stt'
  305. )
  306. })
  307. it('should throw error when not joined', async () => {
  308. const unjoinedManager = new RtmManagerAdapter(mockAppId)
  309. await expect(unjoinedManager.releaseLock()).rejects.toThrow(SttError)
  310. await expect(unjoinedManager.releaseLock()).rejects.toThrow(
  311. 'RTM manager must be joined to a channel before releasing lock'
  312. )
  313. })
  314. })
  315. describe('destroy', () => {
  316. it('should destroy manager successfully', async () => {
  317. // 重置模拟调用次数
  318. vi.clearAllMocks()
  319. // 模拟fetch返回token
  320. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  321. await manager.join({
  322. channel: 'test-channel',
  323. userId: 'test-user',
  324. userName: 'Test User',
  325. })
  326. const destroyingHandler = vi.fn()
  327. const destroyedHandler = vi.fn()
  328. manager.on('destroying', destroyingHandler)
  329. manager.on('destroyed', destroyedHandler)
  330. await manager.destroy()
  331. expect(manager.isJoined).toBe(false)
  332. expect(manager.config).toBeUndefined()
  333. expect(manager.userList).toHaveLength(0)
  334. expect(destroyingHandler).toHaveBeenCalledTimes(1)
  335. expect(destroyedHandler).toHaveBeenCalledTimes(1)
  336. // 验证 RTM 客户端被正确销毁
  337. const mockRTM = vi.mocked(AgoraRTM.RTM)
  338. const mockClient = mockRTM.mock.results[0].value
  339. expect(mockClient.logout).toHaveBeenCalledTimes(1)
  340. })
  341. it('should handle destroy when not joined', async () => {
  342. await expect(manager.destroy()).resolves.not.toThrow()
  343. })
  344. it('should remove all event listeners', async () => {
  345. // 模拟fetch返回token
  346. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  347. await manager.join({
  348. channel: 'test-channel',
  349. userId: 'test-user',
  350. userName: 'Test User',
  351. })
  352. const testHandler = vi.fn()
  353. manager.on('connected', testHandler)
  354. await manager.destroy()
  355. // 验证事件监听器被移除
  356. manager.emit('connected', { channel: 'test', userId: 'test' })
  357. expect(testHandler).not.toHaveBeenCalled()
  358. })
  359. })
  360. describe('event handling', () => {
  361. beforeEach(async () => {
  362. // 模拟fetch返回token
  363. vi.mocked(fetch).mockResolvedValue(mockFetchResponse({ token: 'test-token' }) as any)
  364. await manager.join({
  365. channel: 'test-channel',
  366. userId: 'test-user',
  367. userName: 'Test User',
  368. })
  369. })
  370. it('should handle presence events correctly', async () => {
  371. const userListChangedHandler = vi.fn()
  372. manager.on('userListChanged', userListChangedHandler)
  373. // 模拟 presence 事件
  374. const mockRTM = vi.mocked(AgoraRTM.RTM)
  375. const mockClient = mockRTM.mock.results[0].value
  376. const presenceHandler = mockClient.eventHandlers?.presence
  377. if (presenceHandler) {
  378. // 模拟 SNAPSHOT 事件
  379. presenceHandler({
  380. channelName: 'test-channel',
  381. eventType: 'SNAPSHOT',
  382. snapshot: [
  383. {
  384. states: {
  385. type: 'UserInfo',
  386. userId: 'other-user',
  387. userName: 'Other User',
  388. },
  389. },
  390. ],
  391. })
  392. expect(userListChangedHandler).toHaveBeenCalledWith(
  393. expect.arrayContaining([
  394. { userId: 'test-user', userName: 'Test User' },
  395. { userId: 'other-user', userName: 'Other User' },
  396. ])
  397. )
  398. }
  399. })
  400. it('should handle storage events correctly', async () => {
  401. const languagesChangedHandler = vi.fn()
  402. const sttDataChangedHandler = vi.fn()
  403. manager.on('languagesChanged', languagesChangedHandler)
  404. manager.on('sttDataChanged', sttDataChangedHandler)
  405. // 模拟 storage 事件
  406. const mockRTM = vi.mocked(AgoraRTM.RTM)
  407. const mockClient = mockRTM.mock.results[0].value
  408. const storageHandler = mockClient.eventHandlers?.storage
  409. if (storageHandler) {
  410. // 模拟 UPDATE 事件
  411. storageHandler({
  412. channelName: 'test-channel',
  413. eventType: 'UPDATE',
  414. data: {
  415. metadata: {
  416. transcribe1: { value: '"en-US"' },
  417. translate1List: { value: '["zh-CN","ja-JP"]' },
  418. transcribe2: { value: '"zh-CN"' },
  419. translate2List: { value: '["en-US"]' },
  420. status: { value: '"start"' },
  421. taskId: { value: '"test-task-id"' },
  422. },
  423. },
  424. })
  425. expect(languagesChangedHandler).toHaveBeenCalledWith({
  426. transcribe1: 'en-US',
  427. translate1List: ['zh-CN', 'ja-JP'],
  428. transcribe2: 'zh-CN',
  429. translate2List: ['en-US'],
  430. })
  431. expect(sttDataChangedHandler).toHaveBeenCalledWith({
  432. status: 'start',
  433. taskId: 'test-task-id',
  434. })
  435. }
  436. })
  437. })
  438. })