@app-connect/core 1.7.34 → 1.7.36
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.
- package/connector/mock.js +9 -4
- package/connector/proxy/engine.js +3 -2
- package/connector/proxy/index.js +2 -2
- package/docs/architecture.md +1 -0
- package/docs/handlers.md +1 -1
- package/docs/models.md +2 -0
- package/docs/routes.md +1 -1
- package/handlers/disposition.js +3 -2
- package/handlers/log.js +18 -6
- package/handlers/plugin.js +13 -6
- package/index.js +22 -11
- package/lib/callLogLookup.js +82 -9
- package/lib/migrateCallLogsSchema.js +128 -7
- package/models/callLogModel.js +6 -0
- package/package.json +1 -1
- package/releaseNotes.json +44 -0
- package/test/connector/developerPortal.test.js +166 -0
- package/test/connector/mock.test.js +131 -0
- package/test/connector/proxy/engine.test.js +85 -0
- package/test/connector/proxy/index.test.js +246 -0
- package/test/connector/proxy/sample.json +1 -0
- package/test/handlers/admin.test.js +344 -0
- package/test/handlers/appointment.test.js +260 -0
- package/test/handlers/auth.test.js +6 -2
- package/test/handlers/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +327 -3
- package/test/handlers/managedOAuth.test.js +262 -0
- package/test/handlers/plugin.test.js +305 -0
- package/test/handlers/user.test.js +381 -0
- package/test/index.test.js +166 -1
- package/test/lib/analytics.test.js +146 -0
- package/test/lib/authSession.test.js +173 -0
- package/test/lib/encode.test.js +59 -0
- package/test/lib/errorHandler.test.js +246 -0
- package/test/lib/generalErrorMessage.test.js +82 -0
- package/test/lib/s3ErrorLogReport.test.js +187 -0
- package/test/mcp/mcpHandlerMore.test.js +384 -0
- package/test/mcp/tools/appointmentTools.test.js +362 -0
- package/test/models/callDownListModel.test.js +125 -0
- package/test/models/dynamo/lockSchema.test.js +37 -0
- package/test/models/dynamo/noteCacheSchema.test.js +45 -0
- package/test/models/llmSessionModel.test.js +91 -0
- package/test/models/models.test.js +92 -0
- package/test/routes/calldownRoutes.test.js +224 -0
- package/test/routes/coreRouterBroadRoutes.test.js +855 -0
- package/test/routes/dispositionRoutes.test.js +192 -0
- package/test/routes/managedAuthRoutes.test.js +151 -0
- package/test/routes/pluginRoutes.test.js +262 -0
|
@@ -126,6 +126,86 @@ describe('proxy connector - more coverage', () => {
|
|
|
126
126
|
expect(token).toBe(Buffer.from('abc:').toString('base64'));
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
test('getOauthInfo maps configured OAuth settings and falls back when config loading fails', async () => {
|
|
130
|
+
const oauthConfig = {
|
|
131
|
+
auth: {
|
|
132
|
+
type: 'oauth',
|
|
133
|
+
clientId: 'client-id',
|
|
134
|
+
clientSecret: 'client-secret',
|
|
135
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
136
|
+
redirectUri: 'https://app.example.com/callback'
|
|
137
|
+
},
|
|
138
|
+
operations: {}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await expect(proxy.getOauthInfo({
|
|
142
|
+
proxyConfig: oauthConfig,
|
|
143
|
+
tokenUrl: 'https://override.example.com/token'
|
|
144
|
+
})).resolves.toEqual({
|
|
145
|
+
clientId: 'client-id',
|
|
146
|
+
clientSecret: 'client-secret',
|
|
147
|
+
accessTokenUri: 'https://override.example.com/token',
|
|
148
|
+
redirectUri: 'https://app.example.com/callback'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
Connector.getProxyConfig.mockRejectedValueOnce(new Error('config unavailable'));
|
|
152
|
+
await expect(proxy.getOauthInfo({ proxyId: 'missing-config' })).resolves.toEqual({});
|
|
153
|
+
await expect(proxy.getAuthType({ proxyId: '' })).resolves.toBe('apiKey');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('getUserInfo returns a warning when the proxy config has no getUserInfo operation', async () => {
|
|
157
|
+
await expect(proxy.getUserInfo({
|
|
158
|
+
proxyConfig: { operations: {} },
|
|
159
|
+
platform: 'minimal'
|
|
160
|
+
})).resolves.toMatchObject({
|
|
161
|
+
successful: false,
|
|
162
|
+
returnMessage: {
|
|
163
|
+
messageType: 'warning',
|
|
164
|
+
message: 'Could not load user information. The platform does not support getUserInfo operation.'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('getUserList maps configured list responses and returns empty when unsupported', async () => {
|
|
170
|
+
const userListConfig = {
|
|
171
|
+
requestDefaults: { baseUrl: 'https://api.example.com' },
|
|
172
|
+
operations: {
|
|
173
|
+
getUserList: {
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: '/users',
|
|
176
|
+
responseMapping: {
|
|
177
|
+
listPath: 'body.records',
|
|
178
|
+
idPath: 'user.id',
|
|
179
|
+
namePath: 'profile.name',
|
|
180
|
+
emailPath: 'email'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
axios.mockResolvedValueOnce({
|
|
186
|
+
data: {
|
|
187
|
+
records: [
|
|
188
|
+
{ user: { id: 'u1' }, profile: { name: 'Ada' }, email: 'ada@example.com' },
|
|
189
|
+
{ user: { id: 'u2' }, profile: { name: 'Grace' }, email: 'grace@example.com' }
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await expect(proxy.getUserList({
|
|
195
|
+
user: { platformAdditionalInfo: { proxyId: 'p1' } },
|
|
196
|
+
authHeader: 'Bearer token',
|
|
197
|
+
proxyConfig: userListConfig
|
|
198
|
+
})).resolves.toEqual([
|
|
199
|
+
{ id: 'u1', name: 'Ada', email: 'ada@example.com' },
|
|
200
|
+
{ id: 'u2', name: 'Grace', email: 'grace@example.com' }
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
await expect(proxy.getUserList({
|
|
204
|
+
user: { platformAdditionalInfo: { proxyId: 'p1' } },
|
|
205
|
+
proxyConfig: { operations: {} }
|
|
206
|
+
})).resolves.toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
129
209
|
test('getUserInfo maps id/name/message/platformAdditionalInfo', async () => {
|
|
130
210
|
const cfg = JSON.parse(JSON.stringify(sampleConfig));
|
|
131
211
|
delete cfg.auth; // ensure provided authHeader is used
|
|
@@ -165,6 +245,37 @@ describe('proxy connector - more coverage', () => {
|
|
|
165
245
|
expect(out.matchedContactInfo[0].id).toBe('c1');
|
|
166
246
|
});
|
|
167
247
|
|
|
248
|
+
test('contact operations return supported empty responses when omitted from manifest', async () => {
|
|
249
|
+
const proxyConfig = { operations: {} };
|
|
250
|
+
const user = { accessToken: 't', platformAdditionalInfo: { proxyId: 'p1' } };
|
|
251
|
+
|
|
252
|
+
await expect(proxy.findContact({
|
|
253
|
+
user,
|
|
254
|
+
authHeader: 'x',
|
|
255
|
+
phoneNumber: '+1',
|
|
256
|
+
proxyConfig
|
|
257
|
+
})).resolves.toEqual({ successful: true, matchedContactInfo: [] });
|
|
258
|
+
|
|
259
|
+
await expect(proxy.findContactWithName({
|
|
260
|
+
user,
|
|
261
|
+
authHeader: 'x',
|
|
262
|
+
name: 'Ada',
|
|
263
|
+
proxyConfig
|
|
264
|
+
})).resolves.toEqual({ successful: true, matchedContactInfo: [] });
|
|
265
|
+
|
|
266
|
+
await expect(proxy.createContact({
|
|
267
|
+
user,
|
|
268
|
+
authHeader: 'x',
|
|
269
|
+
phoneNumber: '+1',
|
|
270
|
+
newContactName: 'Ada',
|
|
271
|
+
newContactType: 'Contact',
|
|
272
|
+
proxyConfig
|
|
273
|
+
})).resolves.toEqual({
|
|
274
|
+
contactInfo: null,
|
|
275
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
168
279
|
test('createContact maps object response', async () => {
|
|
169
280
|
Connector.getProxyConfig.mockResolvedValue(sampleConfig);
|
|
170
281
|
axios.mockResolvedValue({ data: { id: 'c2', name: 'Bob', type: 'Lead' } });
|
|
@@ -182,6 +293,70 @@ describe('proxy connector - more coverage', () => {
|
|
|
182
293
|
expect(res.returnMessage.messageType).toBe('success');
|
|
183
294
|
});
|
|
184
295
|
|
|
296
|
+
test('call and message log operations return Not supported when omitted from manifest', async () => {
|
|
297
|
+
const proxyConfig = { operations: {} };
|
|
298
|
+
const user = { accessToken: 't', platformAdditionalInfo: { proxyId: 'p1' } };
|
|
299
|
+
const commonCall = {
|
|
300
|
+
user,
|
|
301
|
+
contactInfo: { id: 'c1', name: 'Ada' },
|
|
302
|
+
authHeader: 'x',
|
|
303
|
+
proxyConfig
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
await expect(proxy.createCallLog({
|
|
307
|
+
...commonCall,
|
|
308
|
+
callLog: { direction: 'Inbound', startTime: Date.now(), duration: 30 },
|
|
309
|
+
note: ''
|
|
310
|
+
})).resolves.toEqual({
|
|
311
|
+
logId: undefined,
|
|
312
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await expect(proxy.getCallLog({
|
|
316
|
+
user,
|
|
317
|
+
callLogId: 'L1',
|
|
318
|
+
contactId: 'c1',
|
|
319
|
+
authHeader: 'x',
|
|
320
|
+
proxyConfig
|
|
321
|
+
})).resolves.toEqual({
|
|
322
|
+
callLogInfo: null,
|
|
323
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await expect(proxy.updateCallLog({
|
|
327
|
+
user,
|
|
328
|
+
existingCallLog: { thirdPartyLogId: 'L1' },
|
|
329
|
+
authHeader: 'x',
|
|
330
|
+
startTime: Date.now(),
|
|
331
|
+
duration: 30,
|
|
332
|
+
proxyConfig
|
|
333
|
+
})).resolves.toEqual({
|
|
334
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await expect(proxy.createMessageLog({
|
|
338
|
+
user,
|
|
339
|
+
contactInfo: { id: 'c1', name: 'Ada' },
|
|
340
|
+
authHeader: 'x',
|
|
341
|
+
message: { creationTime: Date.now() },
|
|
342
|
+
proxyConfig
|
|
343
|
+
})).resolves.toEqual({
|
|
344
|
+
logId: undefined,
|
|
345
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await expect(proxy.updateMessageLog({
|
|
349
|
+
user,
|
|
350
|
+
contactInfo: { id: 'c1', name: 'Ada' },
|
|
351
|
+
existingMessageLog: { thirdPartyLogId: 'M1' },
|
|
352
|
+
message: { creationTime: Date.now() },
|
|
353
|
+
authHeader: 'x',
|
|
354
|
+
proxyConfig
|
|
355
|
+
})).resolves.toEqual({
|
|
356
|
+
returnMessage: { message: 'Not supported', messageType: 'warning', ttl: 2000 }
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
185
360
|
test('createMessageLog maps idPath and updateMessageLog returns success', async () => {
|
|
186
361
|
Connector.getProxyConfig.mockResolvedValue(sampleConfig);
|
|
187
362
|
axios.mockResolvedValueOnce({ data: { activity: { id: 'M1' } } });
|
|
@@ -227,6 +402,40 @@ describe('proxy connector - more coverage', () => {
|
|
|
227
402
|
expect(res.returnMessage.message).toMatch(/Not supported/);
|
|
228
403
|
});
|
|
229
404
|
|
|
405
|
+
test('upsertCallDisposition performs configured operation when supported', async () => {
|
|
406
|
+
const dispositionConfig = {
|
|
407
|
+
requestDefaults: { baseUrl: 'https://api.example.com' },
|
|
408
|
+
operations: {
|
|
409
|
+
upsertCallDisposition: {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
url: '/activities/{{thirdPartyLogId}}/disposition',
|
|
412
|
+
body: {
|
|
413
|
+
result: '{{dispositions.0.value}}'
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
Connector.getProxyConfig.mockResolvedValue(dispositionConfig);
|
|
419
|
+
axios.mockResolvedValueOnce({ data: { ok: true } });
|
|
420
|
+
|
|
421
|
+
const res = await proxy.upsertCallDisposition({
|
|
422
|
+
user: { accessToken: 't', platformAdditionalInfo: { proxyId: 'p1' } },
|
|
423
|
+
existingCallLog: { thirdPartyLogId: 'L1' },
|
|
424
|
+
authHeader: 'x',
|
|
425
|
+
dispositions: [{ value: 'Connected' }]
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(res).toEqual({
|
|
429
|
+
logId: 'L1',
|
|
430
|
+
returnMessage: { message: 'Disposition updated', messageType: 'success', ttl: 2000 }
|
|
431
|
+
});
|
|
432
|
+
expect(axios.mock.calls[0][0]).toMatchObject({
|
|
433
|
+
method: 'POST',
|
|
434
|
+
url: 'https://api.example.com/activities/L1/disposition',
|
|
435
|
+
data: { result: 'Connected' }
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
230
439
|
test('getLicenseStatus maps values with custom config', async () => {
|
|
231
440
|
const licenseConfig = {
|
|
232
441
|
requestDefaults: { baseUrl: 'https://api.example.com' },
|
|
@@ -252,6 +461,23 @@ describe('proxy connector - more coverage', () => {
|
|
|
252
461
|
expect(s.licenseStatusDescription).toBe('All good');
|
|
253
462
|
});
|
|
254
463
|
|
|
464
|
+
test('getLicenseStatus handles missing users and missing license operations', async () => {
|
|
465
|
+
UserModel.findByPk.mockResolvedValueOnce(null);
|
|
466
|
+
await expect(proxy.getLicenseStatus({ userId: 'missing', platform: 'x' })).resolves.toEqual({
|
|
467
|
+
isLicenseValid: false,
|
|
468
|
+
licenseStatus: 'Invalid (User not found)',
|
|
469
|
+
licenseStatusDescription: ''
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
UserModel.findByPk.mockResolvedValueOnce({ id: 'u1', accessToken: 't', platformAdditionalInfo: { proxyId: 'p1' } });
|
|
473
|
+
Connector.getProxyConfig.mockResolvedValueOnce({ operations: {} });
|
|
474
|
+
await expect(proxy.getLicenseStatus({ userId: 'u1', platform: 'x' })).resolves.toEqual({
|
|
475
|
+
isLicenseValid: true,
|
|
476
|
+
licenseStatus: 'Basic',
|
|
477
|
+
licenseStatusDescription: ''
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
255
481
|
test('unAuthorize without custom op clears tokens and saves user', async () => {
|
|
256
482
|
Connector.getProxyConfig.mockResolvedValue(sampleConfig); // no unAuthorize op
|
|
257
483
|
const user = { accessToken: 't', refreshToken: 'r', save: jest.fn(), platformAdditionalInfo: { proxyId: 'p1' } };
|
|
@@ -275,5 +501,25 @@ describe('proxy connector - more coverage', () => {
|
|
|
275
501
|
expect(out.returnMessage.message).toMatch(/Logged out/);
|
|
276
502
|
expect(user.accessToken).toBe('');
|
|
277
503
|
});
|
|
504
|
+
|
|
505
|
+
test('unAuthorize returns a database warning when saving cleared tokens fails', async () => {
|
|
506
|
+
Connector.getProxyConfig.mockResolvedValue(sampleConfig);
|
|
507
|
+
const user = {
|
|
508
|
+
accessToken: 't',
|
|
509
|
+
refreshToken: 'r',
|
|
510
|
+
save: jest.fn().mockRejectedValue(new Error('save failed')),
|
|
511
|
+
platformAdditionalInfo: { proxyId: 'p1' }
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
await expect(proxy.unAuthorize({ user })).resolves.toMatchObject({
|
|
515
|
+
successful: false,
|
|
516
|
+
returnMessage: {
|
|
517
|
+
message: 'Database operation failed',
|
|
518
|
+
messageType: 'warning'
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
expect(user.accessToken).toBe('');
|
|
522
|
+
expect(user.refreshToken).toBe('');
|
|
523
|
+
});
|
|
278
524
|
});
|
|
279
525
|
|
|
@@ -29,6 +29,7 @@ const oauth = require('../../lib/oauth');
|
|
|
29
29
|
const { RingCentral } = require('../../lib/ringcentral');
|
|
30
30
|
const { Connector } = require('../../models/dynamo/connectorSchema');
|
|
31
31
|
const { sequelize } = require('../../models/sequelize');
|
|
32
|
+
const { getHashValue } = require('../../lib/util');
|
|
32
33
|
|
|
33
34
|
describe('Admin Handler', () => {
|
|
34
35
|
beforeAll(async () => {
|
|
@@ -612,5 +613,348 @@ describe('Admin Handler', () => {
|
|
|
612
613
|
expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
|
|
613
614
|
});
|
|
614
615
|
});
|
|
616
|
+
|
|
617
|
+
describe('reports and mapping branch coverage', () => {
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.example.com';
|
|
620
|
+
process.env.RINGCENTRAL_CLIENT_ID = 'client-id';
|
|
621
|
+
process.env.RINGCENTRAL_CLIENT_SECRET = 'client-secret';
|
|
622
|
+
process.env.APP_SERVER = 'https://app.example.com';
|
|
623
|
+
process.env.HASH_KEY = 'hash-key';
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
afterEach(() => {
|
|
627
|
+
delete process.env.RINGCENTRAL_SERVER;
|
|
628
|
+
delete process.env.RINGCENTRAL_CLIENT_ID;
|
|
629
|
+
delete process.env.RINGCENTRAL_CLIENT_SECRET;
|
|
630
|
+
delete process.env.APP_SERVER;
|
|
631
|
+
delete process.env.HASH_KEY;
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('getAdminReport builds aggregation rows and skips unnamed records', async () => {
|
|
635
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
636
|
+
await AdminConfigModel.create({
|
|
637
|
+
id: hashedAccountId,
|
|
638
|
+
adminAccessToken: 'admin-token',
|
|
639
|
+
adminRefreshToken: 'refresh-token',
|
|
640
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
641
|
+
});
|
|
642
|
+
const getCallsAggregationData = jest.fn().mockResolvedValue({
|
|
643
|
+
data: {
|
|
644
|
+
groupedBy: 'Users',
|
|
645
|
+
records: [
|
|
646
|
+
{},
|
|
647
|
+
{
|
|
648
|
+
info: { name: 'Agent 1' },
|
|
649
|
+
counters: {
|
|
650
|
+
callsByDirection: { values: { inbound: 2, outbound: 1 } },
|
|
651
|
+
callsByResponse: { values: { answered: 1 } }
|
|
652
|
+
},
|
|
653
|
+
timers: {
|
|
654
|
+
allCalls: { values: 90 }
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
info: { name: 'Agent 2' },
|
|
659
|
+
counters: {
|
|
660
|
+
callsByDirection: { values: { inbound: 0, outbound: 0 } },
|
|
661
|
+
callsByResponse: { values: { answered: 0 } }
|
|
662
|
+
},
|
|
663
|
+
timers: {
|
|
664
|
+
allCalls: { values: 0 }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
RingCentral.mockImplementation(() => ({
|
|
671
|
+
getCallsAggregationData
|
|
672
|
+
}));
|
|
673
|
+
|
|
674
|
+
const result = await adminHandler.getAdminReport({
|
|
675
|
+
rcAccountId: 'account',
|
|
676
|
+
timezone: 'UTC',
|
|
677
|
+
timeFrom: '2026-07-01T00:00:00Z',
|
|
678
|
+
timeTo: '2026-07-31T23:59:59Z',
|
|
679
|
+
groupBy: 'undefined'
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
expect(result.groupedBy).toBe('Users');
|
|
683
|
+
expect(result.itemKeys).toEqual(['Agent 1', 'Agent 2']);
|
|
684
|
+
expect(result.callLogStats[0]).toMatchObject({
|
|
685
|
+
name: 'Agent 1',
|
|
686
|
+
inboundCallCount: 2,
|
|
687
|
+
outboundCallCount: 1,
|
|
688
|
+
answeredCallCount: 1,
|
|
689
|
+
answeredCallPercentage: '50.00%',
|
|
690
|
+
totalTalkTime: '90.00',
|
|
691
|
+
averageTalkTime: '30.00'
|
|
692
|
+
});
|
|
693
|
+
expect(result.callLogStats[1]).toMatchObject({
|
|
694
|
+
name: 'Agent 2',
|
|
695
|
+
answeredCallPercentage: '0%',
|
|
696
|
+
totalTalkTime: 0,
|
|
697
|
+
averageTalkTime: 0
|
|
698
|
+
});
|
|
699
|
+
expect(getCallsAggregationData).toHaveBeenCalledWith(expect.objectContaining({
|
|
700
|
+
groupBy: 'Company'
|
|
701
|
+
}));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('getUserReport builds call and SMS stats', async () => {
|
|
705
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
706
|
+
await AdminConfigModel.create({
|
|
707
|
+
id: hashedAccountId,
|
|
708
|
+
adminAccessToken: 'admin-token',
|
|
709
|
+
adminRefreshToken: 'refresh-token',
|
|
710
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
711
|
+
});
|
|
712
|
+
RingCentral.mockImplementation(() => ({
|
|
713
|
+
getCallLogData: jest.fn().mockResolvedValue({
|
|
714
|
+
records: [
|
|
715
|
+
{ direction: 'Inbound', result: 'Call connected', duration: 120 },
|
|
716
|
+
{ direction: 'Inbound', result: 'Missed', duration: 0 },
|
|
717
|
+
{ direction: 'Outbound', result: 'Accepted', duration: 60 }
|
|
718
|
+
]
|
|
719
|
+
}),
|
|
720
|
+
getSMSData: jest.fn().mockResolvedValue({
|
|
721
|
+
records: [
|
|
722
|
+
{ direction: 'Outbound' },
|
|
723
|
+
{ direction: 'Inbound' },
|
|
724
|
+
{ direction: 'Inbound' }
|
|
725
|
+
]
|
|
726
|
+
})
|
|
727
|
+
}));
|
|
728
|
+
|
|
729
|
+
const result = await adminHandler.getUserReport({
|
|
730
|
+
rcAccountId: 'account',
|
|
731
|
+
rcExtensionId: '101',
|
|
732
|
+
timezone: 'UTC',
|
|
733
|
+
timeFrom: '2026-07-01T00:00:00Z',
|
|
734
|
+
timeTo: '2026-07-31T23:59:59Z'
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
expect(result).toEqual({
|
|
738
|
+
callLogStats: {
|
|
739
|
+
inboundCallCount: 2,
|
|
740
|
+
outboundCallCount: 1,
|
|
741
|
+
answeredCallCount: 1,
|
|
742
|
+
answeredCallPercentage: '50.00%',
|
|
743
|
+
totalTalkTime: 3,
|
|
744
|
+
averageTalkTime: 1
|
|
745
|
+
},
|
|
746
|
+
smsLogStats: {
|
|
747
|
+
smsSentCount: 1,
|
|
748
|
+
smsReceivedCount: 2
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test('report helpers return empty data on missing env or provider errors', async () => {
|
|
754
|
+
delete process.env.RINGCENTRAL_SERVER;
|
|
755
|
+
await expect(adminHandler.getAdminReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
756
|
+
callLogStats: {}
|
|
757
|
+
});
|
|
758
|
+
await expect(adminHandler.getUserReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
759
|
+
callLogStats: {}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.example.com';
|
|
763
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
764
|
+
await AdminConfigModel.create({
|
|
765
|
+
id: hashedAccountId,
|
|
766
|
+
adminAccessToken: 'admin-token',
|
|
767
|
+
adminRefreshToken: 'refresh-token',
|
|
768
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
769
|
+
});
|
|
770
|
+
RingCentral.mockImplementation(() => ({
|
|
771
|
+
getCallsAggregationData: jest.fn().mockRejectedValue(new Error('aggregation failed')),
|
|
772
|
+
getCallLogData: jest.fn().mockRejectedValue(new Error('call log failed'))
|
|
773
|
+
}));
|
|
774
|
+
|
|
775
|
+
await expect(adminHandler.getAdminReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
776
|
+
callLogStats: {}
|
|
777
|
+
});
|
|
778
|
+
await expect(adminHandler.getUserReport({ rcAccountId: 'account', rcExtensionId: '101' })).resolves.toBeNull();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('getUserMapping updates existing mappings, creates new matches, and handles oauth revocation', async () => {
|
|
782
|
+
await AdminConfigModel.create({
|
|
783
|
+
id: 'hashed-branch-mapping',
|
|
784
|
+
userMappings: [
|
|
785
|
+
{ crmUserId: 'crm-user-1', rcExtensionId: 'ext-existing' }
|
|
786
|
+
]
|
|
787
|
+
});
|
|
788
|
+
const user = {
|
|
789
|
+
platform: 'testCRM',
|
|
790
|
+
accessToken: 'api-key',
|
|
791
|
+
platformAdditionalInfo: {}
|
|
792
|
+
};
|
|
793
|
+
const connector = {
|
|
794
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
795
|
+
getBasicAuth: jest.fn(() => 'encoded-key'),
|
|
796
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
797
|
+
{ id: 'crm-user-1', name: 'Existing User', email: 'existing@example.com' },
|
|
798
|
+
{ id: 'crm-user-2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
799
|
+
{ id: 'crm-user-3', name: 'No Match', email: 'nomatch@example.com' }
|
|
800
|
+
])
|
|
801
|
+
};
|
|
802
|
+
connectorRegistry.getConnector.mockReturnValue(connector);
|
|
803
|
+
|
|
804
|
+
const result = await adminHandler.getUserMapping({
|
|
805
|
+
user,
|
|
806
|
+
hashedRcAccountId: 'hashed-branch-mapping',
|
|
807
|
+
rcExtensionList: [
|
|
808
|
+
{ id: 'ext-existing', name: 'Existing RC', email: 'existing@example.com', extensionNumber: '100' },
|
|
809
|
+
{ id: 'ext-jane', firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', extensionNumber: '101' }
|
|
810
|
+
]
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
expect(result).toHaveLength(3);
|
|
814
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
|
|
815
|
+
expect(result[1].rcUser[0].extensionId).toBe('ext-jane');
|
|
816
|
+
expect(result[2].rcUser).toEqual([]);
|
|
817
|
+
const updatedConfig = await AdminConfigModel.findByPk('hashed-branch-mapping');
|
|
818
|
+
expect(updatedConfig.userMappings).toEqual(expect.arrayContaining([
|
|
819
|
+
expect.objectContaining({ crmUserId: 'crm-user-2', rcExtensionId: ['ext-jane'] })
|
|
820
|
+
]));
|
|
821
|
+
|
|
822
|
+
connectorRegistry.getConnector.mockReturnValue({
|
|
823
|
+
getUserList: jest.fn(),
|
|
824
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
825
|
+
getOauthInfo: jest.fn().mockResolvedValue({})
|
|
826
|
+
});
|
|
827
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValueOnce(null);
|
|
828
|
+
await expect(adminHandler.getUserMapping({
|
|
829
|
+
user: {
|
|
830
|
+
platform: 'testCRM',
|
|
831
|
+
hostname: 'crm.example.com',
|
|
832
|
+
accessToken: 'expired-token',
|
|
833
|
+
platformAdditionalInfo: {}
|
|
834
|
+
},
|
|
835
|
+
hashedRcAccountId: 'hashed-branch-mapping',
|
|
836
|
+
rcExtensionList: []
|
|
837
|
+
})).resolves.toMatchObject({
|
|
838
|
+
successful: false,
|
|
839
|
+
isRevokeUserSession: true
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test('getUserMapping initializes mappings when no admin config exists and reports database errors', async () => {
|
|
844
|
+
const user = {
|
|
845
|
+
platform: 'testCRM',
|
|
846
|
+
accessToken: 'api-key',
|
|
847
|
+
platformAdditionalInfo: {}
|
|
848
|
+
};
|
|
849
|
+
connectorRegistry.getConnector.mockReturnValue({
|
|
850
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
851
|
+
getBasicAuth: jest.fn(() => 'encoded-key'),
|
|
852
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
853
|
+
{ id: 'crm-user-10', name: 'Alex Agent', email: 'alex@example.com' }
|
|
854
|
+
])
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const result = await adminHandler.getUserMapping({
|
|
858
|
+
user,
|
|
859
|
+
hashedRcAccountId: 'hashed-new-mapping',
|
|
860
|
+
rcExtensionList: [
|
|
861
|
+
{ id: 'ext-alex', firstName: 'Alex', lastName: 'Agent', email: 'alex@example.com', extensionNumber: '201' }
|
|
862
|
+
]
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-alex');
|
|
866
|
+
const newConfig = await AdminConfigModel.findByPk('hashed-new-mapping');
|
|
867
|
+
expect(newConfig.userMappings).toEqual([
|
|
868
|
+
{ crmUserId: 'crm-user-10', rcExtensionId: ['ext-alex'] }
|
|
869
|
+
]);
|
|
870
|
+
|
|
871
|
+
jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('read failed'));
|
|
872
|
+
const errorResult = await adminHandler.getUserMapping({
|
|
873
|
+
user,
|
|
874
|
+
hashedRcAccountId: 'hashed-error',
|
|
875
|
+
rcExtensionList: []
|
|
876
|
+
});
|
|
877
|
+
expect(errorResult.successful).toBe(false);
|
|
878
|
+
AdminConfigModel.findByPk.mockRestore();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test('reinitializeUserMapping handles missing capability, proxy limits, oauth revoke, apiKey remap, and update failure', async () => {
|
|
882
|
+
const baseUser = {
|
|
883
|
+
platform: 'testCRM',
|
|
884
|
+
accessToken: 'api-key',
|
|
885
|
+
platformAdditionalInfo: {}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
889
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
890
|
+
user: baseUser,
|
|
891
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
892
|
+
rcExtensionList: []
|
|
893
|
+
})).resolves.toEqual([]);
|
|
894
|
+
|
|
895
|
+
connectorRegistry.getConnector.mockReturnValueOnce({ getUserList: jest.fn() });
|
|
896
|
+
Connector.getProxyConfig.mockResolvedValueOnce({ operations: {} });
|
|
897
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
898
|
+
user: {
|
|
899
|
+
...baseUser,
|
|
900
|
+
platformAdditionalInfo: { proxyId: 'proxy-1' }
|
|
901
|
+
},
|
|
902
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
903
|
+
rcExtensionList: []
|
|
904
|
+
})).resolves.toEqual([]);
|
|
905
|
+
|
|
906
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
907
|
+
getUserList: jest.fn(),
|
|
908
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
909
|
+
getOauthInfo: jest.fn().mockResolvedValue({})
|
|
910
|
+
});
|
|
911
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValueOnce(null);
|
|
912
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
913
|
+
user: {
|
|
914
|
+
...baseUser,
|
|
915
|
+
hostname: 'crm.example.com'
|
|
916
|
+
},
|
|
917
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
918
|
+
rcExtensionList: []
|
|
919
|
+
})).resolves.toMatchObject({
|
|
920
|
+
successful: false,
|
|
921
|
+
isRevokeUserSession: true
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
925
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
926
|
+
{ id: 'crm-user-20', name: 'Matched User', email: 'matched@example.com' },
|
|
927
|
+
{ id: 'crm-user-21', name: 'Unmatched User', email: 'unmatched@example.com' }
|
|
928
|
+
]),
|
|
929
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
930
|
+
getBasicAuth: jest.fn(() => 'encoded-key')
|
|
931
|
+
});
|
|
932
|
+
const result = await adminHandler.reinitializeUserMapping({
|
|
933
|
+
user: baseUser,
|
|
934
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
935
|
+
rcExtensionList: [
|
|
936
|
+
{ id: 'ext-20', firstName: 'Matched', lastName: 'User', email: 'matched@example.com', extensionNumber: '301' }
|
|
937
|
+
]
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(result).toHaveLength(2);
|
|
941
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-20');
|
|
942
|
+
expect(result[1].rcUser).toEqual([]);
|
|
943
|
+
|
|
944
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
945
|
+
getUserList: jest.fn().mockResolvedValue([]),
|
|
946
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
947
|
+
getBasicAuth: jest.fn(() => 'encoded-key')
|
|
948
|
+
});
|
|
949
|
+
jest.spyOn(AdminConfigModel, 'create').mockRejectedValueOnce(new Error('write failed'));
|
|
950
|
+
const failure = await adminHandler.reinitializeUserMapping({
|
|
951
|
+
user: baseUser,
|
|
952
|
+
hashedRcAccountId: 'hashed-reinit-fail',
|
|
953
|
+
rcExtensionList: []
|
|
954
|
+
});
|
|
955
|
+
expect(failure.successful).toBe(false);
|
|
956
|
+
AdminConfigModel.create.mockRestore();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
615
959
|
});
|
|
616
960
|
|