@app-connect/core 1.7.16 → 1.7.17

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/admin.js CHANGED
@@ -109,7 +109,7 @@ async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy
109
109
  var callLogStats = [];
110
110
  var itemKeys = [];
111
111
  for (const record of callsAggregationData.data.records) {
112
- if(!record?.info?.name){
112
+ if (!record?.info?.name) {
113
113
  continue;
114
114
  }
115
115
  itemKeys.push(record.info.name);
@@ -238,6 +238,17 @@ async function getUserMapping({ user, hashedRcAccountId, rcExtensionList }) {
238
238
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
239
239
  // eslint-disable-next-line no-param-reassign
240
240
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
241
+ if (!user) {
242
+ return {
243
+ successful: false,
244
+ returnMessage: {
245
+ message: `User session expired. Please connect again.`,
246
+ messageType: 'warning',
247
+ ttl: 5000
248
+ },
249
+ isRevokeUserSession: true
250
+ }
251
+ }
241
252
  authHeader = `Bearer ${user.accessToken}`;
242
253
  break;
243
254
  case 'apiKey':
@@ -399,6 +410,17 @@ async function reinitializeUserMapping({ user, hashedRcAccountId, rcExtensionLis
399
410
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
400
411
  // eslint-disable-next-line no-param-reassign
401
412
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
413
+ if (!user) {
414
+ return {
415
+ successful: false,
416
+ returnMessage: {
417
+ message: `User session expired. Please connect again.`,
418
+ messageType: 'warning',
419
+ ttl: 5000
420
+ },
421
+ isRevokeUserSession: true
422
+ }
423
+ }
402
424
  authHeader = `Bearer ${user.accessToken}`;
403
425
  break;
404
426
  case 'apiKey':
@@ -56,6 +56,17 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
56
56
  case 'oauth':
57
57
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
58
58
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
59
+ if (!user) {
60
+ return {
61
+ successful: false,
62
+ returnMessage: {
63
+ message: `User session expired. Please connect again.`,
64
+ messageType: 'warning',
65
+ ttl: 5000
66
+ },
67
+ isRevokeUserSession: true
68
+ }
69
+ }
59
70
  authHeader = `Bearer ${user.accessToken}`;
60
71
  tracer?.trace('handler.findContact:oauthAuth', { authHeader });
61
72
  break;
@@ -153,6 +164,17 @@ async function createContact({ platform, userId, phoneNumber, newContactName, ne
153
164
  case 'oauth':
154
165
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
155
166
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
167
+ if (!user) {
168
+ return {
169
+ successful: false,
170
+ returnMessage: {
171
+ message: `User session expired. Please connect again.`,
172
+ messageType: 'warning',
173
+ ttl: 5000
174
+ },
175
+ isRevokeUserSession: true
176
+ }
177
+ }
156
178
  authHeader = `Bearer ${user.accessToken}`;
157
179
  break;
158
180
  case 'apiKey':
@@ -202,6 +224,17 @@ async function findContactWithName({ platform, userId, name }) {
202
224
  case 'oauth':
203
225
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
204
226
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
227
+ if (!user) {
228
+ return {
229
+ successful: false,
230
+ returnMessage: {
231
+ message: `User session expired. Please connect again.`,
232
+ messageType: 'warning',
233
+ ttl: 5000
234
+ },
235
+ isRevokeUserSession: true
236
+ }
237
+ }
205
238
  authHeader = `Bearer ${user.accessToken}`;
206
239
  break;
207
240
  case 'apiKey':
@@ -45,6 +45,17 @@ async function upsertCallDisposition({ platform, userId, sessionId, dispositions
45
45
  case 'oauth':
46
46
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
47
47
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
48
+ if (!user) {
49
+ return {
50
+ successful: false,
51
+ returnMessage: {
52
+ message: `User session expired. Please connect again.`,
53
+ messageType: 'warning',
54
+ ttl: 5000
55
+ },
56
+ isRevokeUserSession: true
57
+ }
58
+ }
48
59
  authHeader = `Bearer ${user.accessToken}`;
49
60
  break;
50
61
  case 'apiKey':
package/handlers/log.js CHANGED
@@ -13,6 +13,7 @@ const moment = require('moment');
13
13
  const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
14
14
  const logger = require('../lib/logger');
15
15
  const { handleApiError, handleDatabaseError } = require('../lib/errorHandler');
16
+ const { AccountDataModel } = require('../models/accountDataModel');
16
17
 
17
18
  async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
18
19
  try {
@@ -77,6 +78,17 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
77
78
  case 'oauth':
78
79
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
79
80
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
81
+ if (!user) {
82
+ return {
83
+ successful: false,
84
+ returnMessage: {
85
+ message: `User session expired. Please connect again.`,
86
+ messageType: 'warning',
87
+ ttl: 5000
88
+ },
89
+ isRevokeUserSession: true
90
+ }
91
+ }
80
92
  authHeader = `Bearer ${user.accessToken}`;
81
93
  break;
82
94
  case 'apiKey':
@@ -206,6 +218,17 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
206
218
  case 'oauth':
207
219
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
208
220
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
221
+ if (!user) {
222
+ return {
223
+ successful: false,
224
+ returnMessage: {
225
+ message: `User session expired. Please connect again.`,
226
+ messageType: 'warning',
227
+ ttl: 5000
228
+ },
229
+ isRevokeUserSession: true
230
+ }
231
+ }
209
232
  authHeader = `Bearer ${user.accessToken}`;
210
233
  break;
211
234
  case 'apiKey':
@@ -292,6 +315,17 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
292
315
  case 'oauth':
293
316
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
294
317
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
318
+ if (!user) {
319
+ return {
320
+ successful: false,
321
+ returnMessage: {
322
+ message: `User session expired. Please connect again.`,
323
+ messageType: 'warning',
324
+ ttl: 5000
325
+ },
326
+ isRevokeUserSession: true
327
+ }
328
+ }
295
329
  authHeader = `Bearer ${user.accessToken}`;
296
330
  break;
297
331
  case 'apiKey':
@@ -382,7 +416,7 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
382
416
  isFromSSCL,
383
417
  proxyConfig,
384
418
  });
385
- if(!extraDataTracking){
419
+ if (!extraDataTracking) {
386
420
  extraDataTracking = {};
387
421
  }
388
422
  extraDataTracking.withSmartNoteLog = !!incomingData.aiNote;
@@ -442,6 +476,17 @@ async function createMessageLog({ platform, userId, incomingData }) {
442
476
  case 'oauth':
443
477
  const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
444
478
  user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
479
+ if (!user) {
480
+ return {
481
+ successful: false,
482
+ returnMessage: {
483
+ message: `User session expired. Please connect again.`,
484
+ messageType: 'warning',
485
+ ttl: 5000
486
+ },
487
+ isRevokeUserSession: true
488
+ }
489
+ }
445
490
  authHeader = `Bearer ${user.accessToken}`;
446
491
  break;
447
492
  case 'apiKey':
@@ -467,12 +512,33 @@ async function createMessageLog({ platform, userId, incomingData }) {
467
512
  type: incomingData.contactType ?? "",
468
513
  name: incomingData.contactName ?? ""
469
514
  };
515
+ const isGroupSMS = incomingData.logInfo.correspondents.length > 1;
470
516
  // For shared SMS
471
517
  const assigneeName = incomingData.logInfo.assignee?.name;
472
518
  const ownerName = incomingData.logInfo.owner?.name;
473
519
  const isSharedSMS = !!ownerName;
474
520
 
475
- const messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() }; });
521
+ let messageIds = [];
522
+ const correspondents = [];
523
+ if (isGroupSMS) {
524
+ messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() + `-${incomingData.contactId}` }; });
525
+ for (var i = 0; i < incomingData.logInfo.correspondents.length; i++) {
526
+ // find cached contact by composite key; findByPk expects raw PK values, so use where clause
527
+ const correspondentContactInfo = await AccountDataModel.findOne({
528
+ where: {
529
+ rcAccountId: user.rcAccountId,
530
+ platformName: platform,
531
+ dataKey: `contact-${incomingData.logInfo.correspondents[i].phoneNumber}`
532
+ }
533
+ })
534
+ if (correspondentContactInfo && correspondentContactInfo.data[0]?.name != incomingData.contactName) {
535
+ correspondents.push(correspondentContactInfo.data);
536
+ }
537
+ }
538
+ }
539
+ else {
540
+ messageIds = incomingData.logInfo.messages.map(m => { return { id: m.id.toString() }; });
541
+ }
476
542
  let existingMessages = null;
477
543
  try {
478
544
  existingMessages = await MessageLogModel.findAll({
@@ -520,9 +586,18 @@ async function createMessageLog({ platform, userId, incomingData }) {
520
586
  }
521
587
  // Case: normal SMS
522
588
  else {
589
+ if (isGroupSMS) {
590
+ // eslint-disable-next-line no-param-reassign
591
+ incomingData.logInfo.conversationLogId = incomingData.logInfo.conversationLogId + `-${incomingData.contactId}`;
592
+ // eslint-disable-next-line no-param-reassign
593
+ incomingData.logInfo.conversationId = incomingData.logInfo.conversationId + `-${incomingData.contactId}`;
594
+ }
523
595
  // reverse the order of messages to log the oldest message first
524
596
  const reversedMessages = incomingData.logInfo.messages.reverse();
525
597
  for (const message of reversedMessages) {
598
+ if (isGroupSMS) {
599
+ message.id = message.id.toString() + `-${incomingData.contactId}`;
600
+ }
526
601
  if (existingIds.includes(message.id.toString())) {
527
602
  continue;
528
603
  }
@@ -532,14 +607,14 @@ async function createMessageLog({ platform, userId, incomingData }) {
532
607
  }
533
608
  let faxDocLink = null;
534
609
  let faxDownloadLink = null;
535
- if (message.attachments && message.attachments.some(a => a.type === 'RenderedDocument')) {
610
+ if (message.attachments && message.attachments.some(a => a.type === 'RenderedDocument') && incomingData.logInfo.rcAccessToken) {
536
611
  faxDocLink = message.attachments.find(a => a.type === 'RenderedDocument').link;
537
612
  faxDownloadLink = message.attachments.find(a => a.type === 'RenderedDocument').uri + `?access_token=${incomingData.logInfo.rcAccessToken}`
538
613
  }
539
614
  let imageLink = null;
540
615
  let imageDownloadLink = null;
541
616
  let imageContentType = null;
542
- if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'))) {
617
+ if (message.attachments && message.attachments.some(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/')) && incomingData.logInfo.rcAccessToken) {
543
618
  const imageAttachment = message.attachments.find(a => a.type === 'MmsAttachment' && a.contentType.startsWith('image/'));
544
619
  if (imageAttachment) {
545
620
  imageLink = getMediaReaderLinkByPlatformMediaLink(imageAttachment?.uri);
@@ -563,15 +638,15 @@ async function createMessageLog({ platform, userId, incomingData }) {
563
638
  conversationLogId: incomingData.logInfo.conversationLogId
564
639
  }
565
640
  });
566
- let crmLogId = ''
641
+ let crmLogId = ''
567
642
  if (existingSameDateMessageLog) {
568
- const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, assigneeName, ownerName, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission, imageLink, videoLink, proxyConfig });
643
+ const updateMessageResult = await platformModule.updateMessageLog({ user, contactInfo, assigneeName, ownerName, existingMessageLog: existingSameDateMessageLog, message, authHeader, additionalSubmission, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
569
644
  crmLogId = existingSameDateMessageLog.thirdPartyLogId;
570
645
  returnMessage = updateMessageResult?.returnMessage;
571
646
  extraDataTracking = updateMessageResult.extraDataTracking;
572
647
  }
573
648
  else {
574
- const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, assigneeName, ownerName, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
649
+ const createMessageLogResult = await platformModule.createMessageLog({ user, contactInfo, correspondents, assigneeName, ownerName, authHeader, message, additionalSubmission, recordingLink, faxDocLink, faxDownloadLink, imageLink, imageDownloadLink, imageContentType, videoLink, proxyConfig });
575
650
  crmLogId = createMessageLogResult.logId;
576
651
  returnMessage = createMessageLogResult?.returnMessage;
577
652
  extraDataTracking = createMessageLogResult.extraDataTracking;
package/index.js CHANGED
@@ -333,7 +333,7 @@ function createCoreRouter() {
333
333
  }
334
334
  else {
335
335
  tracer?.trace('setAdminSettings:adminValidationFailed', {});
336
- res.status(401).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
336
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
337
337
  success = false;
338
338
  }
339
339
  }
@@ -395,7 +395,7 @@ function createCoreRouter() {
395
395
  }
396
396
  else {
397
397
  tracer?.trace('getAdminSettings:adminValidationFailed', {});
398
- res.status(401).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
398
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
399
399
  success = true;
400
400
  }
401
401
  }
@@ -447,12 +447,18 @@ function createCoreRouter() {
447
447
  const hashedRcAccountId = util.getHashValue(rcAccountId, process.env.HASH_KEY);
448
448
  if (isValidated) {
449
449
  const userMapping = await adminCore.getUserMapping({ user, hashedRcAccountId, rcExtensionList: req.body.rcExtensionList });
450
- res.status(200).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
451
- success = true;
450
+ if (userMapping?.isRevokeUserSession) {
451
+ res.status(401).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
452
+ success = false;
453
+ }
454
+ else {
455
+ res.status(200).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
456
+ success = true;
457
+ }
452
458
  }
453
459
  else {
454
460
  tracer?.trace('getUserMapping:adminValidationFailed', {});
455
- res.status(401).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
461
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
456
462
  success = true;
457
463
  }
458
464
  }
@@ -504,12 +510,18 @@ function createCoreRouter() {
504
510
  const hashedRcAccountId = util.getHashValue(rcAccountId, process.env.HASH_KEY);
505
511
  if (isValidated) {
506
512
  const userMapping = await adminCore.reinitializeUserMapping({ user, hashedRcAccountId, rcExtensionList: req.body.rcExtensionList });
507
- res.status(200).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
508
- success = true;
513
+ if (userMapping?.isRevokeUserSession) {
514
+ res.status(401).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
515
+ success = false;
516
+ }
517
+ else {
518
+ res.status(200).send(tracer ? tracer.wrapResponse(userMapping) : userMapping);
519
+ success = true;
520
+ }
509
521
  }
510
522
  else {
511
523
  tracer?.trace('reinitializeUserMapping:adminValidationFailed', {});
512
- res.status(401).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
524
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
513
525
  success = true;
514
526
  }
515
527
  }
@@ -825,8 +837,7 @@ function createCoreRouter() {
825
837
  const stateParams = new URLSearchParams(state ? decodeURIComponent(state) : '');
826
838
  platformName = stateParams.get('platform');
827
839
  // backward compatibility
828
- if(!platformName)
829
- {
840
+ if (!platformName) {
830
841
  platformName = req.query.callbackUri?.split('platform=')[1] ?? state.split('platform=')[1];
831
842
  }
832
843
  // Extract mcp auth sessionId if present
@@ -870,7 +881,7 @@ function createCoreRouter() {
870
881
  res.status(200).send("Authentication successful. Please go back to AI Agent and confirm it.");
871
882
  success = true;
872
883
  }
873
- else {
884
+ else {
874
885
  res.status(200).send(tracer ? tracer.wrapResponse({ jwtToken, name: userInfo.name, returnMessage }) : { jwtToken, name: userInfo.name, returnMessage });
875
886
  success = true;
876
887
  }
@@ -1057,7 +1068,7 @@ function createCoreRouter() {
1057
1068
  }
1058
1069
  const { id: userId, platform } = decodedToken;
1059
1070
  platformName = platform;
1060
- const { successful, returnMessage, contact, extraDataTracking } = await contactCore.findContact({
1071
+ const { successful, returnMessage, contact, extraDataTracking, isRevokeUserSession } = await contactCore.findContact({
1061
1072
  platform,
1062
1073
  userId,
1063
1074
  phoneNumber: req.query.phoneNumber.replace(' ', '+'),
@@ -1066,15 +1077,21 @@ function createCoreRouter() {
1066
1077
  tracer,
1067
1078
  isForceRefreshAccountData: req.query?.isForceRefreshAccountData === 'true'
1068
1079
  });
1069
- tracer?.trace('findContact:result', { successful, returnMessage, contact });
1070
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1071
- if (successful) {
1072
- const nonNewContact = contact?.filter(c => !c.isNewContact) ?? [];
1073
- resultCount = nonNewContact.length;
1080
+ if (isRevokeUserSession) {
1081
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1082
+ success = false;
1074
1083
  }
1075
- success = successful;
1076
- if (extraDataTracking) {
1077
- extraData = extraDataTracking;
1084
+ else {
1085
+ tracer?.trace('findContact:result', { successful, returnMessage, contact });
1086
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1087
+ if (successful) {
1088
+ const nonNewContact = contact?.filter(c => !c.isNewContact) ?? [];
1089
+ resultCount = nonNewContact.length;
1090
+ }
1091
+ success = successful;
1092
+ if (extraDataTracking) {
1093
+ extraData = extraDataTracking;
1094
+ }
1078
1095
  }
1079
1096
  }
1080
1097
  else {
@@ -1128,11 +1145,17 @@ function createCoreRouter() {
1128
1145
  }
1129
1146
  const { id: userId, platform } = decodedToken;
1130
1147
  platformName = platform;
1131
- const { successful, returnMessage, contact, extraDataTracking } = await contactCore.createContact({ platform, userId, phoneNumber: req.body.phoneNumber, newContactName: req.body.newContactName, newContactType: req.body.newContactType, additionalSubmission: req.body.additionalSubmission });
1132
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1133
- success = true;
1134
- if (extraDataTracking) {
1135
- extraData = extraDataTracking;
1148
+ const { successful, returnMessage, contact, extraDataTracking, isRevokeUserSession } = await contactCore.createContact({ platform, userId, phoneNumber: req.body.phoneNumber, newContactName: req.body.newContactName, newContactType: req.body.newContactType, additionalSubmission: req.body.additionalSubmission });
1149
+ if (isRevokeUserSession) {
1150
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1151
+ success = false;
1152
+ }
1153
+ else {
1154
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1155
+ success = true;
1156
+ if (extraDataTracking) {
1157
+ extraData = extraDataTracking;
1158
+ }
1136
1159
  }
1137
1160
  }
1138
1161
  else {
@@ -1236,13 +1259,19 @@ function createCoreRouter() {
1236
1259
  }
1237
1260
  const { id: userId, platform } = decodedToken;
1238
1261
  platformName = platform;
1239
- const { successful, logs, returnMessage, extraDataTracking } = await logCore.getCallLog({ userId, sessionIds: req.query.sessionIds, platform, requireDetails: req.query.requireDetails === 'true' });
1240
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logs, returnMessage }) : { successful, logs, returnMessage });
1241
- success = true;
1242
- if (extraDataTracking) {
1243
- extraData = extraDataTracking;
1262
+ const { successful, logs, returnMessage, extraDataTracking, isRevokeUserSession } = await logCore.getCallLog({ userId, sessionIds: req.query.sessionIds, platform, requireDetails: req.query.requireDetails === 'true' });
1263
+ if (isRevokeUserSession) {
1264
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1265
+ success = false;
1266
+ }
1267
+ else {
1268
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logs, returnMessage }) : { successful, logs, returnMessage });
1269
+ success = true;
1270
+ if (extraDataTracking) {
1271
+ extraData = extraDataTracking;
1272
+ }
1273
+ extraData.requireDetails = req.query.requireDetails === 'true';
1244
1274
  }
1245
- extraData.requireDetails = req.query.requireDetails === 'true';
1246
1275
  }
1247
1276
  else {
1248
1277
  tracer?.trace('getCallLog:noToken', {});
@@ -1294,12 +1323,18 @@ function createCoreRouter() {
1294
1323
  }
1295
1324
  const { id: userId, platform } = decodedToken;
1296
1325
  platformName = platform;
1297
- const { successful, logId, returnMessage, extraDataTracking } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1298
- if (extraDataTracking) {
1299
- extraData = extraDataTracking;
1326
+ const { successful, logId, returnMessage, extraDataTracking, isRevokeUserSession } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1327
+ if (isRevokeUserSession) {
1328
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1329
+ success = false;
1330
+ }
1331
+ else {
1332
+ if (extraDataTracking) {
1333
+ extraData = extraDataTracking;
1334
+ }
1335
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage }) : { successful, logId, returnMessage });
1336
+ success = true;
1300
1337
  }
1301
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage }) : { successful, logId, returnMessage });
1302
- success = true;
1303
1338
  }
1304
1339
  else {
1305
1340
  tracer?.trace('createCallLog:noToken', {});
@@ -1407,18 +1442,24 @@ function createCoreRouter() {
1407
1442
  res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
1408
1443
  return;
1409
1444
  }
1410
- const { successful, returnMessage, extraDataTracking } = await dispositionCore.upsertCallDisposition({
1445
+ const { successful, returnMessage, extraDataTracking, isRevokeUserSession } = await dispositionCore.upsertCallDisposition({
1411
1446
  platform,
1412
1447
  userId,
1413
1448
  sessionId: req.body.sessionId,
1414
1449
  dispositions: req.body.dispositions,
1415
1450
  additionalSubmission: req.body.additionalSubmission
1416
1451
  });
1417
- if (extraDataTracking) {
1418
- extraData = extraDataTracking;
1452
+ if (isRevokeUserSession) {
1453
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1454
+ success = false;
1455
+ }
1456
+ else {
1457
+ if (extraDataTracking) {
1458
+ extraData = extraDataTracking;
1459
+ }
1460
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1461
+ success = true;
1419
1462
  }
1420
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1421
- success = true;
1422
1463
  }
1423
1464
  else {
1424
1465
  tracer?.trace('upsertCallDisposition:noToken', {});
@@ -1471,12 +1512,18 @@ function createCoreRouter() {
1471
1512
  }
1472
1513
  const { id: userId, platform } = decodedToken;
1473
1514
  platformName = platform;
1474
- const { successful, returnMessage, logIds, extraDataTracking } = await logCore.createMessageLog({ platform, userId, incomingData: req.body });
1475
- if (extraDataTracking) {
1476
- extraData = extraDataTracking;
1515
+ const { successful, returnMessage, logIds, extraDataTracking, isRevokeUserSession } = await logCore.createMessageLog({ platform, userId, incomingData: req.body });
1516
+ if (isRevokeUserSession) {
1517
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1518
+ success = false;
1519
+ }
1520
+ else {
1521
+ if (extraDataTracking) {
1522
+ extraData = extraDataTracking;
1523
+ }
1524
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, logIds }) : { successful, returnMessage, logIds });
1525
+ success = true;
1477
1526
  }
1478
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, logIds }) : { successful, returnMessage, logIds });
1479
- success = true;
1480
1527
  }
1481
1528
  else {
1482
1529
  tracer?.trace('createMessageLog:noToken', {});
@@ -1707,9 +1754,15 @@ function createCoreRouter() {
1707
1754
  if (jwtToken) {
1708
1755
  const { id: userId, platform } = jwt.decodeJwt(jwtToken);
1709
1756
  platformName = platform;
1710
- const { successful, returnMessage, contact } = await contactCore.findContactWithName({ platform, userId, name: req.query.name });
1711
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1712
- success = successful;
1757
+ const { successful, returnMessage, contact, isRevokeUserSession } = await contactCore.findContactWithName({ platform, userId, name: req.query.name });
1758
+ if (isRevokeUserSession) {
1759
+ res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1760
+ success = false;
1761
+ }
1762
+ else {
1763
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage, contact }) : { successful, returnMessage, contact });
1764
+ success = successful;
1765
+ }
1713
1766
  }
1714
1767
  else {
1715
1768
  tracer?.trace('contactSearchByName:noToken', {});
@@ -1896,7 +1949,7 @@ function createCoreRouter() {
1896
1949
  res.status(200).send(mockUser ? 'Mock user registered' : 'Mock user already existed');
1897
1950
  }
1898
1951
  else {
1899
- res.status(401).send('Unauthorized');
1952
+ res.status(403).send('Unauthorized');
1900
1953
  }
1901
1954
  });
1902
1955
  router.delete('/deleteMockUser', async function (req, res) {
@@ -1906,7 +1959,7 @@ function createCoreRouter() {
1906
1959
  res.status(200).send(foundAndDeleted ? 'Mock user deleted' : 'Mock user not found');
1907
1960
  }
1908
1961
  else {
1909
- res.status(401).send('Unauthorized');
1962
+ res.status(403).send('Unauthorized');
1910
1963
  }
1911
1964
  });
1912
1965
  router.get('/mockCallLog', async function (req, res) {
@@ -1916,7 +1969,7 @@ function createCoreRouter() {
1916
1969
  res.status(200).send(callLogs);
1917
1970
  }
1918
1971
  else {
1919
- res.status(401).send('Unauthorized');
1972
+ res.status(403).send('Unauthorized');
1920
1973
  }
1921
1974
  });
1922
1975
  router.post('/mockCallLog', async function (req, res) {
@@ -1926,7 +1979,7 @@ function createCoreRouter() {
1926
1979
  res.status(200).send('Mock call log created');
1927
1980
  }
1928
1981
  else {
1929
- res.status(401).send('Unauthorized');
1982
+ res.status(403).send('Unauthorized');
1930
1983
  }
1931
1984
  });
1932
1985
  router.delete('/mockCallLog', async function (req, res) {
@@ -1936,7 +1989,7 @@ function createCoreRouter() {
1936
1989
  res.status(200).send('Mock call logs cleaned up');
1937
1990
  }
1938
1991
  else {
1939
- res.status(401).send('Unauthorized');
1992
+ res.status(403).send('Unauthorized');
1940
1993
  }
1941
1994
  });
1942
1995
  }
@@ -1944,7 +1997,7 @@ function createCoreRouter() {
1944
1997
  router.get('/.well-known/openai-apps-challenge', (req, res) => {
1945
1998
  res.send(process.env.CHATGPT_VERIFICATION_CODE);
1946
1999
  });
1947
-
2000
+
1948
2001
  // --- METADATA ENDPOINT 1: Resource Metadata ---
1949
2002
  // Tells the client "I am protected" and "Here is who protects me"
1950
2003
  router.get('/.well-known/oauth-protected-resource', (req, res) => {
package/lib/oauth.js CHANGED
@@ -101,41 +101,48 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
101
101
  try {
102
102
  const startRefreshTime = moment();
103
103
  const token = oauthApp.createToken(user.accessToken, user.refreshToken);
104
- logger.info('token refreshing...')
104
+ logger.info('token refreshing...')
105
105
  const { accessToken, refreshToken, expires } = await token.refresh();
106
106
  user.accessToken = accessToken;
107
107
  user.refreshToken = refreshToken;
108
108
  user.tokenExpiry = expires;
109
- try {
110
- await user.save();
111
- }
112
- catch (error) {
113
- return handleDatabaseError(error, 'Error saving user');
114
- }
109
+ try {
110
+ await user.save();
111
+ }
112
+ catch (error) {
113
+ return handleDatabaseError(error, 'Error saving user');
114
+ }
115
115
  if (newLock) {
116
116
  const deletionStartTime = moment();
117
117
  await newLock.delete();
118
118
  const deletionEndTime = moment();
119
- logger.info(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
119
+ logger.info(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
120
120
  }
121
121
  const endRefreshTime = moment();
122
- logger.info(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
122
+ logger.info(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
123
123
  }
124
124
  catch (e) {
125
125
  console.log('token refreshing failed', e.stack)
126
126
  if (newLock) {
127
127
  await newLock.delete();
128
128
  }
129
+ return null;
129
130
  }
130
131
  }
131
132
  // case: run withou token refresh lock
132
133
  else {
133
- logger.info('token refreshing...')
134
- const token = oauthApp.createToken(user.accessToken, user.refreshToken);
135
- const { accessToken, refreshToken, expires } = await token.refresh();
136
- user.accessToken = accessToken;
137
- user.refreshToken = refreshToken;
138
- user.tokenExpiry = expires;
134
+ try {
135
+ logger.info('token refreshing...')
136
+ const token = oauthApp.createToken(user.accessToken, user.refreshToken);
137
+ const { accessToken, refreshToken, expires } = await token.refresh();
138
+ user.accessToken = accessToken;
139
+ user.refreshToken = refreshToken;
140
+ user.tokenExpiry = expires;
141
+ }
142
+ catch (e) {
143
+ console.log('token refreshing failed', e.stack)
144
+ return null;
145
+ }
139
146
  try {
140
147
  await user.save();
141
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.16",
3
+ "version": "1.7.17",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,20 @@
1
1
  {
2
+ "1.7.17": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Support for Group SMS logging"
7
+ },
8
+ {
9
+ "type": "Better",
10
+ "description": "User session will log be revoked is token refresh fails"
11
+ },
12
+ {
13
+ "type": "Fix",
14
+ "description": "Contact call pop issue"
15
+ }
16
+ ]
17
+ },
2
18
  "1.7.16": {
3
19
  "global": [
4
20
  {
@@ -29,6 +29,7 @@ const logHandler = require('../../handlers/log');
29
29
  const { CallLogModel } = require('../../models/callLogModel');
30
30
  const { MessageLogModel } = require('../../models/messageLogModel');
31
31
  const { UserModel } = require('../../models/userModel');
32
+ const { AccountDataModel } = require('../../models/accountDataModel');
32
33
  const connectorRegistry = require('../../connector/registry');
33
34
  const oauth = require('../../lib/oauth');
34
35
  const { composeCallLog } = require('../../lib/callLogComposer');
@@ -40,12 +41,14 @@ describe('Log Handler', () => {
40
41
  await CallLogModel.sync({ force: true });
41
42
  await MessageLogModel.sync({ force: true });
42
43
  await UserModel.sync({ force: true });
44
+ await AccountDataModel.sync({ force: true });
43
45
  });
44
46
 
45
47
  afterEach(async () => {
46
48
  await CallLogModel.destroy({ where: {} });
47
49
  await MessageLogModel.destroy({ where: {} });
48
50
  await UserModel.destroy({ where: {} });
51
+ await AccountDataModel.destroy({ where: {} });
49
52
  jest.clearAllMocks();
50
53
  });
51
54
 
@@ -742,6 +745,228 @@ describe('Log Handler', () => {
742
745
  expect(mockConnector.createMessageLog).toHaveBeenCalledTimes(0);
743
746
  expect(mockConnector.updateMessageLog).toHaveBeenCalledTimes(1);
744
747
  });
748
+
749
+ test('should handle group SMS with contactId suffix for message IDs', async () => {
750
+ // Arrange - group SMS has multiple correspondents
751
+ await UserModel.create({
752
+ id: 'test-user-id',
753
+ platform: 'testCRM',
754
+ accessToken: 'test-token',
755
+ rcAccountId: 'rc-account-123',
756
+ platformAdditionalInfo: {}
757
+ });
758
+
759
+ const mockConnector = {
760
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
761
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
762
+ createMessageLog: jest.fn().mockResolvedValue({
763
+ logId: 'msg-log-group-123',
764
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
765
+ }),
766
+ updateMessageLog: jest.fn()
767
+ };
768
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
769
+
770
+ const incomingData = {
771
+ logInfo: {
772
+ messages: [{ id: 'msg-group-1', subject: 'Group SMS', direction: 'Outbound', creationTime: new Date() }],
773
+ correspondents: [
774
+ { phoneNumber: '+1234567890' },
775
+ { phoneNumber: '+0987654321' }
776
+ ],
777
+ conversationId: 'conv-group-123',
778
+ conversationLogId: 'conv-log-group-123'
779
+ },
780
+ contactId: 'contact-456',
781
+ contactType: 'Contact',
782
+ contactName: 'Primary Contact',
783
+ additionalSubmission: {}
784
+ };
785
+
786
+ // Act
787
+ const result = await logHandler.createMessageLog({
788
+ platform: 'testCRM',
789
+ userId: 'test-user-id',
790
+ incomingData
791
+ });
792
+
793
+ // Assert
794
+ expect(result.successful).toBe(true);
795
+ expect(result.logIds).toContain('msg-group-1-contact-456');
796
+ const savedLog = await MessageLogModel.findOne({ where: { id: 'msg-group-1-contact-456' } });
797
+ expect(savedLog).not.toBeNull();
798
+ expect(savedLog.thirdPartyLogId).toBe('msg-log-group-123');
799
+ });
800
+
801
+ test('should pass correspondents to createMessageLog when group SMS has different contact names', async () => {
802
+ // Arrange - correspondent in cache with different name
803
+ await UserModel.create({
804
+ id: 'test-user-id',
805
+ platform: 'testCRM',
806
+ accessToken: 'test-token',
807
+ rcAccountId: 'rc-account-123',
808
+ platformAdditionalInfo: {}
809
+ });
810
+
811
+ await AccountDataModel.create({
812
+ rcAccountId: 'rc-account-123',
813
+ platformName: 'testCRM',
814
+ dataKey: 'contact-+0987654321',
815
+ data: [{ name: 'Other Contact', id: 'contact-789' }]
816
+ });
817
+
818
+ const mockConnector = {
819
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
820
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
821
+ createMessageLog: jest.fn().mockResolvedValue({
822
+ logId: 'msg-log-correspondents',
823
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
824
+ }),
825
+ updateMessageLog: jest.fn()
826
+ };
827
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
828
+
829
+ const incomingData = {
830
+ logInfo: {
831
+ messages: [{ id: 'msg-correspondents', subject: 'Group SMS', direction: 'Outbound', creationTime: new Date() }],
832
+ correspondents: [
833
+ { phoneNumber: '+1234567890' },
834
+ { phoneNumber: '+0987654321' }
835
+ ],
836
+ conversationId: 'conv-correspondents',
837
+ conversationLogId: 'conv-log-correspondents'
838
+ },
839
+ contactId: 'contact-456',
840
+ contactType: 'Contact',
841
+ contactName: 'Primary Contact',
842
+ additionalSubmission: {}
843
+ };
844
+
845
+ // Act
846
+ await logHandler.createMessageLog({
847
+ platform: 'testCRM',
848
+ userId: 'test-user-id',
849
+ incomingData
850
+ });
851
+
852
+ // Assert - createMessageLog should receive correspondents with different name
853
+ expect(mockConnector.createMessageLog).toHaveBeenCalledWith(
854
+ expect.objectContaining({
855
+ correspondents: [[{ name: 'Other Contact', id: 'contact-789' }]]
856
+ })
857
+ );
858
+ });
859
+
860
+ test('should not add correspondent when name matches contactName in group SMS', async () => {
861
+ // Arrange - correspondent in cache with same name as contactName
862
+ await UserModel.create({
863
+ id: 'test-user-id',
864
+ platform: 'testCRM',
865
+ accessToken: 'test-token',
866
+ rcAccountId: 'rc-account-123',
867
+ platformAdditionalInfo: {}
868
+ });
869
+
870
+ await AccountDataModel.create({
871
+ rcAccountId: 'rc-account-123',
872
+ platformName: 'testCRM',
873
+ dataKey: 'contact-+0987654321',
874
+ data: [{ name: 'Primary Contact', id: 'contact-789' }]
875
+ });
876
+
877
+ const mockConnector = {
878
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
879
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
880
+ createMessageLog: jest.fn().mockResolvedValue({
881
+ logId: 'msg-log-same-name',
882
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
883
+ }),
884
+ updateMessageLog: jest.fn()
885
+ };
886
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
887
+
888
+ const incomingData = {
889
+ logInfo: {
890
+ messages: [{ id: 'msg-same-name', subject: 'Group SMS', direction: 'Outbound', creationTime: new Date() }],
891
+ correspondents: [
892
+ { phoneNumber: '+1234567890' },
893
+ { phoneNumber: '+0987654321' }
894
+ ],
895
+ conversationId: 'conv-same-name',
896
+ conversationLogId: 'conv-log-same-name'
897
+ },
898
+ contactId: 'contact-456',
899
+ contactType: 'Contact',
900
+ contactName: 'Primary Contact',
901
+ additionalSubmission: {}
902
+ };
903
+
904
+ // Act
905
+ await logHandler.createMessageLog({
906
+ platform: 'testCRM',
907
+ userId: 'test-user-id',
908
+ incomingData
909
+ });
910
+
911
+ // Assert - correspondents should be empty when names match
912
+ expect(mockConnector.createMessageLog).toHaveBeenCalledWith(
913
+ expect.objectContaining({
914
+ correspondents: []
915
+ })
916
+ );
917
+ });
918
+
919
+ test('should use suffixed conversationLogId and conversationId for group SMS', async () => {
920
+ // Arrange
921
+ await UserModel.create({
922
+ id: 'test-user-id',
923
+ platform: 'testCRM',
924
+ accessToken: 'test-token',
925
+ rcAccountId: 'rc-account-123',
926
+ platformAdditionalInfo: {}
927
+ });
928
+
929
+ const mockConnector = {
930
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
931
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
932
+ createMessageLog: jest.fn().mockResolvedValue({
933
+ logId: 'msg-log-suffix',
934
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
935
+ }),
936
+ updateMessageLog: jest.fn()
937
+ };
938
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
939
+
940
+ const incomingData = {
941
+ logInfo: {
942
+ messages: [{ id: 'msg-suffix', subject: 'Group SMS', direction: 'Outbound', creationTime: new Date() }],
943
+ correspondents: [
944
+ { phoneNumber: '+1234567890' },
945
+ { phoneNumber: '+0987654321' }
946
+ ],
947
+ conversationId: 'conv-original',
948
+ conversationLogId: 'conv-log-original'
949
+ },
950
+ contactId: 'contact-999',
951
+ contactType: 'Contact',
952
+ contactName: 'Test Contact',
953
+ additionalSubmission: {}
954
+ };
955
+
956
+ // Act
957
+ const result = await logHandler.createMessageLog({
958
+ platform: 'testCRM',
959
+ userId: 'test-user-id',
960
+ incomingData
961
+ });
962
+
963
+ // Assert - message log saved with suffixed conversationLogId
964
+ expect(result.successful).toBe(true);
965
+ const savedLog = await MessageLogModel.findOne({ where: { id: 'msg-suffix-contact-999' } });
966
+ expect(savedLog).not.toBeNull();
967
+ expect(savedLog.conversationLogId).toBe('conv-log-original-contact-999');
968
+ expect(savedLog.conversationId).toBe('conv-original-contact-999');
969
+ });
745
970
  });
746
971
 
747
972
  describe('saveNoteCache', () => {
package/test/setup.js CHANGED
@@ -1,176 +1,176 @@
1
- // Test setup for @app-connect/core package
2
- const path = require('path');
3
- require('dotenv').config({ path: path.resolve(__dirname, '../.env.test') });
4
-
5
- // Set test timeout
6
- jest.setTimeout(30000);
7
-
8
- // Mock console methods to reduce noise in tests
9
- global.console = {
10
- ...console,
11
- log: jest.fn(),
12
- debug: jest.fn(),
13
- info: jest.fn(),
14
- warn: jest.fn(),
15
- error: jest.fn(),
16
- };
17
-
18
- // Setup database models for testing
19
- beforeAll(async () => {
20
- try {
21
- // Set up test database URL if not provided
22
- if (!process.env.DATABASE_URL) {
23
- process.env.DATABASE_URL = 'sqlite::memory:';
24
- }
25
-
26
- // Import models
27
- const { CallLogModel } = require('../models/callLogModel');
28
- const { MessageLogModel } = require('../models/messageLogModel');
29
- const { UserModel } = require('../models/userModel');
30
- const { CacheModel } = require('../models/cacheModel');
31
- const { AdminConfigModel } = require('../models/adminConfigModel');
32
-
33
- // Sync database models
34
- await CallLogModel.sync({ force: true });
35
- await MessageLogModel.sync({ force: true });
36
- await UserModel.sync({ force: true });
37
- await CacheModel.sync({ force: true });
38
- await AdminConfigModel.sync({ force: true });
39
-
40
- console.log('Database models synced for testing');
41
- } catch (error) {
42
- console.error('Error setting up test database:', error);
43
- // Don't fail the setup, some tests might not need database
44
- }
45
- });
46
-
47
- // Clean up after all tests
48
- afterAll(async () => {
49
- try {
50
- // Close database connections
51
- const { sequelize } = require('../models/sequelize');
52
- if (sequelize) {
53
- await sequelize.close();
54
- }
55
- } catch (error) {
56
- console.error('Error closing database connection:', error);
57
- }
58
- });
59
-
60
- // Global test utilities
61
- global.testUtils = {
62
- // Helper to create mock user
63
- createMockUser: (overrides = {}) => ({
64
- id: 'test-user-id',
65
- platform: 'testCRM',
66
- accessToken: 'test-access-token',
67
- refreshToken: 'test-refresh-token',
68
- tokenExpiry: new Date(Date.now() + 3600000), // 1 hour from now
69
- platformUserInfo: {
70
- id: 'test-platform-user-id',
71
- name: 'Test User',
72
- timezoneName: 'America/Los_Angeles',
73
- timezoneOffset: 0,
74
- platformAdditionalInfo: {}
75
- },
76
- ...overrides
77
- }),
78
-
79
- // Helper to create mock call log
80
- createMockCallLog: (overrides = {}) => ({
81
- id: 'test-call-log-id',
82
- userId: 'test-user-id',
83
- platform: 'testCRM',
84
- thirdPartyLogId: 'test-third-party-id',
85
- contactId: 'test-contact-id',
86
- contactType: 'Contact',
87
- phoneNumber: '+1234567890',
88
- callDirection: 'Inbound',
89
- callResult: 'Answered',
90
- callDuration: 120,
91
- callStartTime: new Date(),
92
- callEndTime: new Date(Date.now() + 120000),
93
- recordingLink: 'https://example.com/recording.mp3',
94
- subject: 'Test Call',
95
- note: 'Test call note',
96
- ...overrides
97
- }),
98
-
99
- // Helper to create mock contact
100
- createMockContact: (overrides = {}) => ({
101
- id: 'test-contact-id',
102
- name: 'Test Contact',
103
- type: 'Contact',
104
- phone: '+1234567890',
105
- additionalInfo: null,
106
- ...overrides
107
- }),
108
-
109
- // Helper to reset connector registry
110
- resetConnectorRegistry: () => {
111
- const connectorRegistry = require('../connector/registry');
112
- connectorRegistry.connectors.clear();
113
- connectorRegistry.manifests.clear();
114
- connectorRegistry.platformInterfaces.clear();
115
- connectorRegistry.releaseNotes = {};
116
- },
117
-
118
- // Helper to create mock connector
119
- createMockConnector: (overrides = {}) => ({
120
- getAuthType: jest.fn().mockReturnValue('apiKey'),
121
- getUserInfo: jest.fn().mockResolvedValue({
122
- successful: true,
123
- platformUserInfo: {
124
- id: 'test-user-id',
125
- name: 'Test User',
126
- timezoneName: 'America/Los_Angeles',
127
- timezoneOffset: 0,
128
- platformAdditionalInfo: {}
129
- }
130
- }),
131
- createCallLog: jest.fn().mockResolvedValue({
132
- logId: 'test-log-id',
133
- returnMessage: {
134
- message: 'Call logged successfully',
135
- messageType: 'success',
136
- ttl: 2000
137
- }
138
- }),
139
- updateCallLog: jest.fn().mockResolvedValue({
140
- updatedNote: 'Call log updated',
141
- returnMessage: {
142
- message: 'Call log updated successfully',
143
- messageType: 'success',
144
- ttl: 2000
145
- }
146
- }),
147
- unAuthorize: jest.fn().mockResolvedValue({
148
- returnMessage: {
149
- messageType: 'success',
150
- message: 'Logged out successfully',
151
- ttl: 1000
152
- }
153
- }),
154
- findContact: jest.fn().mockResolvedValue([
155
- {
156
- id: 'test-contact-id',
157
- name: 'Test Contact',
158
- type: 'Contact',
159
- phone: '+1234567890',
160
- additionalInfo: null
161
- }
162
- ]),
163
- createContact: jest.fn().mockResolvedValue({
164
- contactInfo: {
165
- id: 'new-contact-id',
166
- name: 'New Contact'
167
- },
168
- returnMessage: {
169
- message: 'Contact created successfully',
170
- messageType: 'success',
171
- ttl: 2000
172
- }
173
- }),
174
- ...overrides
175
- })
176
- };
1
+ // Test setup for @app-connect/core package
2
+ const path = require('path');
3
+ require('dotenv').config({ path: path.resolve(__dirname, '../.env.test') });
4
+
5
+ // Set test timeout
6
+ jest.setTimeout(30000);
7
+
8
+ // Mock console methods to reduce noise in tests
9
+ global.console = {
10
+ ...console,
11
+ log: jest.fn(),
12
+ debug: jest.fn(),
13
+ info: jest.fn(),
14
+ warn: jest.fn(),
15
+ error: jest.fn(),
16
+ };
17
+
18
+ // Setup database models for testing
19
+ beforeAll(async () => {
20
+ try {
21
+ // Set up test database URL if not provided
22
+ if (!process.env.DATABASE_URL) {
23
+ process.env.DATABASE_URL = 'sqlite::memory:';
24
+ }
25
+
26
+ // Import models
27
+ const { CallLogModel } = require('../models/callLogModel');
28
+ const { MessageLogModel } = require('../models/messageLogModel');
29
+ const { UserModel } = require('../models/userModel');
30
+ const { CacheModel } = require('../models/cacheModel');
31
+ const { AdminConfigModel } = require('../models/adminConfigModel');
32
+
33
+ // Sync database models
34
+ await CallLogModel.sync({ force: true });
35
+ await MessageLogModel.sync({ force: true });
36
+ await UserModel.sync({ force: true });
37
+ await CacheModel.sync({ force: true });
38
+ await AdminConfigModel.sync({ force: true });
39
+
40
+ console.log('Database models synced for testing');
41
+ } catch (error) {
42
+ console.error('Error setting up test database:', error);
43
+ // Don't fail the setup, some tests might not need database
44
+ }
45
+ });
46
+
47
+ // Clean up after all tests
48
+ afterAll(async () => {
49
+ try {
50
+ // Close database connections
51
+ const { sequelize } = require('../models/sequelize');
52
+ if (sequelize) {
53
+ await sequelize.close();
54
+ }
55
+ } catch (error) {
56
+ console.error('Error closing database connection:', error);
57
+ }
58
+ });
59
+
60
+ // Global test utilities
61
+ global.testUtils = {
62
+ // Helper to create mock user
63
+ createMockUser: (overrides = {}) => ({
64
+ id: 'test-user-id',
65
+ platform: 'testCRM',
66
+ accessToken: 'test-access-token',
67
+ refreshToken: 'test-refresh-token',
68
+ tokenExpiry: new Date(Date.now() + 3600000), // 1 hour from now
69
+ platformUserInfo: {
70
+ id: 'test-platform-user-id',
71
+ name: 'Test User',
72
+ timezoneName: 'America/Los_Angeles',
73
+ timezoneOffset: 0,
74
+ platformAdditionalInfo: {}
75
+ },
76
+ ...overrides
77
+ }),
78
+
79
+ // Helper to create mock call log
80
+ createMockCallLog: (overrides = {}) => ({
81
+ id: 'test-call-log-id',
82
+ userId: 'test-user-id',
83
+ platform: 'testCRM',
84
+ thirdPartyLogId: 'test-third-party-id',
85
+ contactId: 'test-contact-id',
86
+ contactType: 'Contact',
87
+ phoneNumber: '+1234567890',
88
+ callDirection: 'Inbound',
89
+ callResult: 'Answered',
90
+ callDuration: 120,
91
+ callStartTime: new Date(),
92
+ callEndTime: new Date(Date.now() + 120000),
93
+ recordingLink: 'https://example.com/recording.mp3',
94
+ subject: 'Test Call',
95
+ note: 'Test call note',
96
+ ...overrides
97
+ }),
98
+
99
+ // Helper to create mock contact
100
+ createMockContact: (overrides = {}) => ({
101
+ id: 'test-contact-id',
102
+ name: 'Test Contact',
103
+ type: 'Contact',
104
+ phone: '+1234567890',
105
+ additionalInfo: null,
106
+ ...overrides
107
+ }),
108
+
109
+ // Helper to reset connector registry
110
+ resetConnectorRegistry: () => {
111
+ const connectorRegistry = require('../connector/registry');
112
+ connectorRegistry.connectors.clear();
113
+ connectorRegistry.manifests.clear();
114
+ connectorRegistry.platformInterfaces.clear();
115
+ connectorRegistry.releaseNotes = {};
116
+ },
117
+
118
+ // Helper to create mock connector
119
+ createMockConnector: (overrides = {}) => ({
120
+ getAuthType: jest.fn().mockReturnValue('apiKey'),
121
+ getUserInfo: jest.fn().mockResolvedValue({
122
+ successful: true,
123
+ platformUserInfo: {
124
+ id: 'test-user-id',
125
+ name: 'Test User',
126
+ timezoneName: 'America/Los_Angeles',
127
+ timezoneOffset: 0,
128
+ platformAdditionalInfo: {}
129
+ }
130
+ }),
131
+ createCallLog: jest.fn().mockResolvedValue({
132
+ logId: 'test-log-id',
133
+ returnMessage: {
134
+ message: 'Call logged successfully',
135
+ messageType: 'success',
136
+ ttl: 2000
137
+ }
138
+ }),
139
+ updateCallLog: jest.fn().mockResolvedValue({
140
+ updatedNote: 'Call log updated',
141
+ returnMessage: {
142
+ message: 'Call log updated successfully',
143
+ messageType: 'success',
144
+ ttl: 2000
145
+ }
146
+ }),
147
+ unAuthorize: jest.fn().mockResolvedValue({
148
+ returnMessage: {
149
+ messageType: 'success',
150
+ message: 'Logged out successfully',
151
+ ttl: 1000
152
+ }
153
+ }),
154
+ findContact: jest.fn().mockResolvedValue([
155
+ {
156
+ id: 'test-contact-id',
157
+ name: 'Test Contact',
158
+ type: 'Contact',
159
+ phone: '+1234567890',
160
+ additionalInfo: null
161
+ }
162
+ ]),
163
+ createContact: jest.fn().mockResolvedValue({
164
+ contactInfo: {
165
+ id: 'new-contact-id',
166
+ name: 'New Contact'
167
+ },
168
+ returnMessage: {
169
+ message: 'Contact created successfully',
170
+ messageType: 'success',
171
+ ttl: 2000
172
+ }
173
+ }),
174
+ ...overrides
175
+ })
176
+ };