@app-connect/core 1.7.19 → 1.7.21
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 +30 -55
- package/handlers/contact.js +7 -2
- package/handlers/log.js +3 -2
- package/index.js +19 -8
- package/mcp/README.md +5 -2
- package/mcp/mcpHandler.js +27 -2
- package/mcp/tools/checkAuthStatus.js +1 -1
- package/mcp/tools/getPublicConnectors.js +25 -7
- package/mcp/tools/logout.js +28 -4
- package/mcp/ui/App/App.tsx +2 -5
- package/mcp/ui/dist/index.html +10 -10
- package/models/userModel.js +3 -0
- package/package.json +1 -1
- package/releaseNotes.json +24 -0
- package/test/handlers/auth.test.js +31 -0
- package/test/handlers/contact.test.js +162 -0
- package/test/mcp/tools/logout.test.js +15 -9
package/models/userModel.js
CHANGED
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.7.21": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "Fix",
|
|
6
|
+
"description": "Refresh contact for deleted contact issue"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Fix",
|
|
10
|
+
"description": "Click-to-sms button issue"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"1.7.20": {
|
|
15
|
+
"global": [
|
|
16
|
+
{
|
|
17
|
+
"type": "Fix",
|
|
18
|
+
"description": "Click-to-dial not detecting numbers in input fields"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Fix",
|
|
22
|
+
"description": "Warm transfer call logging issue"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
2
26
|
"1.7.19": {
|
|
3
27
|
"global": [
|
|
4
28
|
{
|
|
@@ -501,6 +501,7 @@ describe('Auth Handler', () => {
|
|
|
501
501
|
describe('getLicenseStatus', () => {
|
|
502
502
|
test('should return license status from platform module', async () => {
|
|
503
503
|
// Arrange
|
|
504
|
+
const mockUser = global.testUtils.createMockUser({ id: 'user-123' });
|
|
504
505
|
const mockLicenseStatus = {
|
|
505
506
|
isValid: true,
|
|
506
507
|
expiresAt: '2025-12-31',
|
|
@@ -513,6 +514,9 @@ describe('Auth Handler', () => {
|
|
|
513
514
|
|
|
514
515
|
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
515
516
|
|
|
517
|
+
const { UserModel } = require('../../models/userModel');
|
|
518
|
+
jest.spyOn(UserModel, 'findByPk').mockResolvedValue(mockUser);
|
|
519
|
+
|
|
516
520
|
// Act
|
|
517
521
|
const result = await authHandler.getLicenseStatus({
|
|
518
522
|
userId: 'user-123',
|
|
@@ -523,8 +527,35 @@ describe('Auth Handler', () => {
|
|
|
523
527
|
expect(result).toEqual(mockLicenseStatus);
|
|
524
528
|
expect(mockConnector.getLicenseStatus).toHaveBeenCalledWith({
|
|
525
529
|
userId: 'user-123',
|
|
530
|
+
platform: 'testCRM',
|
|
531
|
+
user: mockUser
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('should return invalid license status when user not found', async () => {
|
|
536
|
+
// Arrange
|
|
537
|
+
const mockConnector = global.testUtils.createMockConnector({
|
|
538
|
+
getLicenseStatus: jest.fn()
|
|
539
|
+
});
|
|
540
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
541
|
+
|
|
542
|
+
const { UserModel } = require('../../models/userModel');
|
|
543
|
+
jest.spyOn(UserModel, 'findByPk').mockResolvedValue(null);
|
|
544
|
+
|
|
545
|
+
// Act
|
|
546
|
+
const result = await authHandler.getLicenseStatus({
|
|
547
|
+
userId: 'missing-user',
|
|
526
548
|
platform: 'testCRM'
|
|
527
549
|
});
|
|
550
|
+
|
|
551
|
+
// Assert
|
|
552
|
+
expect(result).toEqual({
|
|
553
|
+
isLicenseValid: false,
|
|
554
|
+
licenseStatus: 'Invalid (User not found)',
|
|
555
|
+
licenseStatusDescription: ''
|
|
556
|
+
});
|
|
557
|
+
expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
|
|
558
|
+
expect(mockConnector.getLicenseStatus).not.toHaveBeenCalled();
|
|
528
559
|
});
|
|
529
560
|
});
|
|
530
561
|
|
|
@@ -436,6 +436,168 @@ describe('Contact Handler', () => {
|
|
|
436
436
|
expect(cachedData.data).toEqual(updatedContact);
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
+
test('should remove cached contact when force refresh returns null', async () => {
|
|
440
|
+
// Arrange
|
|
441
|
+
await UserModel.create(mockUser);
|
|
442
|
+
await AccountDataModel.create({
|
|
443
|
+
rcAccountId: 'rc-account-123',
|
|
444
|
+
platformName: 'testCRM',
|
|
445
|
+
dataKey: 'contact-+3333333333',
|
|
446
|
+
data: [{ id: 'stale-contact', name: 'Stale Contact' }]
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const mockConnector = {
|
|
450
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
451
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
452
|
+
findContact: jest.fn().mockResolvedValue({
|
|
453
|
+
successful: false,
|
|
454
|
+
matchedContactInfo: null
|
|
455
|
+
})
|
|
456
|
+
};
|
|
457
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
458
|
+
|
|
459
|
+
// Act
|
|
460
|
+
const result = await contactHandler.findContact({
|
|
461
|
+
platform: 'testCRM',
|
|
462
|
+
userId: 'test-user-id',
|
|
463
|
+
phoneNumber: '+3333333333',
|
|
464
|
+
isForceRefreshAccountData: true
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Assert
|
|
468
|
+
expect(result.successful).toBe(false);
|
|
469
|
+
const cachedData = await AccountDataModel.findOne({
|
|
470
|
+
where: {
|
|
471
|
+
rcAccountId: 'rc-account-123',
|
|
472
|
+
platformName: 'testCRM',
|
|
473
|
+
dataKey: 'contact-+3333333333'
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
expect(cachedData).toBeNull();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('should remove cached contact when force refresh returns empty array', async () => {
|
|
480
|
+
// Arrange
|
|
481
|
+
await UserModel.create(mockUser);
|
|
482
|
+
await AccountDataModel.create({
|
|
483
|
+
rcAccountId: 'rc-account-123',
|
|
484
|
+
platformName: 'testCRM',
|
|
485
|
+
dataKey: 'contact-+1212121212',
|
|
486
|
+
data: [{ id: 'stale-contact', name: 'Stale Contact' }]
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const mockConnector = {
|
|
490
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
491
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
492
|
+
findContact: jest.fn().mockResolvedValue({
|
|
493
|
+
successful: false,
|
|
494
|
+
matchedContactInfo: []
|
|
495
|
+
})
|
|
496
|
+
};
|
|
497
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
498
|
+
|
|
499
|
+
// Act
|
|
500
|
+
await contactHandler.findContact({
|
|
501
|
+
platform: 'testCRM',
|
|
502
|
+
userId: 'test-user-id',
|
|
503
|
+
phoneNumber: '+1212121212',
|
|
504
|
+
isForceRefreshAccountData: true
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Assert
|
|
508
|
+
const cachedData = await AccountDataModel.findOne({
|
|
509
|
+
where: {
|
|
510
|
+
rcAccountId: 'rc-account-123',
|
|
511
|
+
platformName: 'testCRM',
|
|
512
|
+
dataKey: 'contact-+1212121212'
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
expect(cachedData).toBeNull();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('should remove cached contact when force refresh returns only new contact placeholder', async () => {
|
|
519
|
+
// Arrange
|
|
520
|
+
await UserModel.create(mockUser);
|
|
521
|
+
await AccountDataModel.create({
|
|
522
|
+
rcAccountId: 'rc-account-123',
|
|
523
|
+
platformName: 'testCRM',
|
|
524
|
+
dataKey: 'contact-+3434343434',
|
|
525
|
+
data: [{ id: 'stale-contact', name: 'Stale Contact' }]
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const placeholderContact = [
|
|
529
|
+
{ id: 'new-contact', name: 'Create Contact', isNewContact: true }
|
|
530
|
+
];
|
|
531
|
+
const mockConnector = {
|
|
532
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
533
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
534
|
+
findContact: jest.fn().mockResolvedValue({
|
|
535
|
+
successful: false,
|
|
536
|
+
matchedContactInfo: placeholderContact
|
|
537
|
+
})
|
|
538
|
+
};
|
|
539
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
540
|
+
|
|
541
|
+
// Act
|
|
542
|
+
const result = await contactHandler.findContact({
|
|
543
|
+
platform: 'testCRM',
|
|
544
|
+
userId: 'test-user-id',
|
|
545
|
+
phoneNumber: '+3434343434',
|
|
546
|
+
isForceRefreshAccountData: true
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Assert
|
|
550
|
+
expect(result.contact).toEqual(placeholderContact);
|
|
551
|
+
const cachedData = await AccountDataModel.findOne({
|
|
552
|
+
where: {
|
|
553
|
+
rcAccountId: 'rc-account-123',
|
|
554
|
+
platformName: 'testCRM',
|
|
555
|
+
dataKey: 'contact-+3434343434'
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
expect(cachedData).toBeNull();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('should keep cached contact when force refresh connector call errors', async () => {
|
|
562
|
+
// Arrange
|
|
563
|
+
await UserModel.create(mockUser);
|
|
564
|
+
await AccountDataModel.create({
|
|
565
|
+
rcAccountId: 'rc-account-123',
|
|
566
|
+
platformName: 'testCRM',
|
|
567
|
+
dataKey: 'contact-+5656565656',
|
|
568
|
+
data: [{ id: 'stale-contact', name: 'Stale Contact' }]
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const mockConnector = {
|
|
572
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
573
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
574
|
+
findContact: jest.fn().mockRejectedValue({
|
|
575
|
+
response: { status: 500 }
|
|
576
|
+
})
|
|
577
|
+
};
|
|
578
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
579
|
+
|
|
580
|
+
// Act
|
|
581
|
+
const result = await contactHandler.findContact({
|
|
582
|
+
platform: 'testCRM',
|
|
583
|
+
userId: 'test-user-id',
|
|
584
|
+
phoneNumber: '+5656565656',
|
|
585
|
+
isForceRefreshAccountData: true
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Assert
|
|
589
|
+
expect(result.successful).toBe(false);
|
|
590
|
+
const cachedData = await AccountDataModel.findOne({
|
|
591
|
+
where: {
|
|
592
|
+
rcAccountId: 'rc-account-123',
|
|
593
|
+
platformName: 'testCRM',
|
|
594
|
+
dataKey: 'contact-+5656565656'
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
expect(cachedData).not.toBeNull();
|
|
598
|
+
expect(cachedData.data).toEqual([{ id: 'stale-contact', name: 'Stale Contact' }]);
|
|
599
|
+
});
|
|
600
|
+
|
|
439
601
|
test('should work with tracer when provided', async () => {
|
|
440
602
|
// Arrange
|
|
441
603
|
await UserModel.create(mockUser);
|
|
@@ -94,8 +94,9 @@ describe('MCP Tool: logout', () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
test('should
|
|
97
|
+
test('should treat CRM unAuthorize failure as non-fatal and still succeed', async () => {
|
|
98
98
|
// Arrange
|
|
99
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
99
100
|
const mockUser = {
|
|
100
101
|
id: 'test-user-id',
|
|
101
102
|
platform: 'testCRM'
|
|
@@ -120,10 +121,12 @@ describe('MCP Tool: logout', () => {
|
|
|
120
121
|
jwtToken: 'mock-jwt-token'
|
|
121
122
|
});
|
|
122
123
|
|
|
123
|
-
// Assert
|
|
124
|
-
expect(result.success).toBe(
|
|
125
|
-
expect(result.
|
|
126
|
-
expect(
|
|
124
|
+
// Assert — local session is cleared; CRM revoke errors are logged only
|
|
125
|
+
expect(result.success).toBe(true);
|
|
126
|
+
expect(result.data.message).toContain('IMPORTANT');
|
|
127
|
+
expect(mockConnector.unAuthorize).toHaveBeenCalled();
|
|
128
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
129
|
+
consoleSpy.mockRestore();
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
test('should handle invalid JWT token', async () => {
|
|
@@ -140,8 +143,9 @@ describe('MCP Tool: logout', () => {
|
|
|
140
143
|
expect(result.error).toBeDefined();
|
|
141
144
|
});
|
|
142
145
|
|
|
143
|
-
test('should
|
|
146
|
+
test('should succeed when platform connector is missing (unAuthorize skipped)', async () => {
|
|
144
147
|
// Arrange
|
|
148
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
145
149
|
const mockUser = {
|
|
146
150
|
id: 'test-user-id',
|
|
147
151
|
platform: 'unknownCRM'
|
|
@@ -160,9 +164,11 @@ describe('MCP Tool: logout', () => {
|
|
|
160
164
|
jwtToken: 'mock-jwt-token'
|
|
161
165
|
});
|
|
162
166
|
|
|
163
|
-
// Assert
|
|
164
|
-
expect(result.success).toBe(
|
|
165
|
-
expect(result.
|
|
167
|
+
// Assert — null connector throws on unAuthorize; error is caught, logout still succeeds locally
|
|
168
|
+
expect(result.success).toBe(true);
|
|
169
|
+
expect(result.data.message).toContain('IMPORTANT');
|
|
170
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
171
|
+
consoleSpy.mockRestore();
|
|
166
172
|
});
|
|
167
173
|
});
|
|
168
174
|
});
|