@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.
Files changed (48) hide show
  1. package/connector/mock.js +9 -4
  2. package/connector/proxy/engine.js +3 -2
  3. package/connector/proxy/index.js +2 -2
  4. package/docs/architecture.md +1 -0
  5. package/docs/handlers.md +1 -1
  6. package/docs/models.md +2 -0
  7. package/docs/routes.md +1 -1
  8. package/handlers/disposition.js +3 -2
  9. package/handlers/log.js +16 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +10 -4
  12. package/lib/callLogLookup.js +82 -9
  13. package/lib/migrateCallLogsSchema.js +128 -7
  14. package/models/callLogModel.js +6 -0
  15. package/package.json +1 -1
  16. package/releaseNotes.json +16 -0
  17. package/test/connector/developerPortal.test.js +166 -0
  18. package/test/connector/mock.test.js +131 -0
  19. package/test/connector/proxy/engine.test.js +85 -0
  20. package/test/connector/proxy/index.test.js +246 -0
  21. package/test/connector/proxy/sample.json +1 -0
  22. package/test/handlers/admin.test.js +344 -0
  23. package/test/handlers/appointment.test.js +260 -0
  24. package/test/handlers/calldown.test.js +310 -0
  25. package/test/handlers/disposition.test.js +396 -0
  26. package/test/handlers/log.test.js +324 -1
  27. package/test/handlers/managedOAuth.test.js +262 -0
  28. package/test/handlers/plugin.test.js +305 -0
  29. package/test/handlers/user.test.js +381 -0
  30. package/test/index.test.js +102 -0
  31. package/test/lib/analytics.test.js +146 -0
  32. package/test/lib/authSession.test.js +173 -0
  33. package/test/lib/encode.test.js +59 -0
  34. package/test/lib/errorHandler.test.js +246 -0
  35. package/test/lib/generalErrorMessage.test.js +82 -0
  36. package/test/lib/s3ErrorLogReport.test.js +187 -0
  37. package/test/mcp/mcpHandlerMore.test.js +384 -0
  38. package/test/mcp/tools/appointmentTools.test.js +362 -0
  39. package/test/models/callDownListModel.test.js +125 -0
  40. package/test/models/dynamo/lockSchema.test.js +37 -0
  41. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  42. package/test/models/llmSessionModel.test.js +91 -0
  43. package/test/models/models.test.js +92 -0
  44. package/test/routes/calldownRoutes.test.js +224 -0
  45. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  46. package/test/routes/dispositionRoutes.test.js +192 -0
  47. package/test/routes/managedAuthRoutes.test.js +151 -0
  48. package/test/routes/pluginRoutes.test.js +262 -0
@@ -1,5 +1,9 @@
1
1
  const Sequelize = require('sequelize');
2
2
 
3
+ const CALL_LOGS_TABLE = 'callLogs';
4
+ const HASHED_EXTENSION_ID_COLUMN = 'hashedExtensionId';
5
+ const CALL_LOG_IDENTITY_PK_COLUMNS = ['id', 'sessionId', 'extensionNumber', HASHED_EXTENSION_ID_COLUMN];
6
+
3
7
  function findColumnKey(tableDescription, name) {
4
8
  const lower = name.toLowerCase();
5
9
  return Object.keys(tableDescription).find((k) => k.toLowerCase() === lower);
@@ -13,35 +17,51 @@ async function fetchCallLogsCreateSqlSqlite(sequelize, options = {}) {
13
17
  return rows[0]?.sql ?? null;
14
18
  }
15
19
 
16
- function callLogsCreateSqlClaimsExtensionPk(createSql) {
20
+ function callLogsCreateSqlClaimsPkColumns(createSql, columns) {
17
21
  if (!createSql || typeof createSql !== 'string') {
18
22
  return false;
19
23
  }
20
24
  const pkRegex = /PRIMARY\s+KEY\s*\(([^)]+)\)/gi;
21
25
  let m;
22
26
  while ((m = pkRegex.exec(createSql)) !== null) {
23
- if (/\bextensionNumber\b/i.test(m[1])) {
27
+ const pkColumns = m[1]
28
+ .split(',')
29
+ .map((column) => column.replace(/["'`\[\]\s]/g, '').toLowerCase());
30
+ if (columns.every((column) => pkColumns.includes(column.toLowerCase()))) {
24
31
  return true;
25
32
  }
26
33
  }
27
34
  return false;
28
35
  }
29
36
 
37
+ function callLogsCreateSqlClaimsExtensionPk(createSql) {
38
+ return callLogsCreateSqlClaimsPkColumns(createSql, ['extensionNumber']);
39
+ }
40
+
30
41
  async function sqliteCallLogsPkIncludesExtension(sequelize, options = {}) {
31
42
  const sql = await fetchCallLogsCreateSqlSqlite(sequelize, options);
32
43
  return callLogsCreateSqlClaimsExtensionPk(sql);
33
44
  }
34
45
 
46
+ async function sqliteCallLogsPkIncludesHashedExtension(sequelize, options = {}) {
47
+ const sql = await fetchCallLogsCreateSqlSqlite(sequelize, options);
48
+ return callLogsCreateSqlClaimsPkColumns(sql, CALL_LOG_IDENTITY_PK_COLUMNS);
49
+ }
50
+
51
+ async function describeCallLogsTable(sequelize, options = {}) {
52
+ return sequelize.getQueryInterface().describeTable(CALL_LOGS_TABLE, options);
53
+ }
54
+
35
55
  /**
36
56
  * SQLite cannot change composite PK via Sequelize addConstraint (it appends a second PRIMARY KEY).
37
- * Rebuild the table with the correct 3-column primary key and copy rows.
57
+ * Rebuild the table with the current call-log identity primary key and copy rows.
38
58
  */
39
- async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
59
+ async function migrateCallLogsIdentitySqlite(sequelize, transaction) {
40
60
  const qg = sequelize.getQueryInterface().queryGenerator;
41
61
  const qi = sequelize.getQueryInterface();
42
62
  const q = (id) => qg.quoteIdentifier(id);
43
63
  const tmpName = `callLogs_mig_legacy_${Date.now()}`;
44
- const mainTableSql = qg.quoteTable({ tableName: 'callLogs' });
64
+ const mainTableSql = qg.quoteTable({ tableName: CALL_LOGS_TABLE });
45
65
  const tmpTableSql = qg.quoteTable({ tableName: tmpName });
46
66
 
47
67
  await sequelize.query(
@@ -55,13 +75,14 @@ async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
55
75
  ${q('id')} VARCHAR(255) NOT NULL,
56
76
  ${q('sessionId')} VARCHAR(255) NOT NULL,
57
77
  ${q('extensionNumber')} VARCHAR(255) NOT NULL DEFAULT '',
78
+ ${q(HASHED_EXTENSION_ID_COLUMN)} VARCHAR(255) NOT NULL DEFAULT '',
58
79
  ${q('platform')} VARCHAR(255),
59
80
  ${q('thirdPartyLogId')} VARCHAR(255),
60
81
  ${q('userId')} VARCHAR(255),
61
82
  ${q('contactId')} VARCHAR(255),
62
83
  ${q('createdAt')} DATETIME NOT NULL,
63
84
  ${q('updatedAt')} DATETIME NOT NULL,
64
- PRIMARY KEY (${q('id')}, ${q('sessionId')}, ${q('extensionNumber')})
85
+ PRIMARY KEY (${CALL_LOG_IDENTITY_PK_COLUMNS.map(q).join(', ')})
65
86
  );
66
87
  `,
67
88
  { transaction },
@@ -73,6 +94,7 @@ async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
73
94
  'id',
74
95
  'sessionId',
75
96
  'extensionNumber',
97
+ HASHED_EXTENSION_ID_COLUMN,
76
98
  'platform',
77
99
  'thirdPartyLogId',
78
100
  'userId',
@@ -82,7 +104,7 @@ async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
82
104
  ];
83
105
 
84
106
  const missingRequired = targetCols.filter(
85
- (c) => findColumnKey(oldDesc, c) == null && c !== 'extensionNumber',
107
+ (c) => findColumnKey(oldDesc, c) == null && c !== 'extensionNumber' && c !== HASHED_EXTENSION_ID_COLUMN,
86
108
  );
87
109
  if (missingRequired.length) {
88
110
  throw new Error(
@@ -109,8 +131,107 @@ async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
109
131
  await sequelize.query(`DROP TABLE ${tmpTableSql};`, { transaction });
110
132
  }
111
133
 
134
+ async function migrateCallLogsExtensionNumberSqlite(sequelize, transaction) {
135
+ return migrateCallLogsIdentitySqlite(sequelize, transaction);
136
+ }
137
+
138
+ async function getPostgresPrimaryKeyConstraint(sequelize, options = {}) {
139
+ const constraints = await sequelize.query(
140
+ `
141
+ SELECT tc.constraint_name AS "constraintName"
142
+ FROM information_schema.table_constraints tc
143
+ WHERE tc.table_name = :tableName
144
+ AND tc.constraint_type = 'PRIMARY KEY'
145
+ LIMIT 1
146
+ `,
147
+ {
148
+ type: Sequelize.QueryTypes.SELECT,
149
+ replacements: { tableName: CALL_LOGS_TABLE },
150
+ ...options,
151
+ },
152
+ );
153
+ return constraints[0]?.constraintName ?? null;
154
+ }
155
+
156
+ async function postgresCallLogsPkIncludesHashedExtension(sequelize, options = {}) {
157
+ const pkConstraintName = await getPostgresPrimaryKeyConstraint(sequelize, options);
158
+ if (!pkConstraintName) {
159
+ return false;
160
+ }
161
+ const rows = await sequelize.query(
162
+ `
163
+ SELECT kcu.column_name AS "columnName"
164
+ FROM information_schema.key_column_usage kcu
165
+ WHERE kcu.table_name = :tableName
166
+ AND kcu.constraint_name = :constraintName
167
+ ORDER BY kcu.ordinal_position
168
+ `,
169
+ {
170
+ type: Sequelize.QueryTypes.SELECT,
171
+ replacements: { tableName: CALL_LOGS_TABLE, constraintName: pkConstraintName },
172
+ ...options,
173
+ },
174
+ );
175
+ const pkColumns = rows.map((row) => row.columnName.toLowerCase());
176
+ return CALL_LOG_IDENTITY_PK_COLUMNS.every((column) => pkColumns.includes(column.toLowerCase()));
177
+ }
178
+
179
+ async function migrateCallLogsIdentityPostgres(sequelize, transaction) {
180
+ const qi = sequelize.getQueryInterface();
181
+ const qg = qi.queryGenerator;
182
+ const table = qg.quoteTable({ tableName: CALL_LOGS_TABLE });
183
+ const pkConstraintName = await getPostgresPrimaryKeyConstraint(sequelize, { transaction });
184
+ if (pkConstraintName) {
185
+ await sequelize.query(
186
+ `ALTER TABLE ${table} DROP CONSTRAINT ${qg.quoteIdentifier(pkConstraintName)};`,
187
+ { transaction },
188
+ );
189
+ }
190
+ await sequelize.query(
191
+ `ALTER TABLE ${table} ADD PRIMARY KEY (${CALL_LOG_IDENTITY_PK_COLUMNS.map((column) => qg.quoteIdentifier(column)).join(', ')});`,
192
+ { transaction },
193
+ );
194
+ }
195
+
196
+ async function ensureCallLogsHashedExtensionIdSchema(sequelize) {
197
+ const qi = sequelize.getQueryInterface();
198
+ const dialect = sequelize.getDialect();
199
+ let desc = await describeCallLogsTable(sequelize);
200
+
201
+ if (findColumnKey(desc, HASHED_EXTENSION_ID_COLUMN) == null) {
202
+ await qi.addColumn(CALL_LOGS_TABLE, HASHED_EXTENSION_ID_COLUMN, {
203
+ type: Sequelize.STRING,
204
+ allowNull: false,
205
+ defaultValue: '',
206
+ });
207
+ desc = await describeCallLogsTable(sequelize);
208
+ }
209
+
210
+ if (dialect === 'sqlite') {
211
+ const hasIdentityPk = await sqliteCallLogsPkIncludesHashedExtension(sequelize);
212
+ if (!hasIdentityPk) {
213
+ await sequelize.transaction(async (transaction) => {
214
+ await migrateCallLogsIdentitySqlite(sequelize, transaction);
215
+ });
216
+ }
217
+ return;
218
+ }
219
+
220
+ if (dialect === 'postgres') {
221
+ const hasIdentityPk = await postgresCallLogsPkIncludesHashedExtension(sequelize);
222
+ if (!hasIdentityPk) {
223
+ await sequelize.transaction(async (transaction) => {
224
+ await migrateCallLogsIdentityPostgres(sequelize, transaction);
225
+ });
226
+ }
227
+ }
228
+ }
229
+
112
230
  module.exports = {
113
231
  findColumnKey,
114
232
  migrateCallLogsExtensionNumberSqlite,
233
+ migrateCallLogsIdentitySqlite,
115
234
  sqliteCallLogsPkIncludesExtension,
235
+ sqliteCallLogsPkIncludesHashedExtension,
236
+ ensureCallLogsHashedExtensionIdSchema,
116
237
  };
@@ -18,6 +18,12 @@ exports.CallLogModel = sequelize.define('callLogs', {
18
18
  allowNull: false,
19
19
  defaultValue: '',
20
20
  },
21
+ hashedExtensionId: {
22
+ type: Sequelize.STRING,
23
+ primaryKey: true,
24
+ allowNull: false,
25
+ defaultValue: '',
26
+ },
21
27
  platform: {
22
28
  type: Sequelize.STRING,
23
29
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.35",
3
+ "version": "1.7.36",
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.36": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Custom fields for call pop parameters"
7
+ },
8
+ {
9
+ "type": "New",
10
+ "description": "New option for auto log multi match resolver - Earliest created"
11
+ },
12
+ {
13
+ "type": "Fix",
14
+ "description": "Server-side logging call log id inconsistency"
15
+ }
16
+ ]
17
+ },
2
18
  "1.7.35": {
3
19
  "global": [
4
20
  {
@@ -0,0 +1,166 @@
1
+ jest.mock('axios', () => ({
2
+ get: jest.fn(),
3
+ }));
4
+
5
+ jest.mock('../../lib/logger', () => ({
6
+ logger: {
7
+ error: jest.fn(),
8
+ },
9
+ }));
10
+
11
+ const axios = require('axios');
12
+ const { logger } = require('../../lib/logger');
13
+ const {
14
+ getPublicConnectorList,
15
+ getConnectorManifest,
16
+ } = require('../../connector/developerPortal');
17
+
18
+ describe('developerPortal connector', () => {
19
+ beforeEach(() => {
20
+ axios.get.mockReset();
21
+ logger.error.mockReset();
22
+ });
23
+
24
+ test('loads the public connector list', async () => {
25
+ axios.get.mockResolvedValueOnce({
26
+ data: {
27
+ connectors: [
28
+ { id: 'public-a' },
29
+ { id: 'public-b' },
30
+ ],
31
+ },
32
+ });
33
+
34
+ await expect(getPublicConnectorList()).resolves.toEqual({
35
+ connectors: [
36
+ { id: 'public-a' },
37
+ { id: 'public-b' },
38
+ ],
39
+ });
40
+ expect(axios.get).toHaveBeenCalledWith('https://appconnect.labs.ringcentral.com/public-api/connectors');
41
+ expect(logger.error).not.toHaveBeenCalled();
42
+ });
43
+
44
+ test('returns null when public connector list loading fails', async () => {
45
+ const error = new Error('network down');
46
+ axios.get.mockRejectedValueOnce(error);
47
+
48
+ await expect(getPublicConnectorList()).resolves.toBeNull();
49
+
50
+ expect(logger.error).toHaveBeenCalledWith('Error getting public connector list:', error);
51
+ });
52
+
53
+ test('loads a public connector manifest by id', async () => {
54
+ axios.get.mockResolvedValueOnce({
55
+ data: {
56
+ id: 'salesforce',
57
+ name: 'Salesforce',
58
+ },
59
+ });
60
+
61
+ await expect(getConnectorManifest({
62
+ connectorId: 'salesforce',
63
+ })).resolves.toEqual({
64
+ id: 'salesforce',
65
+ name: 'Salesforce',
66
+ });
67
+ expect(axios.get).toHaveBeenCalledWith('https://appconnect.labs.ringcentral.com/public-api/connectors/salesforce/manifest');
68
+ });
69
+
70
+ test('loads a private connector manifest for the requesting account', async () => {
71
+ axios.get
72
+ .mockResolvedValueOnce({
73
+ data: {
74
+ privateConnectors: [
75
+ { id: 'private-crm' },
76
+ ],
77
+ sharedConnectors: [],
78
+ },
79
+ })
80
+ .mockResolvedValueOnce({
81
+ data: {
82
+ id: 'private-crm',
83
+ access: 'internal',
84
+ },
85
+ });
86
+
87
+ await expect(getConnectorManifest({
88
+ rcAccountId: 'rc-account-1',
89
+ connectorId: 'private-crm',
90
+ isPrivate: true,
91
+ })).resolves.toEqual({
92
+ id: 'private-crm',
93
+ access: 'internal',
94
+ });
95
+
96
+ expect(axios.get).toHaveBeenNthCalledWith(
97
+ 1,
98
+ 'https://appconnect.labs.ringcentral.com/public-api/connectors/internal?accountId=rc-account-1',
99
+ );
100
+ expect(axios.get).toHaveBeenNthCalledWith(
101
+ 2,
102
+ 'https://appconnect.labs.ringcentral.com/public-api/connectors/private-crm/manifest?access=internal&type=connector&accountId=rc-account-1',
103
+ );
104
+ });
105
+
106
+ test('loads a shared private connector manifest from the owner account', async () => {
107
+ axios.get
108
+ .mockResolvedValueOnce({
109
+ data: {
110
+ privateConnectors: [],
111
+ sharedConnectors: [
112
+ { id: 'shared-crm', accountId: 'owner-account' },
113
+ ],
114
+ },
115
+ })
116
+ .mockResolvedValueOnce({
117
+ data: {
118
+ id: 'shared-crm',
119
+ ownerAccountId: 'owner-account',
120
+ },
121
+ });
122
+
123
+ await expect(getConnectorManifest({
124
+ rcAccountId: 'viewer-account',
125
+ connectorId: 'shared-crm',
126
+ isPrivate: true,
127
+ })).resolves.toEqual({
128
+ id: 'shared-crm',
129
+ ownerAccountId: 'owner-account',
130
+ });
131
+
132
+ expect(axios.get).toHaveBeenNthCalledWith(
133
+ 2,
134
+ 'https://appconnect.labs.ringcentral.com/public-api/connectors/shared-crm/manifest?access=internal&type=connector&accountId=owner-account',
135
+ );
136
+ });
137
+
138
+ test('returns null when a private connector is not visible to the account', async () => {
139
+ axios.get.mockResolvedValueOnce({
140
+ data: {
141
+ privateConnectors: [],
142
+ sharedConnectors: [],
143
+ },
144
+ });
145
+
146
+ await expect(getConnectorManifest({
147
+ rcAccountId: 'rc-account-2',
148
+ connectorId: 'missing-crm',
149
+ isPrivate: true,
150
+ })).resolves.toBeNull();
151
+
152
+ expect(logger.error.mock.calls[0][0]).toBe('Error getting connector manifest:');
153
+ expect(logger.error.mock.calls[0][1].message).toBe('Connector not found');
154
+ });
155
+
156
+ test('returns null when public manifest loading fails', async () => {
157
+ const error = new Error('manifest unavailable');
158
+ axios.get.mockRejectedValueOnce(error);
159
+
160
+ await expect(getConnectorManifest({
161
+ connectorId: 'broken-crm',
162
+ })).resolves.toBeNull();
163
+
164
+ expect(logger.error).toHaveBeenCalledWith('Error getting connector manifest:', error);
165
+ });
166
+ });
@@ -0,0 +1,131 @@
1
+ jest.mock('shortid', () => ({
2
+ generate: jest.fn(() => 'mock-log-id'),
3
+ }));
4
+
5
+ const mockConnector = require('../../connector/mock');
6
+ const { UserModel } = require('../../models/userModel');
7
+ const { CallLogModel } = require('../../models/callLogModel');
8
+
9
+ describe('mock connector', () => {
10
+ beforeEach(async () => {
11
+ await CallLogModel.destroy({ where: {} });
12
+ await UserModel.destroy({ where: {} });
13
+ });
14
+
15
+ test('creates and deletes the mock user idempotently', async () => {
16
+ const firstUser = await mockConnector.createUser();
17
+ const secondUser = await mockConnector.createUser();
18
+
19
+ expect(firstUser.id).toBe('mockUser');
20
+ expect(secondUser.id).toBe('mockUser');
21
+ await expect(UserModel.count({ where: { id: 'mockUser' } })).resolves.toBe(1);
22
+
23
+ await expect(mockConnector.deleteUser()).resolves.toBe(true);
24
+ await expect(mockConnector.deleteUser()).resolves.toBe(false);
25
+ await expect(UserModel.findByPk('mockUser')).resolves.toBeNull();
26
+ });
27
+
28
+ test('creates a mock call log only once for the same session identity', async () => {
29
+ await mockConnector.createCallLog({
30
+ sessionId: 'session-1',
31
+ extensionNumber: 101,
32
+ hashedExtensionId: 'hash-101',
33
+ });
34
+ await mockConnector.createCallLog({
35
+ sessionId: 'session-1',
36
+ extensionNumber: '101',
37
+ hashedExtensionId: 'hash-101',
38
+ });
39
+
40
+ const records = await CallLogModel.findAll({
41
+ where: {
42
+ sessionId: 'session-1',
43
+ },
44
+ });
45
+ expect(records).toHaveLength(1);
46
+ expect(records[0]).toMatchObject({
47
+ id: 'mock-log-id',
48
+ sessionId: 'session-1',
49
+ extensionNumber: '101',
50
+ hashedExtensionId: 'hash-101',
51
+ userId: 'mockUser',
52
+ });
53
+ });
54
+
55
+ test('returns matched and unmatched call logs in requested session order', async () => {
56
+ await mockConnector.createCallLog({
57
+ sessionId: 'session-1',
58
+ extensionNumber: '101',
59
+ hashedExtensionId: 'hash-101',
60
+ });
61
+ await mockConnector.createCallLog({
62
+ sessionId: 'session-3',
63
+ extensionNumber: '101',
64
+ hashedExtensionId: 'hash-101',
65
+ });
66
+
67
+ await expect(mockConnector.getCallLog({
68
+ sessionIds: 'session-1,session-2,session-3',
69
+ extensionNumber: '101',
70
+ hashedExtensionId: 'hash-101',
71
+ })).resolves.toEqual([
72
+ { sessionId: 'session-1', matched: true, logId: 'mockThirdPartyLogId' },
73
+ { sessionId: 'session-2', matched: false },
74
+ { sessionId: 'session-3', matched: true, logId: 'mockThirdPartyLogId' },
75
+ ]);
76
+ });
77
+
78
+ test('falls back to legacy extension matching when hashed extension id does not match', async () => {
79
+ await CallLogModel.create({
80
+ id: 'legacy-log-id',
81
+ sessionId: 'session-legacy',
82
+ extensionNumber: '101',
83
+ hashedExtensionId: '',
84
+ userId: 'mockUser',
85
+ });
86
+
87
+ await expect(mockConnector.getCallLog({
88
+ sessionIds: 'session-legacy',
89
+ extensionNumber: '101',
90
+ hashedExtensionId: 'new-hash-101',
91
+ })).resolves.toEqual([
92
+ { sessionId: 'session-legacy', matched: true, logId: 'mockThirdPartyLogId' },
93
+ ]);
94
+ });
95
+
96
+ test('cleanUpMockLogs removes only mock user call logs', async () => {
97
+ await CallLogModel.bulkCreate([
98
+ {
99
+ id: 'mock-log',
100
+ sessionId: 'mock-session',
101
+ extensionNumber: '',
102
+ hashedExtensionId: '',
103
+ userId: 'mockUser',
104
+ },
105
+ {
106
+ id: 'real-log',
107
+ sessionId: 'real-session',
108
+ extensionNumber: '',
109
+ hashedExtensionId: '',
110
+ userId: 'realUser',
111
+ },
112
+ ]);
113
+
114
+ await mockConnector.cleanUpMockLogs();
115
+
116
+ await expect(CallLogModel.findOne({
117
+ where: {
118
+ userId: 'mockUser',
119
+ },
120
+ })).resolves.toBeNull();
121
+ const realLog = await CallLogModel.findOne({
122
+ where: {
123
+ userId: 'realUser',
124
+ },
125
+ });
126
+ expect(realLog).toMatchObject({
127
+ id: 'real-log',
128
+ sessionId: 'real-session',
129
+ });
130
+ });
131
+ });
@@ -8,6 +8,7 @@ const {
8
8
  renderDeep,
9
9
  joinUrl,
10
10
  performRequest,
11
+ mapFindContactResponse,
11
12
  } = require('../../../connector/proxy/engine');
12
13
 
13
14
  describe('proxy engine utilities', () => {
@@ -121,6 +122,90 @@ describe('proxy engine utilities', () => {
121
122
  const args = axios.mock.calls[0][0];
122
123
  expect(args.headers.Authorization).toBe(`Basic ${Buffer.from('login-key').toString('base64')}`);
123
124
  });
125
+
126
+ test('mapFindContactResponse maps contact created date', () => {
127
+ const config = {
128
+ operations: {
129
+ findContact: {
130
+ responseMapping: {
131
+ listPath: 'body.contacts',
132
+ item: {
133
+ idPath: 'id',
134
+ namePath: 'name',
135
+ createdDatePath: 'created_at',
136
+ mostRecentActivityDatePath: 'updated_at'
137
+ }
138
+ }
139
+ }
140
+ }
141
+ };
142
+ const response = {
143
+ data: {
144
+ contacts: [
145
+ {
146
+ id: 'c1',
147
+ name: 'Alice',
148
+ created_at: '2024-01-01T00:00:00Z',
149
+ updated_at: '2024-02-01T00:00:00Z'
150
+ }
151
+ ]
152
+ }
153
+ };
154
+
155
+ expect(mapFindContactResponse({ config, response })).toEqual([
156
+ expect.objectContaining({
157
+ id: 'c1',
158
+ name: 'Alice',
159
+ createdDate: '2024-01-01T00:00:00Z',
160
+ mostRecentActivityDate: '2024-02-01T00:00:00Z'
161
+ })
162
+ ]);
163
+ });
164
+
165
+ test('mapFindContactResponse can map findContactWithName responses', () => {
166
+ const config = {
167
+ operations: {
168
+ findContact: {
169
+ responseMapping: {
170
+ listPath: 'body.phoneMatches',
171
+ item: {
172
+ idPath: 'id',
173
+ namePath: 'name'
174
+ }
175
+ }
176
+ },
177
+ findContactWithName: {
178
+ responseMapping: {
179
+ listPath: 'body.nameMatches',
180
+ item: {
181
+ idPath: 'personId',
182
+ namePath: 'displayName',
183
+ createdDatePath: 'created'
184
+ }
185
+ }
186
+ }
187
+ }
188
+ };
189
+ const response = {
190
+ data: {
191
+ nameMatches: [
192
+ {
193
+ personId: 'p1',
194
+ displayName: 'Jane Smith',
195
+ created: '2023-05-01T00:00:00Z'
196
+ }
197
+ ]
198
+ }
199
+ };
200
+
201
+ expect(mapFindContactResponse({ config, response, opName: 'findContactWithName' })).toEqual([
202
+ expect.objectContaining({
203
+ id: 'p1',
204
+ name: 'Jane Smith',
205
+ createdDate: '2023-05-01T00:00:00Z'
206
+ })
207
+ ]);
208
+ });
124
209
  });
125
210
 
126
211