@app-connect/core 1.7.25 → 1.7.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -31
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -164
  6. package/connector/proxy/index.js +500 -500
  7. package/connector/registry.js +252 -252
  8. package/docs/README.md +50 -50
  9. package/docs/architecture.md +93 -93
  10. package/docs/connectors.md +116 -116
  11. package/docs/handlers.md +125 -125
  12. package/docs/libraries.md +101 -101
  13. package/docs/models.md +144 -144
  14. package/docs/routes.md +115 -115
  15. package/docs/tests.md +73 -73
  16. package/handlers/admin.js +523 -523
  17. package/handlers/appointment.js +193 -0
  18. package/handlers/auth.js +296 -296
  19. package/handlers/calldown.js +99 -99
  20. package/handlers/contact.js +280 -280
  21. package/handlers/disposition.js +82 -80
  22. package/handlers/log.js +984 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/plugin.js +208 -208
  25. package/handlers/user.js +142 -142
  26. package/index.js +3140 -2652
  27. package/jest.config.js +56 -56
  28. package/lib/analytics.js +54 -54
  29. package/lib/authSession.js +109 -109
  30. package/lib/cacheCleanup.js +21 -0
  31. package/lib/callLogComposer.js +898 -898
  32. package/lib/callLogLookup.js +34 -0
  33. package/lib/constants.js +8 -8
  34. package/lib/debugTracer.js +177 -177
  35. package/lib/encode.js +30 -30
  36. package/lib/errorHandler.js +218 -206
  37. package/lib/generalErrorMessage.js +41 -41
  38. package/lib/jwt.js +18 -18
  39. package/lib/logger.js +190 -190
  40. package/lib/migrateCallLogsSchema.js +116 -0
  41. package/lib/ringcentral.js +266 -266
  42. package/lib/s3ErrorLogReport.js +65 -65
  43. package/lib/sharedSMSComposer.js +471 -471
  44. package/lib/util.js +67 -67
  45. package/mcp/README.md +412 -395
  46. package/mcp/lib/validator.js +91 -91
  47. package/mcp/mcpHandler.js +425 -425
  48. package/mcp/tools/cancelAppointment.js +101 -0
  49. package/mcp/tools/checkAuthStatus.js +105 -105
  50. package/mcp/tools/confirmAppointment.js +101 -0
  51. package/mcp/tools/createAppointment.js +157 -0
  52. package/mcp/tools/createCallLog.js +327 -316
  53. package/mcp/tools/createContact.js +117 -117
  54. package/mcp/tools/createMessageLog.js +287 -287
  55. package/mcp/tools/doAuth.js +60 -60
  56. package/mcp/tools/findContactByName.js +93 -93
  57. package/mcp/tools/findContactByPhone.js +101 -101
  58. package/mcp/tools/getCallLog.js +111 -102
  59. package/mcp/tools/getGoogleFilePicker.js +99 -99
  60. package/mcp/tools/getHelp.js +43 -43
  61. package/mcp/tools/getPublicConnectors.js +94 -94
  62. package/mcp/tools/getSessionInfo.js +90 -90
  63. package/mcp/tools/index.js +51 -41
  64. package/mcp/tools/listAppointments.js +163 -0
  65. package/mcp/tools/logout.js +96 -96
  66. package/mcp/tools/rcGetCallLogs.js +65 -65
  67. package/mcp/tools/updateAppointment.js +154 -0
  68. package/mcp/tools/updateCallLog.js +130 -126
  69. package/mcp/ui/App/App.tsx +358 -358
  70. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  71. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  72. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  73. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  74. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  75. package/mcp/ui/App/lib/callTool.ts +130 -130
  76. package/mcp/ui/App/lib/debugLog.ts +41 -41
  77. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  78. package/mcp/ui/App/main.css +5 -5
  79. package/mcp/ui/App/root.tsx +13 -13
  80. package/mcp/ui/index.html +13 -13
  81. package/mcp/ui/package-lock.json +6356 -6356
  82. package/mcp/ui/package.json +25 -25
  83. package/mcp/ui/tsconfig.json +26 -26
  84. package/mcp/ui/vite.config.ts +16 -16
  85. package/models/accountDataModel.js +33 -33
  86. package/models/adminConfigModel.js +35 -35
  87. package/models/cacheModel.js +30 -26
  88. package/models/callDownListModel.js +34 -34
  89. package/models/callLogModel.js +33 -27
  90. package/models/dynamo/connectorSchema.js +146 -146
  91. package/models/dynamo/lockSchema.js +24 -24
  92. package/models/dynamo/noteCacheSchema.js +29 -29
  93. package/models/llmSessionModel.js +17 -17
  94. package/models/messageLogModel.js +25 -25
  95. package/models/sequelize.js +16 -16
  96. package/models/userModel.js +45 -45
  97. package/package.json +1 -1
  98. package/releaseNotes.json +1093 -1081
  99. package/test/connector/proxy/engine.test.js +126 -126
  100. package/test/connector/proxy/index.test.js +279 -279
  101. package/test/connector/proxy/sample.json +161 -161
  102. package/test/connector/registry.test.js +415 -415
  103. package/test/handlers/admin.test.js +616 -616
  104. package/test/handlers/auth.test.js +1018 -1018
  105. package/test/handlers/contact.test.js +1014 -1014
  106. package/test/handlers/log.test.js +1298 -1160
  107. package/test/handlers/managedAuth.test.js +457 -457
  108. package/test/handlers/plugin.test.js +380 -380
  109. package/test/index.test.js +105 -105
  110. package/test/lib/cacheCleanup.test.js +42 -0
  111. package/test/lib/callLogComposer.test.js +1231 -1231
  112. package/test/lib/debugTracer.test.js +328 -328
  113. package/test/lib/jwt.test.js +176 -176
  114. package/test/lib/logger.test.js +206 -206
  115. package/test/lib/oauth.test.js +359 -359
  116. package/test/lib/ringcentral.test.js +467 -467
  117. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  118. package/test/lib/util.test.js +329 -329
  119. package/test/mcp/tools/checkAuthStatus.test.js +83 -83
  120. package/test/mcp/tools/createCallLog.test.js +436 -436
  121. package/test/mcp/tools/createContact.test.js +58 -58
  122. package/test/mcp/tools/createMessageLog.test.js +595 -595
  123. package/test/mcp/tools/doAuth.test.js +113 -113
  124. package/test/mcp/tools/findContactByName.test.js +275 -275
  125. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  126. package/test/mcp/tools/getCallLog.test.js +298 -298
  127. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  128. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  129. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  130. package/test/mcp/tools/logout.test.js +233 -233
  131. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  132. package/test/mcp/tools/updateCallLog.test.js +360 -360
  133. package/test/models/accountDataModel.test.js +98 -98
  134. package/test/models/dynamo/connectorSchema.test.js +189 -189
  135. package/test/models/models.test.js +568 -539
  136. package/test/routes/managedAuthRoutes.test.js +104 -104
  137. package/test/setup.js +178 -178
@@ -1,616 +1,616 @@
1
- // Use in-memory SQLite for isolated model tests
2
- jest.mock('../../models/sequelize', () => {
3
- const { Sequelize } = require('sequelize');
4
- return {
5
- sequelize: new Sequelize({
6
- dialect: 'sqlite',
7
- storage: ':memory:',
8
- logging: false,
9
- }),
10
- };
11
- });
12
-
13
- jest.mock('axios');
14
- jest.mock('../../connector/registry');
15
- jest.mock('../../lib/oauth');
16
- jest.mock('../../lib/ringcentral');
17
- jest.mock('../../models/dynamo/connectorSchema', () => ({
18
- Connector: {
19
- getProxyConfig: jest.fn()
20
- }
21
- }));
22
-
23
- const axios = require('axios');
24
- const adminHandler = require('../../handlers/admin');
25
- const { AdminConfigModel } = require('../../models/adminConfigModel');
26
- const { UserModel } = require('../../models/userModel');
27
- const connectorRegistry = require('../../connector/registry');
28
- const oauth = require('../../lib/oauth');
29
- const { RingCentral } = require('../../lib/ringcentral');
30
- const { Connector } = require('../../models/dynamo/connectorSchema');
31
- const { sequelize } = require('../../models/sequelize');
32
-
33
- describe('Admin Handler', () => {
34
- beforeAll(async () => {
35
- await AdminConfigModel.sync({ force: true });
36
- await UserModel.sync({ force: true });
37
- });
38
-
39
- afterEach(async () => {
40
- await AdminConfigModel.destroy({ where: {} });
41
- await UserModel.destroy({ where: {} });
42
- jest.clearAllMocks();
43
- });
44
-
45
- afterAll(async () => {
46
- await sequelize.close();
47
- });
48
-
49
- describe('validateAdminRole', () => {
50
- test('should return validated true when user has admin permissions', async () => {
51
- // Arrange
52
- axios.get.mockResolvedValue({
53
- data: {
54
- permissions: {
55
- admin: { enabled: true }
56
- },
57
- account: { id: 'rc-account-123' },
58
- id: 'extension-123'
59
- }
60
- });
61
-
62
- // Act
63
- const result = await adminHandler.validateAdminRole({
64
- rcAccessToken: 'valid-token'
65
- });
66
-
67
- // Assert
68
- expect(result.isValidated).toBe(true);
69
- expect(result.rcAccountId).toBe('rc-account-123');
70
- expect(axios.get).toHaveBeenCalledWith(
71
- 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
72
- { headers: { Authorization: 'Bearer valid-token' } }
73
- );
74
- });
75
-
76
- test('should return validated false when user lacks admin permissions', async () => {
77
- // Arrange
78
- axios.get.mockResolvedValue({
79
- data: {
80
- permissions: {
81
- admin: { enabled: false }
82
- },
83
- account: { id: 'rc-account-456' },
84
- id: 'extension-456'
85
- }
86
- });
87
-
88
- // Act
89
- const result = await adminHandler.validateAdminRole({
90
- rcAccessToken: 'non-admin-token'
91
- });
92
-
93
- // Assert
94
- expect(result.isValidated).toBe(false);
95
- expect(result.rcAccountId).toBe('rc-account-456');
96
- });
97
-
98
- test('should return validated true for dev pass list extension', async () => {
99
- // Arrange
100
- const originalEnv = process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST;
101
- process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST = '999,1000,1001';
102
-
103
- axios.get.mockResolvedValue({
104
- data: {
105
- permissions: {
106
- admin: { enabled: false }
107
- },
108
- account: { id: 'rc-account-dev' },
109
- id: 1000
110
- }
111
- });
112
-
113
- // Act
114
- const result = await adminHandler.validateAdminRole({
115
- rcAccessToken: 'dev-token'
116
- });
117
-
118
- // Assert
119
- expect(result.isValidated).toBe(true);
120
-
121
- // Cleanup
122
- process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST = originalEnv;
123
- });
124
- });
125
-
126
- describe('validateRcUserToken', () => {
127
- test('should return rc account and extension identity from valid token', async () => {
128
- axios.get.mockResolvedValue({
129
- data: {
130
- account: { id: 'rc-account-789' },
131
- id: 'extension-789',
132
- contact: {
133
- firstName: 'Alex',
134
- lastName: 'Johnson'
135
- }
136
- }
137
- });
138
-
139
- const result = await adminHandler.validateRcUserToken({
140
- rcAccessToken: 'valid-user-token'
141
- });
142
-
143
- expect(result).toEqual({
144
- rcAccountId: 'rc-account-789',
145
- rcExtensionId: 'extension-789'
146
- });
147
- expect(axios.get).toHaveBeenCalledWith(
148
- 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
149
- { headers: { Authorization: 'Bearer valid-user-token' } }
150
- );
151
- });
152
-
153
- test('should throw when rcAccessToken is missing', async () => {
154
- await expect(adminHandler.validateRcUserToken({})).rejects.toThrow('rcAccessToken is required');
155
- expect(axios.get).not.toHaveBeenCalled();
156
- });
157
- });
158
-
159
- describe('upsertAdminSettings', () => {
160
- test('should create new admin config when none exists', async () => {
161
- // Act
162
- await adminHandler.upsertAdminSettings({
163
- hashedRcAccountId: 'hashed-123',
164
- adminSettings: {
165
- userSettings: { autoLogCalls: true, autoLogMessages: false }
166
- }
167
- });
168
-
169
- // Assert
170
- const config = await AdminConfigModel.findByPk('hashed-123');
171
- expect(config).not.toBeNull();
172
- expect(config.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: false });
173
- });
174
-
175
- test('should update existing admin config', async () => {
176
- // Arrange
177
- await AdminConfigModel.create({
178
- id: 'hashed-existing',
179
- userSettings: { autoLogCalls: false, autoLogMessages: true }
180
- });
181
-
182
- // Act
183
- await adminHandler.upsertAdminSettings({
184
- hashedRcAccountId: 'hashed-existing',
185
- adminSettings: {
186
- userSettings: { autoLogCalls: true, autoLogMessages: false }
187
- }
188
- });
189
-
190
- // Assert
191
- const config = await AdminConfigModel.findByPk('hashed-existing');
192
- expect(config.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: false });
193
- });
194
- });
195
-
196
- describe('getAdminSettings', () => {
197
- test('should return admin settings when they exist', async () => {
198
- // Arrange
199
- await AdminConfigModel.create({
200
- id: 'hashed-get-test',
201
- userSettings: { autoLogCalls: true, autoLogMessages: true }
202
- });
203
-
204
- // Act
205
- const result = await adminHandler.getAdminSettings({
206
- hashedRcAccountId: 'hashed-get-test'
207
- });
208
-
209
- // Assert
210
- expect(result).not.toBeNull();
211
- expect(result.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: true });
212
- });
213
-
214
- test('should return null when settings do not exist', async () => {
215
- // Act
216
- const result = await adminHandler.getAdminSettings({
217
- hashedRcAccountId: 'non-existent'
218
- });
219
-
220
- // Assert
221
- expect(result).toBeNull();
222
- });
223
- });
224
-
225
- describe('updateAdminRcTokens', () => {
226
- test('should update tokens for existing config', async () => {
227
- // Arrange
228
- await AdminConfigModel.create({
229
- id: 'hashed-token-test',
230
- adminAccessToken: 'old-access',
231
- adminRefreshToken: 'old-refresh',
232
- adminTokenExpiry: new Date('2024-01-01')
233
- });
234
-
235
- const newExpiry = new Date('2024-12-31');
236
-
237
- // Act
238
- await adminHandler.updateAdminRcTokens({
239
- hashedRcAccountId: 'hashed-token-test',
240
- adminAccessToken: 'new-access',
241
- adminRefreshToken: 'new-refresh',
242
- adminTokenExpiry: newExpiry
243
- });
244
-
245
- // Assert
246
- const config = await AdminConfigModel.findByPk('hashed-token-test');
247
- expect(config.adminAccessToken).toBe('new-access');
248
- expect(config.adminRefreshToken).toBe('new-refresh');
249
- });
250
-
251
- test('should create new config with tokens when none exists', async () => {
252
- // Arrange
253
- const expiry = new Date('2024-12-31');
254
-
255
- // Act
256
- await adminHandler.updateAdminRcTokens({
257
- hashedRcAccountId: 'hashed-new-token',
258
- adminAccessToken: 'new-access-token',
259
- adminRefreshToken: 'new-refresh-token',
260
- adminTokenExpiry: expiry
261
- });
262
-
263
- // Assert
264
- const config = await AdminConfigModel.findByPk('hashed-new-token');
265
- expect(config).not.toBeNull();
266
- expect(config.adminAccessToken).toBe('new-access-token');
267
- expect(config.adminRefreshToken).toBe('new-refresh-token');
268
- });
269
- });
270
-
271
- describe('getServerLoggingSettings', () => {
272
- test('should return settings from platform module when available', async () => {
273
- // Arrange
274
- const mockUser = { platform: 'testCRM', accessToken: 'token' };
275
- const mockSettings = { enableAutoLog: true, logLevel: 'debug' };
276
-
277
- const mockConnector = {
278
- getServerLoggingSettings: jest.fn().mockResolvedValue(mockSettings)
279
- };
280
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
281
-
282
- // Act
283
- const result = await adminHandler.getServerLoggingSettings({ user: mockUser });
284
-
285
- // Assert
286
- expect(result).toEqual(mockSettings);
287
- expect(mockConnector.getServerLoggingSettings).toHaveBeenCalledWith({ user: mockUser });
288
- });
289
-
290
- test('should return empty object when platform module lacks getServerLoggingSettings', async () => {
291
- // Arrange
292
- const mockUser = { platform: 'testCRM', accessToken: 'token' };
293
-
294
- const mockConnector = {};
295
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
296
-
297
- // Act
298
- const result = await adminHandler.getServerLoggingSettings({ user: mockUser });
299
-
300
- // Assert
301
- expect(result).toEqual({});
302
- });
303
- });
304
-
305
- describe('updateServerLoggingSettings', () => {
306
- test('should update settings via platform module when available', async () => {
307
- // Arrange
308
- const mockUser = { platform: 'testCRM', accessToken: 'token' };
309
- const additionalFieldValues = { field1: 'value1' };
310
-
311
- const mockConnector = {
312
- getOauthInfo: jest.fn().mockResolvedValue({
313
- clientId: 'id',
314
- clientSecret: 'secret',
315
- accessTokenUri: 'https://token.url'
316
- }),
317
- updateServerLoggingSettings: jest.fn().mockResolvedValue({
318
- successful: true,
319
- returnMessage: { message: 'Settings updated' }
320
- })
321
- };
322
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
323
- oauth.getOAuthApp.mockReturnValue({});
324
-
325
- // Act
326
- const result = await adminHandler.updateServerLoggingSettings({
327
- user: mockUser,
328
- additionalFieldValues
329
- });
330
-
331
- // Assert
332
- expect(result.successful).toBe(true);
333
- expect(mockConnector.updateServerLoggingSettings).toHaveBeenCalled();
334
- });
335
-
336
- test('should return empty object when platform module lacks updateServerLoggingSettings', async () => {
337
- // Arrange
338
- const mockUser = { platform: 'testCRM', accessToken: 'token' };
339
-
340
- const mockConnector = {
341
- getOauthInfo: jest.fn().mockResolvedValue({})
342
- };
343
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
344
- oauth.getOAuthApp.mockReturnValue({});
345
-
346
- // Act
347
- const result = await adminHandler.updateServerLoggingSettings({
348
- user: mockUser,
349
- additionalFieldValues: {}
350
- });
351
-
352
- // Assert
353
- expect(result).toEqual({});
354
- });
355
- });
356
-
357
- describe('getAdminReport', () => {
358
- test('should return empty stats when RC credentials are not configured', async () => {
359
- // Arrange
360
- const originalServer = process.env.RINGCENTRAL_SERVER;
361
- delete process.env.RINGCENTRAL_SERVER;
362
-
363
- // Act
364
- const result = await adminHandler.getAdminReport({
365
- rcAccountId: 'account-123',
366
- timezone: 'America/Los_Angeles',
367
- timeFrom: '2024-01-01',
368
- timeTo: '2024-01-31',
369
- groupBy: 'Users'
370
- });
371
-
372
- // Assert
373
- expect(result).toEqual({ callLogStats: {} });
374
-
375
- // Cleanup
376
- if (originalServer) {
377
- process.env.RINGCENTRAL_SERVER = originalServer;
378
- }
379
- });
380
-
381
- test('should handle errors and return empty stats', async () => {
382
- // Arrange
383
- const originalServer = process.env.RINGCENTRAL_SERVER;
384
- const originalClientId = process.env.RINGCENTRAL_CLIENT_ID;
385
- const originalClientSecret = process.env.RINGCENTRAL_CLIENT_SECRET;
386
-
387
- process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
388
- process.env.RINGCENTRAL_CLIENT_ID = 'test-client-id';
389
- process.env.RINGCENTRAL_CLIENT_SECRET = 'test-client-secret';
390
-
391
- // Mock AdminConfigModel.findByPk to throw error
392
- jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('Database error'));
393
-
394
- // Act
395
- const result = await adminHandler.getAdminReport({
396
- rcAccountId: 'account-123',
397
- timezone: 'America/Los_Angeles',
398
- timeFrom: '2024-01-01',
399
- timeTo: '2024-01-31',
400
- groupBy: 'Users'
401
- });
402
-
403
- // Assert
404
- expect(result).toEqual({ callLogStats: {} });
405
-
406
- // Cleanup
407
- process.env.RINGCENTRAL_SERVER = originalServer;
408
- process.env.RINGCENTRAL_CLIENT_ID = originalClientId;
409
- process.env.RINGCENTRAL_CLIENT_SECRET = originalClientSecret;
410
- });
411
- });
412
-
413
- describe('getUserReport', () => {
414
- test('should return empty stats when RC credentials are not configured', async () => {
415
- // Arrange
416
- const originalServer = process.env.RINGCENTRAL_SERVER;
417
- delete process.env.RINGCENTRAL_SERVER;
418
-
419
- // Act
420
- const result = await adminHandler.getUserReport({
421
- rcAccountId: 'account-123',
422
- rcExtensionId: 'extension-123',
423
- timezone: 'America/Los_Angeles',
424
- timeFrom: '2024-01-01',
425
- timeTo: '2024-01-31'
426
- });
427
-
428
- // Assert
429
- expect(result).toEqual({ callLogStats: {} });
430
-
431
- // Cleanup
432
- if (originalServer) {
433
- process.env.RINGCENTRAL_SERVER = originalServer;
434
- }
435
- });
436
-
437
- test('should handle errors and return null', async () => {
438
- // Arrange
439
- const originalServer = process.env.RINGCENTRAL_SERVER;
440
- const originalClientId = process.env.RINGCENTRAL_CLIENT_ID;
441
- const originalClientSecret = process.env.RINGCENTRAL_CLIENT_SECRET;
442
-
443
- process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
444
- process.env.RINGCENTRAL_CLIENT_ID = 'test-client-id';
445
- process.env.RINGCENTRAL_CLIENT_SECRET = 'test-client-secret';
446
-
447
- // Mock AdminConfigModel.findByPk to throw error
448
- jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('Database error'));
449
-
450
- // Act
451
- const result = await adminHandler.getUserReport({
452
- rcAccountId: 'account-123',
453
- rcExtensionId: 'extension-123',
454
- timezone: 'America/Los_Angeles',
455
- timeFrom: '2024-01-01',
456
- timeTo: '2024-01-31'
457
- });
458
-
459
- // Assert
460
- expect(result).toBeNull();
461
-
462
- // Cleanup
463
- process.env.RINGCENTRAL_SERVER = originalServer;
464
- process.env.RINGCENTRAL_CLIENT_ID = originalClientId;
465
- process.env.RINGCENTRAL_CLIENT_SECRET = originalClientSecret;
466
- });
467
- });
468
-
469
- describe('getUserMapping', () => {
470
- test('should return empty array when platform module lacks getUserList', async () => {
471
- // Arrange
472
- await UserModel.create({
473
- id: 'test-user-id',
474
- platform: 'testCRM',
475
- accessToken: 'token',
476
- platformAdditionalInfo: {}
477
- });
478
-
479
- const mockConnector = {};
480
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
481
-
482
- // Act
483
- const result = await adminHandler.getUserMapping({
484
- user: { id: 'test-user-id', platform: 'testCRM', platformAdditionalInfo: {} },
485
- hashedRcAccountId: 'hashed-123',
486
- rcExtensionList: []
487
- });
488
-
489
- // Assert
490
- expect(result).toEqual([]);
491
- });
492
-
493
- test('should return empty array when proxy config lacks getUserList operation', async () => {
494
- // Arrange
495
- const user = {
496
- id: 'test-user-id',
497
- platform: 'testCRM',
498
- accessToken: 'token',
499
- platformAdditionalInfo: { proxyId: 'proxy-123' }
500
- };
501
-
502
- Connector.getProxyConfig.mockResolvedValue({
503
- operations: {}
504
- });
505
-
506
- const mockConnector = {
507
- getUserList: jest.fn()
508
- };
509
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
510
-
511
- // Act
512
- const result = await adminHandler.getUserMapping({
513
- user,
514
- hashedRcAccountId: 'hashed-123',
515
- rcExtensionList: []
516
- });
517
-
518
- // Assert
519
- expect(result).toEqual([]);
520
- });
521
-
522
- test('should map CRM users to RC extensions', async () => {
523
- // Arrange
524
- await AdminConfigModel.create({
525
- id: 'hashed-mapping',
526
- userMappings: []
527
- });
528
-
529
- const user = {
530
- id: 'test-user-id',
531
- platform: 'testCRM',
532
- accessToken: 'token',
533
- platformAdditionalInfo: {}
534
- };
535
-
536
- const crmUsers = [
537
- { id: 'crm-user-1', name: 'John Doe', email: 'john@example.com' },
538
- { id: 'crm-user-2', name: 'Jane Smith', email: 'jane@example.com' }
539
- ];
540
-
541
- const rcExtensions = [
542
- { id: 'ext-1', firstName: 'John', lastName: 'Doe', email: 'john@example.com', extensionNumber: '101' },
543
- { id: 'ext-2', firstName: 'Bob', lastName: 'Wilson', email: 'bob@example.com', extensionNumber: '102' }
544
- ];
545
-
546
- const mockConnector = {
547
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
548
- getBasicAuth: jest.fn().mockReturnValue('base64'),
549
- getUserList: jest.fn().mockResolvedValue(crmUsers)
550
- };
551
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
552
-
553
- // Act
554
- const result = await adminHandler.getUserMapping({
555
- user,
556
- hashedRcAccountId: 'hashed-mapping',
557
- rcExtensionList: rcExtensions
558
- });
559
-
560
- // Assert
561
- expect(result).toHaveLength(2);
562
- // John should be matched by email
563
- const johnMapping = result.find(m => m.crmUser.id === 'crm-user-1');
564
- expect(johnMapping.rcUser).toHaveLength(1);
565
- expect(johnMapping.rcUser[0].extensionId).toBe('ext-1');
566
-
567
- // Jane should not be matched
568
- const janeMapping = result.find(m => m.crmUser.id === 'crm-user-2');
569
- expect(janeMapping.rcUser).toHaveLength(0);
570
- });
571
-
572
- test('should preserve existing mappings', async () => {
573
- // Arrange
574
- await AdminConfigModel.create({
575
- id: 'hashed-existing-mapping',
576
- userMappings: [
577
- { crmUserId: 'crm-user-1', rcExtensionId: ['ext-existing'] }
578
- ]
579
- });
580
-
581
- const user = {
582
- id: 'test-user-id',
583
- platform: 'testCRM',
584
- accessToken: 'token',
585
- platformAdditionalInfo: {}
586
- };
587
-
588
- const crmUsers = [
589
- { id: 'crm-user-1', name: 'John Doe', email: 'john@example.com' }
590
- ];
591
-
592
- const rcExtensions = [
593
- { id: 'ext-existing', firstName: 'John', lastName: 'Doe', email: 'john@example.com', extensionNumber: '101' }
594
- ];
595
-
596
- const mockConnector = {
597
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
598
- getBasicAuth: jest.fn().mockReturnValue('base64'),
599
- getUserList: jest.fn().mockResolvedValue(crmUsers)
600
- };
601
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
602
-
603
- // Act
604
- const result = await adminHandler.getUserMapping({
605
- user,
606
- hashedRcAccountId: 'hashed-existing-mapping',
607
- rcExtensionList: rcExtensions
608
- });
609
-
610
- // Assert
611
- expect(result).toHaveLength(1);
612
- expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
613
- });
614
- });
615
- });
616
-
1
+ // Use in-memory SQLite for isolated model tests
2
+ jest.mock('../../models/sequelize', () => {
3
+ const { Sequelize } = require('sequelize');
4
+ return {
5
+ sequelize: new Sequelize({
6
+ dialect: 'sqlite',
7
+ storage: ':memory:',
8
+ logging: false,
9
+ }),
10
+ };
11
+ });
12
+
13
+ jest.mock('axios');
14
+ jest.mock('../../connector/registry');
15
+ jest.mock('../../lib/oauth');
16
+ jest.mock('../../lib/ringcentral');
17
+ jest.mock('../../models/dynamo/connectorSchema', () => ({
18
+ Connector: {
19
+ getProxyConfig: jest.fn()
20
+ }
21
+ }));
22
+
23
+ const axios = require('axios');
24
+ const adminHandler = require('../../handlers/admin');
25
+ const { AdminConfigModel } = require('../../models/adminConfigModel');
26
+ const { UserModel } = require('../../models/userModel');
27
+ const connectorRegistry = require('../../connector/registry');
28
+ const oauth = require('../../lib/oauth');
29
+ const { RingCentral } = require('../../lib/ringcentral');
30
+ const { Connector } = require('../../models/dynamo/connectorSchema');
31
+ const { sequelize } = require('../../models/sequelize');
32
+
33
+ describe('Admin Handler', () => {
34
+ beforeAll(async () => {
35
+ await AdminConfigModel.sync({ force: true });
36
+ await UserModel.sync({ force: true });
37
+ });
38
+
39
+ afterEach(async () => {
40
+ await AdminConfigModel.destroy({ where: {} });
41
+ await UserModel.destroy({ where: {} });
42
+ jest.clearAllMocks();
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await sequelize.close();
47
+ });
48
+
49
+ describe('validateAdminRole', () => {
50
+ test('should return validated true when user has admin permissions', async () => {
51
+ // Arrange
52
+ axios.get.mockResolvedValue({
53
+ data: {
54
+ permissions: {
55
+ admin: { enabled: true }
56
+ },
57
+ account: { id: 'rc-account-123' },
58
+ id: 'extension-123'
59
+ }
60
+ });
61
+
62
+ // Act
63
+ const result = await adminHandler.validateAdminRole({
64
+ rcAccessToken: 'valid-token'
65
+ });
66
+
67
+ // Assert
68
+ expect(result.isValidated).toBe(true);
69
+ expect(result.rcAccountId).toBe('rc-account-123');
70
+ expect(axios.get).toHaveBeenCalledWith(
71
+ 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
72
+ { headers: { Authorization: 'Bearer valid-token' } }
73
+ );
74
+ });
75
+
76
+ test('should return validated false when user lacks admin permissions', async () => {
77
+ // Arrange
78
+ axios.get.mockResolvedValue({
79
+ data: {
80
+ permissions: {
81
+ admin: { enabled: false }
82
+ },
83
+ account: { id: 'rc-account-456' },
84
+ id: 'extension-456'
85
+ }
86
+ });
87
+
88
+ // Act
89
+ const result = await adminHandler.validateAdminRole({
90
+ rcAccessToken: 'non-admin-token'
91
+ });
92
+
93
+ // Assert
94
+ expect(result.isValidated).toBe(false);
95
+ expect(result.rcAccountId).toBe('rc-account-456');
96
+ });
97
+
98
+ test('should return validated true for dev pass list extension', async () => {
99
+ // Arrange
100
+ const originalEnv = process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST;
101
+ process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST = '999,1000,1001';
102
+
103
+ axios.get.mockResolvedValue({
104
+ data: {
105
+ permissions: {
106
+ admin: { enabled: false }
107
+ },
108
+ account: { id: 'rc-account-dev' },
109
+ id: 1000
110
+ }
111
+ });
112
+
113
+ // Act
114
+ const result = await adminHandler.validateAdminRole({
115
+ rcAccessToken: 'dev-token'
116
+ });
117
+
118
+ // Assert
119
+ expect(result.isValidated).toBe(true);
120
+
121
+ // Cleanup
122
+ process.env.ADMIN_EXTENSION_ID_DEV_PASS_LIST = originalEnv;
123
+ });
124
+ });
125
+
126
+ describe('validateRcUserToken', () => {
127
+ test('should return rc account and extension identity from valid token', async () => {
128
+ axios.get.mockResolvedValue({
129
+ data: {
130
+ account: { id: 'rc-account-789' },
131
+ id: 'extension-789',
132
+ contact: {
133
+ firstName: 'Alex',
134
+ lastName: 'Johnson'
135
+ }
136
+ }
137
+ });
138
+
139
+ const result = await adminHandler.validateRcUserToken({
140
+ rcAccessToken: 'valid-user-token'
141
+ });
142
+
143
+ expect(result).toEqual({
144
+ rcAccountId: 'rc-account-789',
145
+ rcExtensionId: 'extension-789'
146
+ });
147
+ expect(axios.get).toHaveBeenCalledWith(
148
+ 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
149
+ { headers: { Authorization: 'Bearer valid-user-token' } }
150
+ );
151
+ });
152
+
153
+ test('should throw when rcAccessToken is missing', async () => {
154
+ await expect(adminHandler.validateRcUserToken({})).rejects.toThrow('rcAccessToken is required');
155
+ expect(axios.get).not.toHaveBeenCalled();
156
+ });
157
+ });
158
+
159
+ describe('upsertAdminSettings', () => {
160
+ test('should create new admin config when none exists', async () => {
161
+ // Act
162
+ await adminHandler.upsertAdminSettings({
163
+ hashedRcAccountId: 'hashed-123',
164
+ adminSettings: {
165
+ userSettings: { autoLogCalls: true, autoLogMessages: false }
166
+ }
167
+ });
168
+
169
+ // Assert
170
+ const config = await AdminConfigModel.findByPk('hashed-123');
171
+ expect(config).not.toBeNull();
172
+ expect(config.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: false });
173
+ });
174
+
175
+ test('should update existing admin config', async () => {
176
+ // Arrange
177
+ await AdminConfigModel.create({
178
+ id: 'hashed-existing',
179
+ userSettings: { autoLogCalls: false, autoLogMessages: true }
180
+ });
181
+
182
+ // Act
183
+ await adminHandler.upsertAdminSettings({
184
+ hashedRcAccountId: 'hashed-existing',
185
+ adminSettings: {
186
+ userSettings: { autoLogCalls: true, autoLogMessages: false }
187
+ }
188
+ });
189
+
190
+ // Assert
191
+ const config = await AdminConfigModel.findByPk('hashed-existing');
192
+ expect(config.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: false });
193
+ });
194
+ });
195
+
196
+ describe('getAdminSettings', () => {
197
+ test('should return admin settings when they exist', async () => {
198
+ // Arrange
199
+ await AdminConfigModel.create({
200
+ id: 'hashed-get-test',
201
+ userSettings: { autoLogCalls: true, autoLogMessages: true }
202
+ });
203
+
204
+ // Act
205
+ const result = await adminHandler.getAdminSettings({
206
+ hashedRcAccountId: 'hashed-get-test'
207
+ });
208
+
209
+ // Assert
210
+ expect(result).not.toBeNull();
211
+ expect(result.userSettings).toEqual({ autoLogCalls: true, autoLogMessages: true });
212
+ });
213
+
214
+ test('should return null when settings do not exist', async () => {
215
+ // Act
216
+ const result = await adminHandler.getAdminSettings({
217
+ hashedRcAccountId: 'non-existent'
218
+ });
219
+
220
+ // Assert
221
+ expect(result).toBeNull();
222
+ });
223
+ });
224
+
225
+ describe('updateAdminRcTokens', () => {
226
+ test('should update tokens for existing config', async () => {
227
+ // Arrange
228
+ await AdminConfigModel.create({
229
+ id: 'hashed-token-test',
230
+ adminAccessToken: 'old-access',
231
+ adminRefreshToken: 'old-refresh',
232
+ adminTokenExpiry: new Date('2024-01-01')
233
+ });
234
+
235
+ const newExpiry = new Date('2024-12-31');
236
+
237
+ // Act
238
+ await adminHandler.updateAdminRcTokens({
239
+ hashedRcAccountId: 'hashed-token-test',
240
+ adminAccessToken: 'new-access',
241
+ adminRefreshToken: 'new-refresh',
242
+ adminTokenExpiry: newExpiry
243
+ });
244
+
245
+ // Assert
246
+ const config = await AdminConfigModel.findByPk('hashed-token-test');
247
+ expect(config.adminAccessToken).toBe('new-access');
248
+ expect(config.adminRefreshToken).toBe('new-refresh');
249
+ });
250
+
251
+ test('should create new config with tokens when none exists', async () => {
252
+ // Arrange
253
+ const expiry = new Date('2024-12-31');
254
+
255
+ // Act
256
+ await adminHandler.updateAdminRcTokens({
257
+ hashedRcAccountId: 'hashed-new-token',
258
+ adminAccessToken: 'new-access-token',
259
+ adminRefreshToken: 'new-refresh-token',
260
+ adminTokenExpiry: expiry
261
+ });
262
+
263
+ // Assert
264
+ const config = await AdminConfigModel.findByPk('hashed-new-token');
265
+ expect(config).not.toBeNull();
266
+ expect(config.adminAccessToken).toBe('new-access-token');
267
+ expect(config.adminRefreshToken).toBe('new-refresh-token');
268
+ });
269
+ });
270
+
271
+ describe('getServerLoggingSettings', () => {
272
+ test('should return settings from platform module when available', async () => {
273
+ // Arrange
274
+ const mockUser = { platform: 'testCRM', accessToken: 'token' };
275
+ const mockSettings = { enableAutoLog: true, logLevel: 'debug' };
276
+
277
+ const mockConnector = {
278
+ getServerLoggingSettings: jest.fn().mockResolvedValue(mockSettings)
279
+ };
280
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
281
+
282
+ // Act
283
+ const result = await adminHandler.getServerLoggingSettings({ user: mockUser });
284
+
285
+ // Assert
286
+ expect(result).toEqual(mockSettings);
287
+ expect(mockConnector.getServerLoggingSettings).toHaveBeenCalledWith({ user: mockUser });
288
+ });
289
+
290
+ test('should return empty object when platform module lacks getServerLoggingSettings', async () => {
291
+ // Arrange
292
+ const mockUser = { platform: 'testCRM', accessToken: 'token' };
293
+
294
+ const mockConnector = {};
295
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
296
+
297
+ // Act
298
+ const result = await adminHandler.getServerLoggingSettings({ user: mockUser });
299
+
300
+ // Assert
301
+ expect(result).toEqual({});
302
+ });
303
+ });
304
+
305
+ describe('updateServerLoggingSettings', () => {
306
+ test('should update settings via platform module when available', async () => {
307
+ // Arrange
308
+ const mockUser = { platform: 'testCRM', accessToken: 'token' };
309
+ const additionalFieldValues = { field1: 'value1' };
310
+
311
+ const mockConnector = {
312
+ getOauthInfo: jest.fn().mockResolvedValue({
313
+ clientId: 'id',
314
+ clientSecret: 'secret',
315
+ accessTokenUri: 'https://token.url'
316
+ }),
317
+ updateServerLoggingSettings: jest.fn().mockResolvedValue({
318
+ successful: true,
319
+ returnMessage: { message: 'Settings updated' }
320
+ })
321
+ };
322
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
323
+ oauth.getOAuthApp.mockReturnValue({});
324
+
325
+ // Act
326
+ const result = await adminHandler.updateServerLoggingSettings({
327
+ user: mockUser,
328
+ additionalFieldValues
329
+ });
330
+
331
+ // Assert
332
+ expect(result.successful).toBe(true);
333
+ expect(mockConnector.updateServerLoggingSettings).toHaveBeenCalled();
334
+ });
335
+
336
+ test('should return empty object when platform module lacks updateServerLoggingSettings', async () => {
337
+ // Arrange
338
+ const mockUser = { platform: 'testCRM', accessToken: 'token' };
339
+
340
+ const mockConnector = {
341
+ getOauthInfo: jest.fn().mockResolvedValue({})
342
+ };
343
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
344
+ oauth.getOAuthApp.mockReturnValue({});
345
+
346
+ // Act
347
+ const result = await adminHandler.updateServerLoggingSettings({
348
+ user: mockUser,
349
+ additionalFieldValues: {}
350
+ });
351
+
352
+ // Assert
353
+ expect(result).toEqual({});
354
+ });
355
+ });
356
+
357
+ describe('getAdminReport', () => {
358
+ test('should return empty stats when RC credentials are not configured', async () => {
359
+ // Arrange
360
+ const originalServer = process.env.RINGCENTRAL_SERVER;
361
+ delete process.env.RINGCENTRAL_SERVER;
362
+
363
+ // Act
364
+ const result = await adminHandler.getAdminReport({
365
+ rcAccountId: 'account-123',
366
+ timezone: 'America/Los_Angeles',
367
+ timeFrom: '2024-01-01',
368
+ timeTo: '2024-01-31',
369
+ groupBy: 'Users'
370
+ });
371
+
372
+ // Assert
373
+ expect(result).toEqual({ callLogStats: {} });
374
+
375
+ // Cleanup
376
+ if (originalServer) {
377
+ process.env.RINGCENTRAL_SERVER = originalServer;
378
+ }
379
+ });
380
+
381
+ test('should handle errors and return empty stats', async () => {
382
+ // Arrange
383
+ const originalServer = process.env.RINGCENTRAL_SERVER;
384
+ const originalClientId = process.env.RINGCENTRAL_CLIENT_ID;
385
+ const originalClientSecret = process.env.RINGCENTRAL_CLIENT_SECRET;
386
+
387
+ process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
388
+ process.env.RINGCENTRAL_CLIENT_ID = 'test-client-id';
389
+ process.env.RINGCENTRAL_CLIENT_SECRET = 'test-client-secret';
390
+
391
+ // Mock AdminConfigModel.findByPk to throw error
392
+ jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('Database error'));
393
+
394
+ // Act
395
+ const result = await adminHandler.getAdminReport({
396
+ rcAccountId: 'account-123',
397
+ timezone: 'America/Los_Angeles',
398
+ timeFrom: '2024-01-01',
399
+ timeTo: '2024-01-31',
400
+ groupBy: 'Users'
401
+ });
402
+
403
+ // Assert
404
+ expect(result).toEqual({ callLogStats: {} });
405
+
406
+ // Cleanup
407
+ process.env.RINGCENTRAL_SERVER = originalServer;
408
+ process.env.RINGCENTRAL_CLIENT_ID = originalClientId;
409
+ process.env.RINGCENTRAL_CLIENT_SECRET = originalClientSecret;
410
+ });
411
+ });
412
+
413
+ describe('getUserReport', () => {
414
+ test('should return empty stats when RC credentials are not configured', async () => {
415
+ // Arrange
416
+ const originalServer = process.env.RINGCENTRAL_SERVER;
417
+ delete process.env.RINGCENTRAL_SERVER;
418
+
419
+ // Act
420
+ const result = await adminHandler.getUserReport({
421
+ rcAccountId: 'account-123',
422
+ rcExtensionId: 'extension-123',
423
+ timezone: 'America/Los_Angeles',
424
+ timeFrom: '2024-01-01',
425
+ timeTo: '2024-01-31'
426
+ });
427
+
428
+ // Assert
429
+ expect(result).toEqual({ callLogStats: {} });
430
+
431
+ // Cleanup
432
+ if (originalServer) {
433
+ process.env.RINGCENTRAL_SERVER = originalServer;
434
+ }
435
+ });
436
+
437
+ test('should handle errors and return null', async () => {
438
+ // Arrange
439
+ const originalServer = process.env.RINGCENTRAL_SERVER;
440
+ const originalClientId = process.env.RINGCENTRAL_CLIENT_ID;
441
+ const originalClientSecret = process.env.RINGCENTRAL_CLIENT_SECRET;
442
+
443
+ process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
444
+ process.env.RINGCENTRAL_CLIENT_ID = 'test-client-id';
445
+ process.env.RINGCENTRAL_CLIENT_SECRET = 'test-client-secret';
446
+
447
+ // Mock AdminConfigModel.findByPk to throw error
448
+ jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('Database error'));
449
+
450
+ // Act
451
+ const result = await adminHandler.getUserReport({
452
+ rcAccountId: 'account-123',
453
+ rcExtensionId: 'extension-123',
454
+ timezone: 'America/Los_Angeles',
455
+ timeFrom: '2024-01-01',
456
+ timeTo: '2024-01-31'
457
+ });
458
+
459
+ // Assert
460
+ expect(result).toBeNull();
461
+
462
+ // Cleanup
463
+ process.env.RINGCENTRAL_SERVER = originalServer;
464
+ process.env.RINGCENTRAL_CLIENT_ID = originalClientId;
465
+ process.env.RINGCENTRAL_CLIENT_SECRET = originalClientSecret;
466
+ });
467
+ });
468
+
469
+ describe('getUserMapping', () => {
470
+ test('should return empty array when platform module lacks getUserList', async () => {
471
+ // Arrange
472
+ await UserModel.create({
473
+ id: 'test-user-id',
474
+ platform: 'testCRM',
475
+ accessToken: 'token',
476
+ platformAdditionalInfo: {}
477
+ });
478
+
479
+ const mockConnector = {};
480
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
481
+
482
+ // Act
483
+ const result = await adminHandler.getUserMapping({
484
+ user: { id: 'test-user-id', platform: 'testCRM', platformAdditionalInfo: {} },
485
+ hashedRcAccountId: 'hashed-123',
486
+ rcExtensionList: []
487
+ });
488
+
489
+ // Assert
490
+ expect(result).toEqual([]);
491
+ });
492
+
493
+ test('should return empty array when proxy config lacks getUserList operation', async () => {
494
+ // Arrange
495
+ const user = {
496
+ id: 'test-user-id',
497
+ platform: 'testCRM',
498
+ accessToken: 'token',
499
+ platformAdditionalInfo: { proxyId: 'proxy-123' }
500
+ };
501
+
502
+ Connector.getProxyConfig.mockResolvedValue({
503
+ operations: {}
504
+ });
505
+
506
+ const mockConnector = {
507
+ getUserList: jest.fn()
508
+ };
509
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
510
+
511
+ // Act
512
+ const result = await adminHandler.getUserMapping({
513
+ user,
514
+ hashedRcAccountId: 'hashed-123',
515
+ rcExtensionList: []
516
+ });
517
+
518
+ // Assert
519
+ expect(result).toEqual([]);
520
+ });
521
+
522
+ test('should map CRM users to RC extensions', async () => {
523
+ // Arrange
524
+ await AdminConfigModel.create({
525
+ id: 'hashed-mapping',
526
+ userMappings: []
527
+ });
528
+
529
+ const user = {
530
+ id: 'test-user-id',
531
+ platform: 'testCRM',
532
+ accessToken: 'token',
533
+ platformAdditionalInfo: {}
534
+ };
535
+
536
+ const crmUsers = [
537
+ { id: 'crm-user-1', name: 'John Doe', email: 'john@example.com' },
538
+ { id: 'crm-user-2', name: 'Jane Smith', email: 'jane@example.com' }
539
+ ];
540
+
541
+ const rcExtensions = [
542
+ { id: 'ext-1', firstName: 'John', lastName: 'Doe', email: 'john@example.com', extensionNumber: '101' },
543
+ { id: 'ext-2', firstName: 'Bob', lastName: 'Wilson', email: 'bob@example.com', extensionNumber: '102' }
544
+ ];
545
+
546
+ const mockConnector = {
547
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
548
+ getBasicAuth: jest.fn().mockReturnValue('base64'),
549
+ getUserList: jest.fn().mockResolvedValue(crmUsers)
550
+ };
551
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
552
+
553
+ // Act
554
+ const result = await adminHandler.getUserMapping({
555
+ user,
556
+ hashedRcAccountId: 'hashed-mapping',
557
+ rcExtensionList: rcExtensions
558
+ });
559
+
560
+ // Assert
561
+ expect(result).toHaveLength(2);
562
+ // John should be matched by email
563
+ const johnMapping = result.find(m => m.crmUser.id === 'crm-user-1');
564
+ expect(johnMapping.rcUser).toHaveLength(1);
565
+ expect(johnMapping.rcUser[0].extensionId).toBe('ext-1');
566
+
567
+ // Jane should not be matched
568
+ const janeMapping = result.find(m => m.crmUser.id === 'crm-user-2');
569
+ expect(janeMapping.rcUser).toHaveLength(0);
570
+ });
571
+
572
+ test('should preserve existing mappings', async () => {
573
+ // Arrange
574
+ await AdminConfigModel.create({
575
+ id: 'hashed-existing-mapping',
576
+ userMappings: [
577
+ { crmUserId: 'crm-user-1', rcExtensionId: ['ext-existing'] }
578
+ ]
579
+ });
580
+
581
+ const user = {
582
+ id: 'test-user-id',
583
+ platform: 'testCRM',
584
+ accessToken: 'token',
585
+ platformAdditionalInfo: {}
586
+ };
587
+
588
+ const crmUsers = [
589
+ { id: 'crm-user-1', name: 'John Doe', email: 'john@example.com' }
590
+ ];
591
+
592
+ const rcExtensions = [
593
+ { id: 'ext-existing', firstName: 'John', lastName: 'Doe', email: 'john@example.com', extensionNumber: '101' }
594
+ ];
595
+
596
+ const mockConnector = {
597
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
598
+ getBasicAuth: jest.fn().mockReturnValue('base64'),
599
+ getUserList: jest.fn().mockResolvedValue(crmUsers)
600
+ };
601
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
602
+
603
+ // Act
604
+ const result = await adminHandler.getUserMapping({
605
+ user,
606
+ hashedRcAccountId: 'hashed-existing-mapping',
607
+ rcExtensionList: rcExtensions
608
+ });
609
+
610
+ // Assert
611
+ expect(result).toHaveLength(1);
612
+ expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
613
+ });
614
+ });
615
+ });
616
+