@app-connect/core 1.7.34 → 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 +18 -6
- package/handlers/plugin.js +13 -6
- package/index.js +22 -11
- 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 +44 -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/auth.test.js +6 -2
- package/test/handlers/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +327 -3
- 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 +166 -1
- 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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
57
|
+
* Rebuild the table with the current call-log identity primary key and copy rows.
|
|
38
58
|
*/
|
|
39
|
-
async function
|
|
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:
|
|
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
|
|
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
|
};
|
package/models/callLogModel.js
CHANGED
|
@@ -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
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
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
|
+
},
|
|
18
|
+
"1.7.35": {
|
|
19
|
+
"global": [
|
|
20
|
+
{
|
|
21
|
+
"type": "New",
|
|
22
|
+
"description": "Can show RingCentral Account Id on support page"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "New",
|
|
26
|
+
"description": "Add hashedExtensionId in plugin calls"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "New",
|
|
30
|
+
"description": "Show notification when having incoming call while minized"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "Fix",
|
|
34
|
+
"description": "User session refresh and validtion issue"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "Fix",
|
|
38
|
+
"description": "Warm-transfer call pop"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "Fix",
|
|
42
|
+
"description": "Refresh manifest for override properties"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
"1.7.34": {
|
|
3
47
|
"global": [
|
|
4
48
|
{
|
|
@@ -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
|
|