@app-connect/core 1.7.35 → 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 +16 -6
- package/handlers/plugin.js +13 -6
- package/index.js +10 -4
- 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 +16 -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/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +324 -1
- 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 +102 -0
- 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
|
@@ -38,6 +38,7 @@ const { composeCallLog } = require('../../lib/callLogComposer');
|
|
|
38
38
|
const { NoteCache } = require('../../models/dynamo/noteCacheSchema');
|
|
39
39
|
const axios = require('axios');
|
|
40
40
|
const { sequelize } = require('../../models/sequelize');
|
|
41
|
+
const { getHashValue } = require('../../lib/util');
|
|
41
42
|
|
|
42
43
|
describe('Log Handler', () => {
|
|
43
44
|
beforeAll(async () => {
|
|
@@ -170,6 +171,82 @@ describe('Log Handler', () => {
|
|
|
170
171
|
expect(savedLog.thirdPartyLogId).toBe('new-log-102');
|
|
171
172
|
});
|
|
172
173
|
|
|
174
|
+
test('should block duplicate client log when SSCL already logged same hashed extension', async () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const hashedExtensionId = getHashValue('rc-ext-1', 'test-hash-key');
|
|
177
|
+
await CallLogModel.create({
|
|
178
|
+
id: 'tel-session-123',
|
|
179
|
+
sessionId: 'session-123',
|
|
180
|
+
extensionNumber: '',
|
|
181
|
+
hashedExtensionId,
|
|
182
|
+
platform: 'testCRM',
|
|
183
|
+
thirdPartyLogId: 'sscl-log',
|
|
184
|
+
userId: 'test-user-id'
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const incomingData = {
|
|
188
|
+
...mockIncomingData,
|
|
189
|
+
extensionNumber: '101',
|
|
190
|
+
hashedExtensionId
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
const result = await logHandler.createCallLog({
|
|
195
|
+
platform: 'testCRM',
|
|
196
|
+
userId: 'test-user-id',
|
|
197
|
+
incomingData,
|
|
198
|
+
hashedAccountId: 'hashed-123',
|
|
199
|
+
isFromSSCL: false
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Assert
|
|
203
|
+
expect(result.successful).toBe(false);
|
|
204
|
+
expect(result.returnMessage.message).toContain('Existing log for session');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('should derive hashed extension id from SSCL raw rcExtensionId when saving', async () => {
|
|
208
|
+
// Arrange
|
|
209
|
+
process.env.HASH_KEY = 'test-hash-key';
|
|
210
|
+
await UserModel.create(mockUser);
|
|
211
|
+
|
|
212
|
+
const mockConnector = {
|
|
213
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
214
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
215
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
216
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
217
|
+
logId: 'new-sscl-log',
|
|
218
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
222
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
223
|
+
|
|
224
|
+
const incomingData = {
|
|
225
|
+
...mockIncomingData,
|
|
226
|
+
rcExtensionId: 'rc-ext-1'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Act
|
|
230
|
+
const result = await logHandler.createCallLog({
|
|
231
|
+
platform: 'testCRM',
|
|
232
|
+
userId: 'test-user-id',
|
|
233
|
+
incomingData,
|
|
234
|
+
hashedAccountId: 'hashed-123',
|
|
235
|
+
isFromSSCL: true
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Assert
|
|
239
|
+
expect(result.successful).toBe(true);
|
|
240
|
+
const savedLog = await CallLogModel.findOne({
|
|
241
|
+
where: {
|
|
242
|
+
sessionId: 'session-123',
|
|
243
|
+
hashedExtensionId: getHashValue('rc-ext-1', 'test-hash-key')
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
expect(savedLog).not.toBeNull();
|
|
247
|
+
expect(savedLog.thirdPartyLogId).toBe('new-sscl-log');
|
|
248
|
+
});
|
|
249
|
+
|
|
173
250
|
test('should return warning when user not found', async () => {
|
|
174
251
|
// Act
|
|
175
252
|
const result = await logHandler.createCallLog({
|
|
@@ -419,6 +496,53 @@ describe('Log Handler', () => {
|
|
|
419
496
|
}
|
|
420
497
|
});
|
|
421
498
|
|
|
499
|
+
test('should mark async plugin task failed when token sync URL is missing', async () => {
|
|
500
|
+
await UserModel.create(mockUser);
|
|
501
|
+
await AccountDataModel.create({
|
|
502
|
+
rcAccountId: mockUser.rcAccountId,
|
|
503
|
+
platformName: 'asyncPlugin',
|
|
504
|
+
dataKey: 'pluginData',
|
|
505
|
+
data: {
|
|
506
|
+
name: 'plugin.async',
|
|
507
|
+
supportedLogTypes: ['call'],
|
|
508
|
+
isAsync: true,
|
|
509
|
+
endpointUrl: 'https://plugins.example.com/plugin/asyncPlugin',
|
|
510
|
+
jwtToken: 'plugin-jwt-token'
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const mockConnector = {
|
|
515
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
516
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
517
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
518
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
519
|
+
logId: 'new-log-async-failed',
|
|
520
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
521
|
+
})
|
|
522
|
+
};
|
|
523
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
524
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
525
|
+
|
|
526
|
+
const result = await logHandler.createCallLog({
|
|
527
|
+
platform: 'testCRM',
|
|
528
|
+
userId: 'test-user-id',
|
|
529
|
+
incomingData: mockIncomingData,
|
|
530
|
+
hashedAccountId: 'hashed-123',
|
|
531
|
+
isFromSSCL: false
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(result.successful).toBe(true);
|
|
535
|
+
const cache = await CacheModel.findOne({
|
|
536
|
+
where: {
|
|
537
|
+
cacheKey: 'asyncPluginTask-asyncPlugin'
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
expect(cache).not.toBeNull();
|
|
541
|
+
expect(cache.status).toBe('failed');
|
|
542
|
+
expect(cache.data.message).toBe('Plugin token sync URL is not set');
|
|
543
|
+
expect(axios.post).not.toHaveBeenCalled();
|
|
544
|
+
});
|
|
545
|
+
|
|
422
546
|
test('should successfully create call log with oauth auth', async () => {
|
|
423
547
|
// Arrange
|
|
424
548
|
const oauthUser = { ...mockUser };
|
|
@@ -617,6 +741,43 @@ describe('Log Handler', () => {
|
|
|
617
741
|
}]);
|
|
618
742
|
});
|
|
619
743
|
|
|
744
|
+
test('should match SSCL log by hashedExtensionId when extensionNumber differs', async () => {
|
|
745
|
+
// Arrange
|
|
746
|
+
await UserModel.create({
|
|
747
|
+
id: 'test-user-id',
|
|
748
|
+
platform: 'testCRM',
|
|
749
|
+
accessToken: 'test-token'
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await CallLogModel.create({
|
|
753
|
+
id: 'call-1',
|
|
754
|
+
sessionId: 'session-1',
|
|
755
|
+
extensionNumber: '',
|
|
756
|
+
hashedExtensionId: 'hashed-ext-1',
|
|
757
|
+
platform: 'testCRM',
|
|
758
|
+
thirdPartyLogId: 'sscl-log',
|
|
759
|
+
userId: 'test-user-id'
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Act
|
|
763
|
+
const result = await logHandler.getCallLog({
|
|
764
|
+
userId: 'test-user-id',
|
|
765
|
+
sessionIds: 'session-1',
|
|
766
|
+
extensionNumber: '101',
|
|
767
|
+
hashedExtensionId: 'hashed-ext-1',
|
|
768
|
+
platform: 'testCRM',
|
|
769
|
+
requireDetails: false
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Assert
|
|
773
|
+
expect(result.successful).toBe(true);
|
|
774
|
+
expect(result.logs).toEqual([{
|
|
775
|
+
sessionId: 'session-1',
|
|
776
|
+
matched: true,
|
|
777
|
+
logId: 'sscl-log'
|
|
778
|
+
}]);
|
|
779
|
+
});
|
|
780
|
+
|
|
620
781
|
test('should keep session-only lookup backward compatible when extensionNumber is empty', async () => {
|
|
621
782
|
// Arrange
|
|
622
783
|
await UserModel.create({
|
|
@@ -1291,6 +1452,21 @@ describe('Log Handler', () => {
|
|
|
1291
1452
|
});
|
|
1292
1453
|
|
|
1293
1454
|
describe('handleAsyncPluginCallback', () => {
|
|
1455
|
+
test('should return not found when callback task cache is missing', async () => {
|
|
1456
|
+
const result = await logHandler.handleAsyncPluginCallback({
|
|
1457
|
+
taskId: 'missing-task',
|
|
1458
|
+
body: {
|
|
1459
|
+
successful: true,
|
|
1460
|
+
note: 'Callback note'
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
expect(result).toEqual({
|
|
1465
|
+
statusCode: 404,
|
|
1466
|
+
body: { successful: false, message: 'Async task not found' }
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1294
1470
|
test('should append callback note to call log and remove task cache on success', async () => {
|
|
1295
1471
|
await UserModel.create({
|
|
1296
1472
|
id: 'test-user-id',
|
|
@@ -1412,6 +1588,154 @@ describe('Log Handler', () => {
|
|
|
1412
1588
|
expect(cache.data.message).toBe('Plugin failed');
|
|
1413
1589
|
});
|
|
1414
1590
|
|
|
1591
|
+
test('should destroy expired task cache and return not found', async () => {
|
|
1592
|
+
await CacheModel.create({
|
|
1593
|
+
id: 'task-expired',
|
|
1594
|
+
status: 'pending',
|
|
1595
|
+
userId: 'test-user-id',
|
|
1596
|
+
cacheKey: 'asyncPluginTask-testPlugin',
|
|
1597
|
+
expiry: new Date(Date.now() - 1000),
|
|
1598
|
+
data: {
|
|
1599
|
+
pluginId: 'testPlugin',
|
|
1600
|
+
platform: 'testCRM',
|
|
1601
|
+
userId: 'test-user-id',
|
|
1602
|
+
sessionId: 'session-1'
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
const result = await logHandler.handleAsyncPluginCallback({
|
|
1607
|
+
taskId: 'task-expired',
|
|
1608
|
+
body: {
|
|
1609
|
+
successful: true,
|
|
1610
|
+
note: 'Late callback note'
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
expect(result).toEqual({
|
|
1615
|
+
statusCode: 404,
|
|
1616
|
+
body: { successful: false, message: 'Async task not found' }
|
|
1617
|
+
});
|
|
1618
|
+
expect(await CacheModel.findByPk('task-expired')).toBeNull();
|
|
1619
|
+
expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test('should mark task failed when callback cannot find the original call log', async () => {
|
|
1623
|
+
await CacheModel.create({
|
|
1624
|
+
id: 'task-missing-log',
|
|
1625
|
+
status: 'pending',
|
|
1626
|
+
userId: 'test-user-id',
|
|
1627
|
+
cacheKey: 'asyncPluginTask-testPlugin',
|
|
1628
|
+
expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
1629
|
+
data: {
|
|
1630
|
+
pluginId: 'testPlugin',
|
|
1631
|
+
platform: 'testCRM',
|
|
1632
|
+
userId: 'test-user-id',
|
|
1633
|
+
sessionId: 'missing-session',
|
|
1634
|
+
extensionNumber: '',
|
|
1635
|
+
incomingData: {
|
|
1636
|
+
logInfo: {
|
|
1637
|
+
sessionId: 'missing-session'
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
const result = await logHandler.handleAsyncPluginCallback({
|
|
1644
|
+
taskId: 'task-missing-log',
|
|
1645
|
+
body: {
|
|
1646
|
+
successful: true,
|
|
1647
|
+
note: 'Callback note'
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
expect(result).toEqual({
|
|
1652
|
+
statusCode: 500,
|
|
1653
|
+
body: { successful: false, message: 'Call log not found for async plugin task' }
|
|
1654
|
+
});
|
|
1655
|
+
const cache = await CacheModel.findByPk('task-missing-log');
|
|
1656
|
+
expect(cache.status).toBe('failed');
|
|
1657
|
+
expect(cache.data.message).toBe('Call log not found for async plugin task');
|
|
1658
|
+
expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
test('should mark task failed when callback note cannot be written to CRM', async () => {
|
|
1662
|
+
await UserModel.create({
|
|
1663
|
+
id: 'test-user-id',
|
|
1664
|
+
platform: 'testCRM',
|
|
1665
|
+
accessToken: 'test-token',
|
|
1666
|
+
platformAdditionalInfo: {}
|
|
1667
|
+
});
|
|
1668
|
+
await CallLogModel.create({
|
|
1669
|
+
id: 'call-1',
|
|
1670
|
+
sessionId: 'session-1',
|
|
1671
|
+
extensionNumber: '',
|
|
1672
|
+
platform: 'testCRM',
|
|
1673
|
+
thirdPartyLogId: 'log-1',
|
|
1674
|
+
userId: 'test-user-id',
|
|
1675
|
+
contactId: 'contact-1'
|
|
1676
|
+
});
|
|
1677
|
+
await CacheModel.create({
|
|
1678
|
+
id: 'task-update-failed',
|
|
1679
|
+
status: 'pending',
|
|
1680
|
+
userId: 'test-user-id',
|
|
1681
|
+
cacheKey: 'asyncPluginTask-testPlugin',
|
|
1682
|
+
expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
1683
|
+
data: {
|
|
1684
|
+
pluginId: 'testPlugin',
|
|
1685
|
+
platform: 'testCRM',
|
|
1686
|
+
userId: 'test-user-id',
|
|
1687
|
+
sessionId: 'session-1',
|
|
1688
|
+
extensionNumber: '',
|
|
1689
|
+
incomingData: {
|
|
1690
|
+
logInfo: {
|
|
1691
|
+
sessionId: 'session-1',
|
|
1692
|
+
startTime: '2026-06-12T10:00:00.000Z',
|
|
1693
|
+
duration: 120,
|
|
1694
|
+
result: 'Completed',
|
|
1695
|
+
direction: 'Outbound',
|
|
1696
|
+
from: { phoneNumber: '+1234567890' },
|
|
1697
|
+
to: { phoneNumber: '+1987654321' }
|
|
1698
|
+
},
|
|
1699
|
+
note: 'Original note'
|
|
1700
|
+
},
|
|
1701
|
+
hashedAccountId: 'hashed-123',
|
|
1702
|
+
isFromSSCL: false
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
const mockConnector = {
|
|
1706
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
1707
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
1708
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
1709
|
+
getCallLog: jest.fn().mockResolvedValue({
|
|
1710
|
+
callLogInfo: {
|
|
1711
|
+
fullBody: 'Existing body',
|
|
1712
|
+
note: 'Existing note',
|
|
1713
|
+
fullLogResponse: { id: 'log-1' }
|
|
1714
|
+
}
|
|
1715
|
+
}),
|
|
1716
|
+
updateCallLog: jest.fn().mockRejectedValue(new Error('CRM update failed'))
|
|
1717
|
+
};
|
|
1718
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
1719
|
+
composeCallLog.mockReturnValue('Updated composed log');
|
|
1720
|
+
|
|
1721
|
+
const result = await logHandler.handleAsyncPluginCallback({
|
|
1722
|
+
taskId: 'task-update-failed',
|
|
1723
|
+
body: {
|
|
1724
|
+
successful: true,
|
|
1725
|
+
note: 'Callback note'
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
expect(result).toEqual({
|
|
1730
|
+
statusCode: 500,
|
|
1731
|
+
body: { successful: false, message: 'CRM update failed' }
|
|
1732
|
+
});
|
|
1733
|
+
const cache = await CacheModel.findByPk('task-update-failed');
|
|
1734
|
+
expect(cache).not.toBeNull();
|
|
1735
|
+
expect(cache.status).toBe('failed');
|
|
1736
|
+
expect(cache.data.message).toBe('CRM update failed');
|
|
1737
|
+
expect(mockConnector.updateCallLog).toHaveBeenCalled();
|
|
1738
|
+
});
|
|
1415
1739
|
test('should reject callback without successful boolean', async () => {
|
|
1416
1740
|
const result = await logHandler.handleAsyncPluginCallback({
|
|
1417
1741
|
taskId: 'task-1',
|
|
@@ -1521,4 +1845,3 @@ describe('Log Handler', () => {
|
|
|
1521
1845
|
});
|
|
1522
1846
|
});
|
|
1523
1847
|
});
|
|
1524
|
-
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
jest.mock('../../models/sequelize', () => {
|
|
2
|
+
const { Sequelize } = require('sequelize');
|
|
3
|
+
return {
|
|
4
|
+
sequelize: new Sequelize({
|
|
5
|
+
dialect: 'sqlite',
|
|
6
|
+
storage: ':memory:',
|
|
7
|
+
logging: false
|
|
8
|
+
})
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const managedOAuth = require('../../handlers/managedOAuth');
|
|
13
|
+
const { AccountDataModel } = require('../../models/accountDataModel');
|
|
14
|
+
const { CacheModel } = require('../../models/cacheModel');
|
|
15
|
+
const { sequelize } = require('../../models/sequelize');
|
|
16
|
+
|
|
17
|
+
describe('Managed OAuth Handler', () => {
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
process.env.APP_SERVER_SECRET_KEY = 'test-app-server-secret-key-123456';
|
|
20
|
+
await AccountDataModel.sync({ force: true });
|
|
21
|
+
await CacheModel.sync({ force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await AccountDataModel.destroy({ where: {} });
|
|
26
|
+
await CacheModel.destroy({ where: {} });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await sequelize.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('returns empty state when account and platform are missing', async () => {
|
|
34
|
+
await expect(managedOAuth.getManagedOAuthState({
|
|
35
|
+
rcAccountId: '',
|
|
36
|
+
platform: '',
|
|
37
|
+
isAdmin: false
|
|
38
|
+
})).resolves.toEqual({
|
|
39
|
+
isAdmin: false,
|
|
40
|
+
hasAccountOAuth: false,
|
|
41
|
+
hasPendingOAuth: false
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await expect(managedOAuth.clearPendingManagedOAuth({})).resolves.toBe(0);
|
|
45
|
+
await expect(managedOAuth.clearAccountManagedOAuth({ rcAccountId: 'acc-1' })).resolves.toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('upserts pending OAuth values, filters empty fields, and exposes pending values to admin', async () => {
|
|
49
|
+
const firstRecord = await managedOAuth.upsertPendingManagedOAuth({
|
|
50
|
+
rcAccountId: 'acc-1',
|
|
51
|
+
values: {
|
|
52
|
+
clientId: 'client-id',
|
|
53
|
+
clientSecret: 'client-secret',
|
|
54
|
+
accessTokenUri: '',
|
|
55
|
+
authorizationUri: 'https://auth.example.com',
|
|
56
|
+
ignored: 'ignored'
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(firstRecord.id).toBe('acc-1-managed-oauth-account');
|
|
61
|
+
|
|
62
|
+
const secondRecord = await managedOAuth.upsertPendingManagedOAuth({
|
|
63
|
+
rcAccountId: 'acc-1',
|
|
64
|
+
values: {
|
|
65
|
+
clientId: 'client-id-2',
|
|
66
|
+
redirectUri: 'https://app.example.com/callback',
|
|
67
|
+
scopes: ['read', 'write'],
|
|
68
|
+
hostname: 'crm.example.com'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(secondRecord.id).toBe(firstRecord.id);
|
|
73
|
+
|
|
74
|
+
const pendingValues = await managedOAuth.getPendingManagedOAuthValues({ rcAccountId: 'acc-1' });
|
|
75
|
+
expect(pendingValues).toEqual({
|
|
76
|
+
clientId: 'client-id-2',
|
|
77
|
+
redirectUri: 'https://app.example.com/callback',
|
|
78
|
+
scopes: ['read', 'write'],
|
|
79
|
+
hostname: 'crm.example.com'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const state = await managedOAuth.getManagedOAuthState({
|
|
83
|
+
rcAccountId: 'acc-1',
|
|
84
|
+
platform: 'testCRM',
|
|
85
|
+
isAdmin: true
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(state).toEqual({
|
|
89
|
+
isAdmin: true,
|
|
90
|
+
hasAccountOAuth: false,
|
|
91
|
+
hasPendingOAuth: true,
|
|
92
|
+
pendingValues
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('expired pending OAuth cache is destroyed and ignored', async () => {
|
|
97
|
+
const record = await managedOAuth.upsertPendingManagedOAuth({
|
|
98
|
+
rcAccountId: 'expired-acc',
|
|
99
|
+
values: {
|
|
100
|
+
clientId: 'expired-client'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
await record.update({
|
|
104
|
+
expiry: new Date(Date.now() - 1000)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const values = await managedOAuth.getPendingManagedOAuthValues({ rcAccountId: 'expired-acc' });
|
|
108
|
+
|
|
109
|
+
expect(values).toEqual({});
|
|
110
|
+
expect(await CacheModel.findByPk('expired-acc-managed-oauth-account')).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('migrates pending OAuth to account record and hides account client secret in state', async () => {
|
|
114
|
+
await managedOAuth.upsertPendingManagedOAuth({
|
|
115
|
+
rcAccountId: 'acc-2',
|
|
116
|
+
values: {
|
|
117
|
+
clientId: 'client-id',
|
|
118
|
+
clientSecret: 'client-secret',
|
|
119
|
+
accessTokenUri: 'https://token.example.com'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await expect(managedOAuth.migratePendingManagedOAuth({
|
|
124
|
+
rcAccountId: 'acc-2',
|
|
125
|
+
platform: 'testCRM'
|
|
126
|
+
})).resolves.toBe(true);
|
|
127
|
+
|
|
128
|
+
expect(await managedOAuth.getPendingManagedOAuthValues({ rcAccountId: 'acc-2' })).toEqual({});
|
|
129
|
+
|
|
130
|
+
const accountValues = await managedOAuth.getAccountManagedOAuthValues({
|
|
131
|
+
rcAccountId: 'acc-2',
|
|
132
|
+
platform: 'testCRM'
|
|
133
|
+
});
|
|
134
|
+
expect(accountValues).toEqual({
|
|
135
|
+
clientId: 'client-id',
|
|
136
|
+
clientSecret: 'client-secret',
|
|
137
|
+
accessTokenUri: 'https://token.example.com'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const state = await managedOAuth.getManagedOAuthState({
|
|
141
|
+
rcAccountId: 'acc-2',
|
|
142
|
+
platform: 'testCRM',
|
|
143
|
+
isAdmin: true
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(state).toEqual({
|
|
147
|
+
isAdmin: true,
|
|
148
|
+
hasAccountOAuth: true,
|
|
149
|
+
hasPendingOAuth: false,
|
|
150
|
+
oauthValues: {
|
|
151
|
+
clientId: 'client-id',
|
|
152
|
+
accessTokenUri: 'https://token.example.com'
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('migrates pending OAuth into an existing account record and resolves account source first', async () => {
|
|
158
|
+
await managedOAuth.upsertPendingManagedOAuth({
|
|
159
|
+
rcAccountId: 'acc-3',
|
|
160
|
+
values: {
|
|
161
|
+
clientId: 'pending-client'
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
await AccountDataModel.create({
|
|
165
|
+
rcAccountId: 'acc-3',
|
|
166
|
+
platformName: 'testCRM',
|
|
167
|
+
dataKey: managedOAuth.MANAGED_OAUTH_ACCOUNT_DATA_KEY,
|
|
168
|
+
data: {
|
|
169
|
+
fields: {
|
|
170
|
+
clientId: {
|
|
171
|
+
encrypted: false,
|
|
172
|
+
value: 'old-client'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await expect(managedOAuth.migratePendingManagedOAuth({
|
|
179
|
+
rcAccountId: 'acc-3',
|
|
180
|
+
platform: 'testCRM'
|
|
181
|
+
})).resolves.toBe(true);
|
|
182
|
+
|
|
183
|
+
await expect(managedOAuth.resolveManagedOAuthInfo({
|
|
184
|
+
rcAccountId: 'acc-3',
|
|
185
|
+
platform: 'testCRM'
|
|
186
|
+
})).resolves.toEqual({
|
|
187
|
+
source: 'account',
|
|
188
|
+
oauthInfo: {
|
|
189
|
+
clientId: 'pending-client'
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('resolveManagedOAuthInfo falls back to pending values or null source', async () => {
|
|
195
|
+
await managedOAuth.upsertPendingManagedOAuth({
|
|
196
|
+
rcAccountId: 'acc-4',
|
|
197
|
+
values: {
|
|
198
|
+
clientId: 'pending-client'
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await expect(managedOAuth.resolveManagedOAuthInfo({
|
|
203
|
+
rcAccountId: 'acc-4',
|
|
204
|
+
platform: 'testCRM'
|
|
205
|
+
})).resolves.toEqual({
|
|
206
|
+
source: 'pending',
|
|
207
|
+
oauthInfo: {
|
|
208
|
+
clientId: 'pending-client'
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await expect(managedOAuth.resolveManagedOAuthInfo({
|
|
213
|
+
rcAccountId: 'missing-acc',
|
|
214
|
+
platform: 'testCRM'
|
|
215
|
+
})).resolves.toEqual({
|
|
216
|
+
source: null,
|
|
217
|
+
oauthInfo: null
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('clear and reset delete account and pending OAuth data', async () => {
|
|
222
|
+
await managedOAuth.upsertPendingManagedOAuth({
|
|
223
|
+
rcAccountId: 'acc-5',
|
|
224
|
+
values: {
|
|
225
|
+
clientId: 'pending-client'
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
await managedOAuth.migratePendingManagedOAuth({
|
|
229
|
+
rcAccountId: 'acc-5',
|
|
230
|
+
platform: 'testCRM'
|
|
231
|
+
});
|
|
232
|
+
await managedOAuth.upsertPendingManagedOAuth({
|
|
233
|
+
rcAccountId: 'acc-5',
|
|
234
|
+
values: {
|
|
235
|
+
clientId: 'pending-client-2'
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const resetResult = await managedOAuth.resetManagedOAuth({
|
|
240
|
+
rcAccountId: 'acc-5',
|
|
241
|
+
platform: 'testCRM'
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(resetResult).toEqual({
|
|
245
|
+
deletedAccountCount: 1,
|
|
246
|
+
deletedPendingCount: 1
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('throws when upserting pending OAuth without account id and no-ops missing migrations', async () => {
|
|
251
|
+
await expect(managedOAuth.upsertPendingManagedOAuth({
|
|
252
|
+
values: {
|
|
253
|
+
clientId: 'client-id'
|
|
254
|
+
}
|
|
255
|
+
})).rejects.toThrow('rcAccountId is required');
|
|
256
|
+
|
|
257
|
+
await expect(managedOAuth.migratePendingManagedOAuth({
|
|
258
|
+
rcAccountId: 'no-pending',
|
|
259
|
+
platform: 'testCRM'
|
|
260
|
+
})).resolves.toBe(false);
|
|
261
|
+
});
|
|
262
|
+
});
|