@app-connect/core 1.7.8 → 1.7.10
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/handlers/auth.js +10 -4
- package/handlers/contact.js +42 -9
- package/handlers/log.js +4 -4
- package/index.js +101 -79
- package/lib/oauth.js +0 -2
- package/models/accountDataModel.js +34 -0
- package/package.json +70 -69
- package/releaseNotes.json +24 -0
- package/test/connector/registry.test.js +145 -0
- package/test/handlers/admin.test.js +583 -0
- package/test/handlers/auth.test.js +355 -0
- package/test/handlers/contact.test.js +852 -0
- package/test/handlers/log.test.js +868 -0
- package/test/lib/callLogComposer.test.js +1231 -0
- package/test/lib/debugTracer.test.js +328 -0
- package/test/lib/oauth.test.js +359 -0
- package/test/lib/ringcentral.test.js +473 -0
- package/test/lib/util.test.js +282 -0
- package/test/models/accountDataModel.test.js +98 -0
- package/test/models/dynamo/connectorSchema.test.js +189 -0
- package/test/models/models.test.js +539 -0
- package/test/setup.js +176 -176
|
@@ -3,12 +3,38 @@ const connectorRegistry = require('../../connector/registry');
|
|
|
3
3
|
|
|
4
4
|
// Mock the connector registry
|
|
5
5
|
jest.mock('../../connector/registry');
|
|
6
|
+
jest.mock('../../lib/oauth');
|
|
7
|
+
jest.mock('../../models/dynamo/connectorSchema', () => ({
|
|
8
|
+
Connector: {
|
|
9
|
+
getProxyConfig: jest.fn()
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
jest.mock('../../lib/ringcentral', () => ({
|
|
13
|
+
RingCentral: jest.fn().mockImplementation(() => ({
|
|
14
|
+
generateToken: jest.fn()
|
|
15
|
+
}))
|
|
16
|
+
}));
|
|
17
|
+
jest.mock('../../handlers/admin', () => ({
|
|
18
|
+
updateAdminRcTokens: jest.fn()
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const oauth = require('../../lib/oauth');
|
|
22
|
+
const { Connector } = require('../../models/dynamo/connectorSchema');
|
|
23
|
+
const { RingCentral } = require('../../lib/ringcentral');
|
|
24
|
+
const adminCore = require('../../handlers/admin');
|
|
6
25
|
|
|
7
26
|
describe('Auth Handler', () => {
|
|
27
|
+
const originalEnv = process.env;
|
|
28
|
+
|
|
8
29
|
beforeEach(() => {
|
|
9
30
|
// Reset mocks
|
|
10
31
|
jest.clearAllMocks();
|
|
11
32
|
global.testUtils.resetConnectorRegistry();
|
|
33
|
+
process.env = { ...originalEnv };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
process.env = originalEnv;
|
|
12
38
|
});
|
|
13
39
|
|
|
14
40
|
describe('onApiKeyLogin', () => {
|
|
@@ -230,4 +256,333 @@ describe('Auth Handler', () => {
|
|
|
230
256
|
expect(result.successful).toBe(false);
|
|
231
257
|
});
|
|
232
258
|
});
|
|
259
|
+
|
|
260
|
+
describe('onOAuthCallback', () => {
|
|
261
|
+
const mockOAuthApp = {
|
|
262
|
+
code: {
|
|
263
|
+
getToken: jest.fn()
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('should handle successful OAuth callback', async () => {
|
|
272
|
+
// Arrange
|
|
273
|
+
const mockUserInfo = {
|
|
274
|
+
successful: true,
|
|
275
|
+
platformUserInfo: {
|
|
276
|
+
id: 'oauth-user-id',
|
|
277
|
+
name: 'OAuth User',
|
|
278
|
+
timezoneName: 'America/New_York',
|
|
279
|
+
timezoneOffset: -300,
|
|
280
|
+
platformAdditionalInfo: {}
|
|
281
|
+
},
|
|
282
|
+
returnMessage: {
|
|
283
|
+
messageType: 'success',
|
|
284
|
+
message: 'Connected successfully'
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
289
|
+
getOauthInfo: jest.fn().mockResolvedValue({
|
|
290
|
+
clientId: 'client-id',
|
|
291
|
+
clientSecret: 'client-secret',
|
|
292
|
+
accessTokenUri: 'https://api.example.com/oauth/token',
|
|
293
|
+
authorizationUri: 'https://api.example.com/oauth/authorize'
|
|
294
|
+
}),
|
|
295
|
+
getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
299
|
+
|
|
300
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
301
|
+
accessToken: 'new-access-token',
|
|
302
|
+
refreshToken: 'new-refresh-token',
|
|
303
|
+
expires: new Date()
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const requestData = {
|
|
307
|
+
platform: 'testCRM',
|
|
308
|
+
hostname: 'api.example.com',
|
|
309
|
+
tokenUrl: 'https://api.example.com/oauth/token',
|
|
310
|
+
query: {
|
|
311
|
+
callbackUri: 'https://app.example.com/callback?code=auth-code-123',
|
|
312
|
+
rcAccountId: 'rc-account-123'
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Act
|
|
317
|
+
const result = await authHandler.onOAuthCallback(requestData);
|
|
318
|
+
|
|
319
|
+
// Assert
|
|
320
|
+
expect(result.userInfo).toBeDefined();
|
|
321
|
+
expect(result.userInfo.id).toBe('oauth-user-id');
|
|
322
|
+
expect(oauth.getOAuthApp).toHaveBeenCalled();
|
|
323
|
+
expect(mockOAuthApp.code.getToken).toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('should return fail message when oauthInfo has error', async () => {
|
|
327
|
+
// Arrange
|
|
328
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
329
|
+
getOauthInfo: jest.fn().mockResolvedValue({
|
|
330
|
+
failMessage: 'OAuth configuration not found'
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
335
|
+
|
|
336
|
+
const requestData = {
|
|
337
|
+
platform: 'testCRM',
|
|
338
|
+
hostname: 'api.example.com',
|
|
339
|
+
tokenUrl: '',
|
|
340
|
+
query: { callbackUri: 'https://app.example.com/callback', rcAccountId: 'rc-123' }
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Act
|
|
344
|
+
const result = await authHandler.onOAuthCallback(requestData);
|
|
345
|
+
|
|
346
|
+
// Assert
|
|
347
|
+
expect(result.userInfo).toBeNull();
|
|
348
|
+
expect(result.returnMessage.messageType).toBe('danger');
|
|
349
|
+
expect(result.returnMessage.message).toBe('OAuth configuration not found');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('should handle failed user info retrieval', async () => {
|
|
353
|
+
// Arrange
|
|
354
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
355
|
+
getOauthInfo: jest.fn().mockResolvedValue({ clientId: 'id', clientSecret: 'secret' }),
|
|
356
|
+
getUserInfo: jest.fn().mockResolvedValue({
|
|
357
|
+
successful: false,
|
|
358
|
+
returnMessage: { messageType: 'error', message: 'User not authorized' }
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
363
|
+
|
|
364
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
365
|
+
accessToken: 'token',
|
|
366
|
+
refreshToken: 'refresh',
|
|
367
|
+
expires: new Date()
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const requestData = {
|
|
371
|
+
platform: 'testCRM',
|
|
372
|
+
hostname: 'api.example.com',
|
|
373
|
+
tokenUrl: '',
|
|
374
|
+
query: { callbackUri: 'https://app.example.com/callback', rcAccountId: 'rc-123' }
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Act
|
|
378
|
+
const result = await authHandler.onOAuthCallback(requestData);
|
|
379
|
+
|
|
380
|
+
// Assert
|
|
381
|
+
expect(result.userInfo).toBeNull();
|
|
382
|
+
expect(result.returnMessage.message).toBe('User not authorized');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('should handle proxyId in OAuth callback', async () => {
|
|
386
|
+
// Arrange
|
|
387
|
+
const proxyConfig = { name: 'Proxy Config', settings: {} };
|
|
388
|
+
Connector.getProxyConfig.mockResolvedValue(proxyConfig);
|
|
389
|
+
|
|
390
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
391
|
+
getOauthInfo: jest.fn().mockResolvedValue({ clientId: 'id', clientSecret: 'secret' }),
|
|
392
|
+
getUserInfo: jest.fn().mockResolvedValue({
|
|
393
|
+
successful: true,
|
|
394
|
+
platformUserInfo: { id: 'proxy-user', name: 'Proxy User' },
|
|
395
|
+
returnMessage: { messageType: 'success', message: 'OK' }
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
400
|
+
|
|
401
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
402
|
+
accessToken: 'token',
|
|
403
|
+
refreshToken: 'refresh',
|
|
404
|
+
expires: new Date()
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const requestData = {
|
|
408
|
+
platform: 'testCRM',
|
|
409
|
+
hostname: 'api.example.com',
|
|
410
|
+
tokenUrl: '',
|
|
411
|
+
query: {
|
|
412
|
+
callbackUri: 'https://app.example.com/callback',
|
|
413
|
+
proxyId: 'proxy-123',
|
|
414
|
+
rcAccountId: 'rc-123'
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Act
|
|
419
|
+
await authHandler.onOAuthCallback(requestData);
|
|
420
|
+
|
|
421
|
+
// Assert
|
|
422
|
+
expect(Connector.getProxyConfig).toHaveBeenCalledWith('proxy-123');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('should call postSaveUserInfo if platform implements it', async () => {
|
|
426
|
+
// Arrange
|
|
427
|
+
const postSaveResult = { id: 'user-id', name: 'User', extra: 'data' };
|
|
428
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
429
|
+
getOauthInfo: jest.fn().mockResolvedValue({ clientId: 'id', clientSecret: 'secret' }),
|
|
430
|
+
getUserInfo: jest.fn().mockResolvedValue({
|
|
431
|
+
successful: true,
|
|
432
|
+
platformUserInfo: { id: 'user-id', name: 'User' },
|
|
433
|
+
returnMessage: { messageType: 'success', message: 'OK' }
|
|
434
|
+
}),
|
|
435
|
+
postSaveUserInfo: jest.fn().mockResolvedValue(postSaveResult)
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
439
|
+
|
|
440
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
441
|
+
accessToken: 'token',
|
|
442
|
+
refreshToken: 'refresh',
|
|
443
|
+
expires: new Date()
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const requestData = {
|
|
447
|
+
platform: 'testCRM',
|
|
448
|
+
hostname: 'api.example.com',
|
|
449
|
+
tokenUrl: '',
|
|
450
|
+
query: { callbackUri: 'https://app.example.com/callback', rcAccountId: 'rc-123' }
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Act
|
|
454
|
+
const result = await authHandler.onOAuthCallback(requestData);
|
|
455
|
+
|
|
456
|
+
// Assert
|
|
457
|
+
expect(mockConnector.postSaveUserInfo).toHaveBeenCalled();
|
|
458
|
+
expect(result.userInfo).toEqual(postSaveResult);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('should use overriding OAuth option if provided', async () => {
|
|
462
|
+
// Arrange
|
|
463
|
+
const overridingOption = { redirect_uri: 'custom-redirect' };
|
|
464
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
465
|
+
getOauthInfo: jest.fn().mockResolvedValue({ clientId: 'id', clientSecret: 'secret' }),
|
|
466
|
+
getOverridingOAuthOption: jest.fn().mockReturnValue(overridingOption),
|
|
467
|
+
getUserInfo: jest.fn().mockResolvedValue({
|
|
468
|
+
successful: true,
|
|
469
|
+
platformUserInfo: { id: 'user-id', name: 'User' },
|
|
470
|
+
returnMessage: { messageType: 'success', message: 'OK' }
|
|
471
|
+
})
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
475
|
+
|
|
476
|
+
mockOAuthApp.code.getToken.mockResolvedValue({
|
|
477
|
+
accessToken: 'token',
|
|
478
|
+
refreshToken: 'refresh',
|
|
479
|
+
expires: new Date()
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const requestData = {
|
|
483
|
+
platform: 'testCRM',
|
|
484
|
+
hostname: 'api.example.com',
|
|
485
|
+
tokenUrl: '',
|
|
486
|
+
query: { callbackUri: 'https://app.example.com/callback?code=code123', rcAccountId: 'rc-123' }
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Act
|
|
490
|
+
await authHandler.onOAuthCallback(requestData);
|
|
491
|
+
|
|
492
|
+
// Assert
|
|
493
|
+
expect(mockConnector.getOverridingOAuthOption).toHaveBeenCalledWith({ code: 'code123' });
|
|
494
|
+
expect(mockOAuthApp.code.getToken).toHaveBeenCalledWith(
|
|
495
|
+
expect.any(String),
|
|
496
|
+
overridingOption
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('getLicenseStatus', () => {
|
|
502
|
+
test('should return license status from platform module', async () => {
|
|
503
|
+
// Arrange
|
|
504
|
+
const mockLicenseStatus = {
|
|
505
|
+
isValid: true,
|
|
506
|
+
expiresAt: '2025-12-31',
|
|
507
|
+
features: ['call_logging', 'sms_logging']
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
511
|
+
getLicenseStatus: jest.fn().mockResolvedValue(mockLicenseStatus)
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
515
|
+
|
|
516
|
+
// Act
|
|
517
|
+
const result = await authHandler.getLicenseStatus({
|
|
518
|
+
userId: 'user-123',
|
|
519
|
+
platform: 'testCRM'
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Assert
|
|
523
|
+
expect(result).toEqual(mockLicenseStatus);
|
|
524
|
+
expect(mockConnector.getLicenseStatus).toHaveBeenCalledWith({
|
|
525
|
+
userId: 'user-123',
|
|
526
|
+
platform: 'testCRM'
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('onRingcentralOAuthCallback', () => {
|
|
532
|
+
beforeEach(() => {
|
|
533
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
|
|
534
|
+
process.env.RINGCENTRAL_CLIENT_ID = 'rc-client-id';
|
|
535
|
+
process.env.RINGCENTRAL_CLIENT_SECRET = 'rc-client-secret';
|
|
536
|
+
process.env.APP_SERVER = 'https://app.example.com';
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('should handle successful RingCentral OAuth callback', async () => {
|
|
540
|
+
// Arrange
|
|
541
|
+
const mockGenerateToken = jest.fn().mockResolvedValue({
|
|
542
|
+
access_token: 'rc-access-token',
|
|
543
|
+
refresh_token: 'rc-refresh-token',
|
|
544
|
+
expire_time: Date.now() + 3600000
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
RingCentral.mockImplementation(() => ({
|
|
548
|
+
generateToken: mockGenerateToken
|
|
549
|
+
}));
|
|
550
|
+
|
|
551
|
+
// Act
|
|
552
|
+
await authHandler.onRingcentralOAuthCallback({
|
|
553
|
+
code: 'rc-auth-code',
|
|
554
|
+
rcAccountId: 'hashed-rc-account-id'
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Assert
|
|
558
|
+
expect(RingCentral).toHaveBeenCalledWith({
|
|
559
|
+
server: 'https://platform.ringcentral.com',
|
|
560
|
+
clientId: 'rc-client-id',
|
|
561
|
+
clientSecret: 'rc-client-secret',
|
|
562
|
+
redirectUri: 'https://app.example.com/ringcentral/oauth/callback'
|
|
563
|
+
});
|
|
564
|
+
expect(mockGenerateToken).toHaveBeenCalledWith({ code: 'rc-auth-code' });
|
|
565
|
+
expect(adminCore.updateAdminRcTokens).toHaveBeenCalledWith({
|
|
566
|
+
hashedRcAccountId: 'hashed-rc-account-id',
|
|
567
|
+
adminAccessToken: 'rc-access-token',
|
|
568
|
+
adminRefreshToken: 'rc-refresh-token',
|
|
569
|
+
adminTokenExpiry: expect.any(Number)
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('should return early if environment variables are not set', async () => {
|
|
574
|
+
// Arrange
|
|
575
|
+
delete process.env.RINGCENTRAL_SERVER;
|
|
576
|
+
|
|
577
|
+
// Act
|
|
578
|
+
const result = await authHandler.onRingcentralOAuthCallback({
|
|
579
|
+
code: 'rc-auth-code',
|
|
580
|
+
rcAccountId: 'hashed-rc-account-id'
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Assert
|
|
584
|
+
expect(result).toBeUndefined();
|
|
585
|
+
expect(RingCentral).not.toHaveBeenCalled();
|
|
586
|
+
});
|
|
587
|
+
});
|
|
233
588
|
});
|