@app-connect/core 1.7.22 → 1.7.24
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/docs/libraries.md +1 -1
- package/handlers/admin.js +7 -4
- package/handlers/auth.js +13 -5
- package/handlers/log.js +217 -109
- package/handlers/plugin.js +183 -1
- package/handlers/user.js +1 -1
- package/index.js +165 -7
- package/lib/authSession.js +44 -12
- package/lib/callLogComposer.js +36 -36
- package/lib/util.js +0 -18
- package/mcp/mcpHandler.js +31 -8
- package/mcp/tools/checkAuthStatus.js +16 -14
- package/mcp/tools/getPublicConnectors.js +11 -1
- package/mcp/tools/getSessionInfo.js +90 -0
- package/mcp/tools/index.js +3 -1
- package/mcp/tools/logout.js +10 -1
- package/models/llmSessionModel.js +3 -0
- package/package.json +1 -1
- package/releaseNotes.json +20 -0
- package/test/handlers/admin.test.js +1 -2
- package/test/handlers/log.test.js +60 -0
- package/test/handlers/plugin.test.js +93 -0
- package/test/lib/callLogComposer.test.js +21 -21
- package/test/lib/util.test.js +1 -332
- package/test/mcp/tools/checkAuthStatus.test.js +82 -0
- package/test/mcp/tools/getSessionInfo.test.js +127 -0
- package/test/mcp/tools/logout.test.js +58 -0
- package/test/routes/managedAuthRoutes.test.js +0 -3
package/mcp/mcpHandler.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const axios = require('axios');
|
|
10
|
+
const { Op } = require('sequelize');
|
|
10
11
|
const tools = require('./tools');
|
|
11
12
|
const { LlmSessionModel } = require('../models/llmSessionModel');
|
|
12
13
|
const { CacheModel } = require('../models/cacheModel');
|
|
@@ -23,6 +24,8 @@ const path = require('path');
|
|
|
23
24
|
*/
|
|
24
25
|
const WIDGET_VERSION = 10;
|
|
25
26
|
const WIDGET_URI = `ui://widget/ConnectorList-v${WIDGET_VERSION}.html`;
|
|
27
|
+
const RC_EXTENSION_CACHE_KEY = 'rcExtensionId';
|
|
28
|
+
const RC_EXTENSION_CACHE_STATUS = 'resolved';
|
|
26
29
|
|
|
27
30
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
28
31
|
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
@@ -104,7 +107,7 @@ async function resolveSessionContext(rcAccessToken, openaiSessionId) {
|
|
|
104
107
|
|
|
105
108
|
if (openaiSessionId) {
|
|
106
109
|
try {
|
|
107
|
-
const cached = await CacheModel.findByPk(`${openaiSessionId}
|
|
110
|
+
const cached = await CacheModel.findByPk(`${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}`);
|
|
108
111
|
if (cached?.data?.rcExtensionId && (!cached.expiry || cached.expiry > new Date())) {
|
|
109
112
|
return { rcExtensionId: cached.data.rcExtensionId };
|
|
110
113
|
}
|
|
@@ -118,11 +121,11 @@ async function resolveSessionContext(rcAccessToken, openaiSessionId) {
|
|
|
118
121
|
rcExtensionId = await resolveRcExtensionId(rcAccessToken);
|
|
119
122
|
if (openaiSessionId && rcExtensionId) {
|
|
120
123
|
await CacheModel.upsert({
|
|
121
|
-
id: `${openaiSessionId}
|
|
124
|
+
id: `${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}`,
|
|
122
125
|
userId: openaiSessionId,
|
|
123
|
-
cacheKey:
|
|
126
|
+
cacheKey: RC_EXTENSION_CACHE_KEY,
|
|
124
127
|
data: { rcExtensionId },
|
|
125
|
-
status:
|
|
128
|
+
status: RC_EXTENSION_CACHE_STATUS,
|
|
126
129
|
expiry: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24h TTL
|
|
127
130
|
});
|
|
128
131
|
}
|
|
@@ -203,15 +206,34 @@ async function handleMcpRequest(req, res) {
|
|
|
203
206
|
toolArgs.rcExtensionId = rcExtensionId;
|
|
204
207
|
if (!toolArgs.jwtToken) {
|
|
205
208
|
let llmSession = await LlmSessionModel.findByPk(rcExtensionId);
|
|
209
|
+
if (llmSession?.expiry && llmSession.expiry < new Date()) {
|
|
210
|
+
await LlmSessionModel.destroy({ where: { id: rcExtensionId } });
|
|
211
|
+
llmSession = null;
|
|
212
|
+
}
|
|
206
213
|
if (!llmSession?.jwtToken && openaiSessionId) {
|
|
207
214
|
const fallback = await LlmSessionModel.findByPk(openaiSessionId);
|
|
208
215
|
if (fallback?.jwtToken) {
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
const { id: fallbackUserId } = jwt.decodeJwt(fallback.jwtToken);
|
|
217
|
+
const fallbackUser = fallbackUserId
|
|
218
|
+
? await UserModel.findByPk(fallbackUserId)
|
|
219
|
+
: null;
|
|
220
|
+
if (fallbackUser?.accessToken) {
|
|
221
|
+
await LlmSessionModel.upsert({ id: rcExtensionId, jwtToken: fallback.jwtToken, expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) });
|
|
222
|
+
llmSession = fallback;
|
|
223
|
+
}
|
|
211
224
|
}
|
|
212
|
-
|
|
225
|
+
if (!llmSession?.jwtToken) {
|
|
213
226
|
const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
|
|
214
|
-
const user = await UserModel.findOne({
|
|
227
|
+
const user = await UserModel.findOne({
|
|
228
|
+
where: {
|
|
229
|
+
hashedRcExtensionId,
|
|
230
|
+
[Op.and]: [
|
|
231
|
+
{ accessToken: { [Op.not]: null } },
|
|
232
|
+
{ accessToken: { [Op.ne]: '' } },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
order: [['updatedAt', 'DESC']],
|
|
236
|
+
});
|
|
215
237
|
if (user?.accessToken) {
|
|
216
238
|
await LlmSessionModel.upsert({
|
|
217
239
|
id: rcExtensionId,
|
|
@@ -219,6 +241,7 @@ async function handleMcpRequest(req, res) {
|
|
|
219
241
|
id: user.id.toString(),
|
|
220
242
|
platform: user.platform
|
|
221
243
|
}),
|
|
244
|
+
expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
222
245
|
});
|
|
223
246
|
llmSession = await LlmSessionModel.findByPk(rcExtensionId);
|
|
224
247
|
}
|
|
@@ -3,7 +3,7 @@ const { LlmSessionModel } = require('../../models/llmSessionModel');
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MCP Tool: Check Auth Status
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Polls the status of an ongoing OAuth authentication session
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -37,7 +37,7 @@ const toolDefinition = {
|
|
|
37
37
|
async function execute(args) {
|
|
38
38
|
try {
|
|
39
39
|
// rcExtensionId is injected by mcpHandler after verifying the RC access
|
|
40
|
-
// token.
|
|
40
|
+
// token. Using it as the DB key binds the CRM credential to a verified
|
|
41
41
|
// RC identity.
|
|
42
42
|
const { sessionId, rcExtensionId } = args;
|
|
43
43
|
if (!rcExtensionId) {
|
|
@@ -53,16 +53,12 @@ async function execute(args) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
switch (session.status) {
|
|
56
|
-
case 'completed':
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
} catch {
|
|
64
|
-
// Record already exists from a prior poll — safe to ignore
|
|
65
|
-
}
|
|
56
|
+
case 'completed':
|
|
57
|
+
await LlmSessionModel.upsert({
|
|
58
|
+
id: rcExtensionId,
|
|
59
|
+
jwtToken: session.jwtToken,
|
|
60
|
+
expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
61
|
+
});
|
|
66
62
|
return {
|
|
67
63
|
data: {
|
|
68
64
|
status: 'completed',
|
|
@@ -71,7 +67,14 @@ async function execute(args) {
|
|
|
71
67
|
message: 'IMPORTANT: Authentication successful! Keep jwtToken in memory for future use. DO NOT directly show it to user.'
|
|
72
68
|
}
|
|
73
69
|
};
|
|
74
|
-
|
|
70
|
+
|
|
71
|
+
case 'expired':
|
|
72
|
+
return {
|
|
73
|
+
data: {
|
|
74
|
+
status: 'expired',
|
|
75
|
+
errorMessage: 'Authentication session expired. Ask the user to start the auth flow again.'
|
|
76
|
+
}
|
|
77
|
+
};
|
|
75
78
|
|
|
76
79
|
case 'failed':
|
|
77
80
|
return {
|
|
@@ -100,4 +103,3 @@ async function execute(args) {
|
|
|
100
103
|
|
|
101
104
|
exports.definition = toolDefinition;
|
|
102
105
|
exports.execute = execute;
|
|
103
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const { Op } = require('sequelize');
|
|
2
3
|
const { UserModel } = require('../../models/userModel');
|
|
3
4
|
const { getHashValue } = require('../../lib/util');
|
|
4
5
|
|
|
@@ -56,7 +57,16 @@ async function execute({ rcAccessToken, openaiSessionId } = {}) {
|
|
|
56
57
|
|
|
57
58
|
// Check if user session already exists from Chrome extension
|
|
58
59
|
const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
|
|
59
|
-
const user = await UserModel.findOne({
|
|
60
|
+
const user = await UserModel.findOne({
|
|
61
|
+
where: {
|
|
62
|
+
hashedRcExtensionId,
|
|
63
|
+
[Op.and]: [
|
|
64
|
+
{ accessToken: { [Op.not]: null } },
|
|
65
|
+
{ accessToken: { [Op.ne]: '' } },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
order: [['updatedAt', 'DESC']],
|
|
69
|
+
});
|
|
60
70
|
// Case: user exists, return user info in plain message
|
|
61
71
|
if (user?.accessToken) {
|
|
62
72
|
return {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const jwt = require('../../lib/jwt');
|
|
2
|
+
const { UserModel } = require('../../models/userModel');
|
|
3
|
+
const { RingCentral } = require('../../lib/ringcentral');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP Tool: Get Session Info
|
|
7
|
+
*
|
|
8
|
+
* Returns non-sensitive information about the current MCP/CRM session.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const toolDefinition = {
|
|
12
|
+
name: 'getSessionInfo',
|
|
13
|
+
description: 'Get the current user session info, including RingCentral identity and CRM connection status.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {},
|
|
17
|
+
required: []
|
|
18
|
+
},
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
openWorldHint: false,
|
|
22
|
+
destructiveHint: false
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute the getSessionInfo tool
|
|
28
|
+
* @param {Object} args - Tool arguments injected by mcpHandler
|
|
29
|
+
* @param {string} [args.openaiSessionId] - OpenAI session identifier
|
|
30
|
+
* @param {string} [args.rcExtensionId] - Verified RingCentral extension identifier
|
|
31
|
+
* @param {string} [args.jwtToken] - CRM JWT token
|
|
32
|
+
* @param {string} [args.rcAccessToken] - RingCentral access token
|
|
33
|
+
* @returns {Object} Result object with session information
|
|
34
|
+
*/
|
|
35
|
+
async function execute(args = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const {
|
|
38
|
+
openaiSessionId = null,
|
|
39
|
+
rcExtensionId = null,
|
|
40
|
+
jwtToken,
|
|
41
|
+
rcAccessToken,
|
|
42
|
+
} = args;
|
|
43
|
+
|
|
44
|
+
const decodedToken = jwtToken ? jwt.decodeJwt(jwtToken) : null;
|
|
45
|
+
const userId = decodedToken?.id ?? null;
|
|
46
|
+
const user = userId ? await UserModel.findByPk(userId) : null;
|
|
47
|
+
|
|
48
|
+
let rcExtensionInfo = null;
|
|
49
|
+
if (rcExtensionId && rcAccessToken) {
|
|
50
|
+
const rcSDK = new RingCentral({
|
|
51
|
+
server: process.env.RINGCENTRAL_SERVER,
|
|
52
|
+
clientId: process.env.RINGCENTRAL_CLIENT_ID,
|
|
53
|
+
clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
|
|
54
|
+
redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
|
|
55
|
+
});
|
|
56
|
+
rcExtensionInfo = await rcSDK.getExtensionInfo(rcExtensionId, {
|
|
57
|
+
access_token: rcAccessToken,
|
|
58
|
+
token_type: 'Bearer'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
data: {
|
|
64
|
+
openaiSessionId,
|
|
65
|
+
dataToShow: {
|
|
66
|
+
isCrmAuthenticated: Boolean(decodedToken && user?.accessToken),
|
|
67
|
+
ringcentral: {
|
|
68
|
+
extensionId: rcExtensionId ?? null,
|
|
69
|
+
name: rcExtensionInfo?.name ?? null,
|
|
70
|
+
},
|
|
71
|
+
crm: {
|
|
72
|
+
userId,
|
|
73
|
+
platform: decodedToken?.platform ?? user?.platform ?? null,
|
|
74
|
+
hostname: user?.hostname ?? null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: error.message || 'Unknown error occurred',
|
|
84
|
+
errorDetails: error.stack
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
exports.definition = toolDefinition;
|
|
90
|
+
exports.execute = execute;
|
package/mcp/tools/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const getHelp = require('./getHelp');
|
|
10
10
|
const getPublicConnectors = require('./getPublicConnectors');
|
|
11
|
+
const getSessionInfo = require('./getSessionInfo');
|
|
11
12
|
const doAuth = require('./doAuth');
|
|
12
13
|
const checkAuthStatus = require('./checkAuthStatus');
|
|
13
14
|
const logout = require('./logout');
|
|
@@ -15,13 +16,14 @@ const findContact = require('./findContactByPhone');
|
|
|
15
16
|
const findContactWithName = require('./findContactByName');
|
|
16
17
|
const createCallLog = require('./createCallLog');
|
|
17
18
|
const rcGetCallLogs = require('./rcGetCallLogs');
|
|
18
|
-
const getGoogleFilePicker = require('./getGoogleFilePicker');
|
|
19
|
+
// const getGoogleFilePicker = require('./getGoogleFilePicker');
|
|
19
20
|
const createContact = require('./createContact');
|
|
20
21
|
|
|
21
22
|
// AI-visible MCP tools — registered in the MCP server
|
|
22
23
|
module.exports.tools = [
|
|
23
24
|
getHelp,
|
|
24
25
|
getPublicConnectors,
|
|
26
|
+
getSessionInfo,
|
|
25
27
|
logout,
|
|
26
28
|
findContact,
|
|
27
29
|
findContactWithName,
|
package/mcp/tools/logout.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const jwt = require('../../lib/jwt');
|
|
2
2
|
const { UserModel } = require('../../models/userModel');
|
|
3
3
|
const { LlmSessionModel } = require('../../models/llmSessionModel');
|
|
4
|
+
const { CacheModel } = require('../../models/cacheModel');
|
|
4
5
|
const connectorRegistry = require('../../connector/registry');
|
|
5
6
|
|
|
7
|
+
const RC_EXTENSION_CACHE_KEY = 'rcExtensionId';
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* MCP Tool: Logout
|
|
8
11
|
*
|
|
@@ -38,7 +41,7 @@ function isMissingSessionTableError(error) {
|
|
|
38
41
|
*/
|
|
39
42
|
async function execute(args) {
|
|
40
43
|
try {
|
|
41
|
-
const { jwtToken } = args;
|
|
44
|
+
const { jwtToken, rcExtensionId, openaiSessionId } = args;
|
|
42
45
|
const session = jwt.decodeJwt(jwtToken);
|
|
43
46
|
if (!session?.platform || !session?.id) {
|
|
44
47
|
throw new Error('Invalid JWT token');
|
|
@@ -46,6 +49,12 @@ async function execute(args) {
|
|
|
46
49
|
const { platform, id } = session;
|
|
47
50
|
try {
|
|
48
51
|
await LlmSessionModel.destroy({ where: { id } });
|
|
52
|
+
if (rcExtensionId && rcExtensionId !== id) {
|
|
53
|
+
await LlmSessionModel.destroy({ where: { id: rcExtensionId } });
|
|
54
|
+
}
|
|
55
|
+
if (openaiSessionId) {
|
|
56
|
+
await CacheModel.destroy({ where: { id: `${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}` } });
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
59
|
catch (error) {
|
|
51
60
|
if (!isMissingSessionTableError(error)) {
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.7.24": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "New",
|
|
6
|
+
"description": "Click-to-dial number matcher selection in User Setting -> General"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Fix",
|
|
10
|
+
"description": "Error report upload"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"1.7.23": {
|
|
15
|
+
"global": [
|
|
16
|
+
{
|
|
17
|
+
"type": "Rename",
|
|
18
|
+
"description": "Rename RingSense to ACE"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
2
22
|
"1.7.22": {
|
|
3
23
|
"global": [
|
|
4
24
|
{
|
|
@@ -142,8 +142,7 @@ describe('Admin Handler', () => {
|
|
|
142
142
|
|
|
143
143
|
expect(result).toEqual({
|
|
144
144
|
rcAccountId: 'rc-account-789',
|
|
145
|
-
rcExtensionId: 'extension-789'
|
|
146
|
-
rcUserName: 'Alex Johnson'
|
|
145
|
+
rcExtensionId: 'extension-789'
|
|
147
146
|
});
|
|
148
147
|
expect(axios.get).toHaveBeenCalledWith(
|
|
149
148
|
'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
|
|
@@ -24,6 +24,7 @@ jest.mock('../../models/dynamo/connectorSchema', () => ({
|
|
|
24
24
|
getProxyConfig: jest.fn()
|
|
25
25
|
}
|
|
26
26
|
}));
|
|
27
|
+
jest.mock('axios');
|
|
27
28
|
|
|
28
29
|
const logHandler = require('../../handlers/log');
|
|
29
30
|
const { CallLogModel } = require('../../models/callLogModel');
|
|
@@ -34,6 +35,7 @@ const connectorRegistry = require('../../connector/registry');
|
|
|
34
35
|
const oauth = require('../../lib/oauth');
|
|
35
36
|
const { composeCallLog } = require('../../lib/callLogComposer');
|
|
36
37
|
const { NoteCache } = require('../../models/dynamo/noteCacheSchema');
|
|
38
|
+
const axios = require('axios');
|
|
37
39
|
const { sequelize } = require('../../models/sequelize');
|
|
38
40
|
|
|
39
41
|
describe('Log Handler', () => {
|
|
@@ -61,6 +63,7 @@ describe('Log Handler', () => {
|
|
|
61
63
|
id: 'test-user-id',
|
|
62
64
|
platform: 'testCRM',
|
|
63
65
|
accessToken: 'test-access-token',
|
|
66
|
+
rcAccountId: '12345',
|
|
64
67
|
platformAdditionalInfo: {}
|
|
65
68
|
};
|
|
66
69
|
|
|
@@ -217,6 +220,63 @@ describe('Log Handler', () => {
|
|
|
217
220
|
expect(savedLog.thirdPartyLogId).toBe('new-log-123');
|
|
218
221
|
});
|
|
219
222
|
|
|
223
|
+
test('should call plugin with Bearer auth and without query jwt token', async () => {
|
|
224
|
+
await UserModel.create(mockUser);
|
|
225
|
+
await AccountDataModel.create({
|
|
226
|
+
rcAccountId: mockUser.rcAccountId,
|
|
227
|
+
platformName: 'testPlugin',
|
|
228
|
+
dataKey: 'pluginData',
|
|
229
|
+
data: {
|
|
230
|
+
name: 'plugin.sample',
|
|
231
|
+
supportedLogTypes: ['call'],
|
|
232
|
+
isAsync: false,
|
|
233
|
+
endpointUrl: 'https://plugins.example.com/plugin/testPlugin',
|
|
234
|
+
jwtToken: 'plugin-jwt-token'
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const mockConnector = {
|
|
239
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
240
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
241
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
242
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
243
|
+
logId: 'new-log-123',
|
|
244
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
245
|
+
})
|
|
246
|
+
};
|
|
247
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
248
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
249
|
+
|
|
250
|
+
axios.post.mockResolvedValue({
|
|
251
|
+
data: {
|
|
252
|
+
...mockIncomingData,
|
|
253
|
+
note: 'updated by plugin'
|
|
254
|
+
},
|
|
255
|
+
headers: {
|
|
256
|
+
'x-refreshed-jwt-token': 'refreshed-plugin-jwt'
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = await logHandler.createCallLog({
|
|
261
|
+
platform: 'testCRM',
|
|
262
|
+
userId: 'test-user-id',
|
|
263
|
+
incomingData: mockIncomingData,
|
|
264
|
+
hashedAccountId: 'hashed-123',
|
|
265
|
+
isFromSSCL: false
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.successful).toBe(true);
|
|
269
|
+
expect(axios.post).toHaveBeenCalledWith(
|
|
270
|
+
'https://plugins.example.com/plugin/testPlugin',
|
|
271
|
+
{ data: mockIncomingData, config: null },
|
|
272
|
+
{
|
|
273
|
+
headers: {
|
|
274
|
+
Authorization: 'Bearer plugin-jwt-token'
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
220
280
|
test('should successfully create call log with oauth auth', async () => {
|
|
221
281
|
// Arrange
|
|
222
282
|
const oauthUser = { ...mockUser };
|
|
@@ -9,18 +9,24 @@ jest.mock('../../models/sequelize', () => {
|
|
|
9
9
|
}),
|
|
10
10
|
};
|
|
11
11
|
});
|
|
12
|
+
jest.mock('axios');
|
|
12
13
|
|
|
13
14
|
const pluginHandler = require('../../handlers/plugin');
|
|
14
15
|
const { CacheModel } = require('../../models/cacheModel');
|
|
16
|
+
const { AccountDataModel } = require('../../models/accountDataModel');
|
|
17
|
+
const axios = require('axios');
|
|
15
18
|
const { sequelize } = require('../../models/sequelize');
|
|
16
19
|
|
|
17
20
|
describe('Plugin Handler', () => {
|
|
18
21
|
beforeAll(async () => {
|
|
22
|
+
process.env.HASH_KEY = 'unit-test-hash-key';
|
|
19
23
|
await CacheModel.sync({ force: true });
|
|
24
|
+
await AccountDataModel.sync({ force: true });
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
afterEach(async () => {
|
|
23
28
|
await CacheModel.destroy({ where: {} });
|
|
29
|
+
await AccountDataModel.destroy({ where: {} });
|
|
24
30
|
jest.clearAllMocks();
|
|
25
31
|
});
|
|
26
32
|
|
|
@@ -283,5 +289,92 @@ describe('Plugin Handler', () => {
|
|
|
283
289
|
expect(remainingTask).not.toBeNull();
|
|
284
290
|
});
|
|
285
291
|
});
|
|
292
|
+
|
|
293
|
+
describe('registerPluginAccount', () => {
|
|
294
|
+
test('should register plugin account and persist plugin jwt token in account data', async () => {
|
|
295
|
+
const rcAccountId = '12345';
|
|
296
|
+
const pluginId = 'sync-all-caps';
|
|
297
|
+
|
|
298
|
+
axios.get.mockResolvedValue({
|
|
299
|
+
data: {
|
|
300
|
+
platforms: {
|
|
301
|
+
'plugin.sample': {
|
|
302
|
+
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
303
|
+
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
axios.post.mockResolvedValue({
|
|
309
|
+
data: {
|
|
310
|
+
jwtToken: 'plugin-jwt-token'
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await pluginHandler.registerPluginAccount({
|
|
315
|
+
pluginId,
|
|
316
|
+
rcAccessToken: 'rc-access-token',
|
|
317
|
+
rcAccountId,
|
|
318
|
+
pluginAccess: 'public',
|
|
319
|
+
pluginName: 'plugin.sample'
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.successful).toBe(true);
|
|
323
|
+
expect(axios.post).toHaveBeenCalledWith(
|
|
324
|
+
`https://plugins.example.com/plugin/${pluginId}/auth/register`,
|
|
325
|
+
{
|
|
326
|
+
rcAccessToken: 'rc-access-token',
|
|
327
|
+
rcAccountId
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const accountData = await AccountDataModel.findOne({
|
|
332
|
+
where: {
|
|
333
|
+
rcAccountId,
|
|
334
|
+
platformName: pluginId,
|
|
335
|
+
dataKey: 'pluginData'
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
expect(accountData).not.toBeNull();
|
|
339
|
+
expect(accountData.data.jwtToken).toBe('plugin-jwt-token');
|
|
340
|
+
expect(accountData.data.endpointUrl).toBe(`https://plugins.example.com/plugin/${pluginId}`);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('should throw when register API does not return jwt token', async () => {
|
|
344
|
+
const rcAccountId = '12345';
|
|
345
|
+
const pluginId = 'sync-all-caps';
|
|
346
|
+
|
|
347
|
+
axios.get.mockResolvedValue({
|
|
348
|
+
data: {
|
|
349
|
+
platforms: {
|
|
350
|
+
'plugin.sample': {
|
|
351
|
+
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
352
|
+
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
axios.post.mockResolvedValue({ data: {} });
|
|
358
|
+
|
|
359
|
+
await expect(pluginHandler.registerPluginAccount({
|
|
360
|
+
pluginId,
|
|
361
|
+
rcAccessToken: 'rc-access-token',
|
|
362
|
+
rcAccountId,
|
|
363
|
+
pluginAccess: 'public',
|
|
364
|
+
pluginName: 'plugin.sample'
|
|
365
|
+
})).rejects.toThrow('Plugin register API did not return jwtToken');
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('token header helper', () => {
|
|
370
|
+
test('should parse refreshed jwt token from response headers', () => {
|
|
371
|
+
const token = pluginHandler.getRefreshedJwtTokenFromHeaders({
|
|
372
|
+
headers: {
|
|
373
|
+
'x-refreshed-jwt-token': 'new-plugin-token'
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
expect(token).toBe('new-plugin-token');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
286
379
|
});
|
|
287
380
|
|