@app-connect/core 1.7.25 → 1.7.27

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 (138) 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 +320 -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 +990 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/managedOAuth.js +247 -0
  25. package/handlers/plugin.js +208 -208
  26. package/handlers/user.js +142 -142
  27. package/index.js +3162 -2652
  28. package/jest.config.js +56 -56
  29. package/lib/analytics.js +54 -54
  30. package/lib/authSession.js +109 -109
  31. package/lib/cacheCleanup.js +21 -0
  32. package/lib/callLogComposer.js +898 -898
  33. package/lib/callLogLookup.js +34 -0
  34. package/lib/constants.js +8 -8
  35. package/lib/debugTracer.js +177 -177
  36. package/lib/encode.js +30 -30
  37. package/lib/errorHandler.js +218 -206
  38. package/lib/generalErrorMessage.js +41 -41
  39. package/lib/jwt.js +18 -18
  40. package/lib/logger.js +190 -190
  41. package/lib/migrateCallLogsSchema.js +116 -0
  42. package/lib/ringcentral.js +266 -266
  43. package/lib/s3ErrorLogReport.js +65 -65
  44. package/lib/sharedSMSComposer.js +471 -471
  45. package/lib/util.js +67 -67
  46. package/mcp/README.md +412 -395
  47. package/mcp/lib/validator.js +91 -91
  48. package/mcp/mcpHandler.js +425 -425
  49. package/mcp/tools/cancelAppointment.js +101 -0
  50. package/mcp/tools/checkAuthStatus.js +105 -105
  51. package/mcp/tools/confirmAppointment.js +101 -0
  52. package/mcp/tools/createAppointment.js +157 -0
  53. package/mcp/tools/createCallLog.js +327 -316
  54. package/mcp/tools/createContact.js +117 -117
  55. package/mcp/tools/createMessageLog.js +287 -287
  56. package/mcp/tools/doAuth.js +60 -60
  57. package/mcp/tools/findContactByName.js +93 -93
  58. package/mcp/tools/findContactByPhone.js +101 -101
  59. package/mcp/tools/getCallLog.js +111 -102
  60. package/mcp/tools/getGoogleFilePicker.js +99 -99
  61. package/mcp/tools/getHelp.js +43 -43
  62. package/mcp/tools/getPublicConnectors.js +94 -94
  63. package/mcp/tools/getSessionInfo.js +90 -90
  64. package/mcp/tools/index.js +51 -41
  65. package/mcp/tools/listAppointments.js +163 -0
  66. package/mcp/tools/logout.js +99 -96
  67. package/mcp/tools/rcGetCallLogs.js +65 -65
  68. package/mcp/tools/updateAppointment.js +154 -0
  69. package/mcp/tools/updateCallLog.js +130 -126
  70. package/mcp/ui/App/App.tsx +358 -358
  71. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  72. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  73. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  74. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  75. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  76. package/mcp/ui/App/lib/callTool.ts +130 -130
  77. package/mcp/ui/App/lib/debugLog.ts +41 -41
  78. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  79. package/mcp/ui/App/main.css +5 -5
  80. package/mcp/ui/App/root.tsx +13 -13
  81. package/mcp/ui/index.html +13 -13
  82. package/mcp/ui/package-lock.json +6356 -6356
  83. package/mcp/ui/package.json +25 -25
  84. package/mcp/ui/tsconfig.json +26 -26
  85. package/mcp/ui/vite.config.ts +16 -16
  86. package/models/accountDataModel.js +33 -33
  87. package/models/adminConfigModel.js +35 -35
  88. package/models/cacheModel.js +30 -26
  89. package/models/callDownListModel.js +34 -34
  90. package/models/callLogModel.js +33 -27
  91. package/models/dynamo/connectorSchema.js +146 -146
  92. package/models/dynamo/lockSchema.js +24 -24
  93. package/models/dynamo/noteCacheSchema.js +29 -29
  94. package/models/llmSessionModel.js +17 -17
  95. package/models/messageLogModel.js +25 -25
  96. package/models/sequelize.js +16 -16
  97. package/models/userModel.js +45 -45
  98. package/package.json +1 -1
  99. package/releaseNotes.json +1101 -1081
  100. package/test/connector/proxy/engine.test.js +126 -126
  101. package/test/connector/proxy/index.test.js +279 -279
  102. package/test/connector/proxy/sample.json +161 -161
  103. package/test/connector/registry.test.js +415 -415
  104. package/test/handlers/admin.test.js +616 -616
  105. package/test/handlers/auth.test.js +1092 -1018
  106. package/test/handlers/contact.test.js +1014 -1014
  107. package/test/handlers/log.test.js +1298 -1160
  108. package/test/handlers/managedAuth.test.js +457 -457
  109. package/test/handlers/plugin.test.js +380 -380
  110. package/test/index.test.js +105 -105
  111. package/test/lib/cacheCleanup.test.js +42 -0
  112. package/test/lib/callLogComposer.test.js +1231 -1231
  113. package/test/lib/debugTracer.test.js +328 -328
  114. package/test/lib/jwt.test.js +176 -176
  115. package/test/lib/logger.test.js +206 -206
  116. package/test/lib/oauth.test.js +359 -359
  117. package/test/lib/ringcentral.test.js +467 -467
  118. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  119. package/test/lib/util.test.js +329 -329
  120. package/test/mcp/tools/checkAuthStatus.test.js +83 -83
  121. package/test/mcp/tools/createCallLog.test.js +436 -436
  122. package/test/mcp/tools/createContact.test.js +58 -58
  123. package/test/mcp/tools/createMessageLog.test.js +595 -595
  124. package/test/mcp/tools/doAuth.test.js +113 -113
  125. package/test/mcp/tools/findContactByName.test.js +275 -275
  126. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  127. package/test/mcp/tools/getCallLog.test.js +298 -298
  128. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  129. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  130. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  131. package/test/mcp/tools/logout.test.js +233 -233
  132. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  133. package/test/mcp/tools/updateCallLog.test.js +360 -360
  134. package/test/models/accountDataModel.test.js +98 -98
  135. package/test/models/dynamo/connectorSchema.test.js +189 -189
  136. package/test/models/models.test.js +568 -539
  137. package/test/routes/managedAuthRoutes.test.js +235 -104
  138. 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
+