@app-connect/core 1.7.18 → 1.7.19

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 (57) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/log.js +181 -10
  3. package/handlers/plugin.js +27 -0
  4. package/handlers/user.js +31 -2
  5. package/index.js +99 -22
  6. package/lib/authSession.js +21 -12
  7. package/lib/callLogComposer.js +1 -1
  8. package/lib/debugTracer.js +20 -2
  9. package/lib/util.js +21 -4
  10. package/mcp/README.md +392 -0
  11. package/mcp/mcpHandler.js +293 -82
  12. package/mcp/tools/checkAuthStatus.js +27 -34
  13. package/mcp/tools/createCallLog.js +13 -9
  14. package/mcp/tools/createContact.js +2 -6
  15. package/mcp/tools/doAuth.js +27 -157
  16. package/mcp/tools/findContactByName.js +6 -9
  17. package/mcp/tools/findContactByPhone.js +2 -6
  18. package/mcp/tools/getGoogleFilePicker.js +5 -9
  19. package/mcp/tools/getHelp.js +2 -3
  20. package/mcp/tools/getPublicConnectors.js +41 -28
  21. package/mcp/tools/index.js +11 -36
  22. package/mcp/tools/logout.js +5 -10
  23. package/mcp/tools/rcGetCallLogs.js +3 -20
  24. package/mcp/ui/App/App.tsx +361 -0
  25. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  26. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  27. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  28. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  29. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  30. package/mcp/ui/App/lib/callTool.ts +130 -0
  31. package/mcp/ui/App/lib/debugLog.ts +41 -0
  32. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  33. package/mcp/ui/App/main.css +6 -0
  34. package/mcp/ui/App/root.tsx +13 -0
  35. package/mcp/ui/dist/index.html +53 -0
  36. package/mcp/ui/index.html +13 -0
  37. package/mcp/ui/package-lock.json +6356 -0
  38. package/mcp/ui/package.json +25 -0
  39. package/mcp/ui/tsconfig.json +26 -0
  40. package/mcp/ui/vite.config.ts +16 -0
  41. package/models/llmSessionModel.js +14 -0
  42. package/package.json +72 -72
  43. package/releaseNotes.json +12 -0
  44. package/test/handlers/plugin.test.js +287 -0
  45. package/test/lib/util.test.js +379 -1
  46. package/test/mcp/tools/createCallLog.test.js +3 -3
  47. package/test/mcp/tools/doAuth.test.js +40 -303
  48. package/test/mcp/tools/findContactByName.test.js +3 -3
  49. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  50. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  51. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  52. package/test/mcp/tools/logout.test.js +2 -2
  53. package/mcp/SupportedPlatforms.md +0 -12
  54. package/mcp/tools/collectAuthInfo.js +0 -91
  55. package/mcp/tools/setConnector.js +0 -69
  56. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  57. package/test/mcp/tools/setConnector.test.js +0 -177
@@ -1,376 +1,113 @@
1
1
  const doAuth = require('../../../mcp/tools/doAuth');
2
- const authCore = require('../../../handlers/auth');
3
- const jwt = require('../../../lib/jwt');
2
+ const { createAuthSession } = require('../../../lib/authSession');
4
3
 
5
- // Mock dependencies
6
- jest.mock('../../../handlers/auth');
7
- jest.mock('../../../lib/jwt');
4
+ jest.mock('../../../lib/authSession');
8
5
 
9
6
  describe('MCP Tool: doAuth', () => {
10
7
  beforeEach(() => {
11
8
  jest.clearAllMocks();
12
- process.env.APP_SERVER_SECRET_KEY = 'test-secret-key';
13
9
  });
14
10
 
15
11
  describe('tool definition', () => {
16
12
  test('should have correct tool definition', () => {
17
13
  expect(doAuth.definition).toBeDefined();
18
14
  expect(doAuth.definition.name).toBe('doAuth');
19
- expect(doAuth.definition.description).toContain('Auth flow step.4');
15
+ expect(doAuth.definition.description).toContain('OAuth session');
20
16
  expect(doAuth.definition.inputSchema).toBeDefined();
21
17
  });
22
18
 
23
- test('should have optional parameters', () => {
24
- expect(doAuth.definition.inputSchema.properties).toHaveProperty('connectorManifest');
19
+ test('should require connectorName and have optional hostname', () => {
20
+ expect(doAuth.definition.inputSchema.required).toContain('connectorName');
25
21
  expect(doAuth.definition.inputSchema.properties).toHaveProperty('connectorName');
26
22
  expect(doAuth.definition.inputSchema.properties).toHaveProperty('hostname');
27
- expect(doAuth.definition.inputSchema.properties).toHaveProperty('apiKey');
28
- expect(doAuth.definition.inputSchema.properties).toHaveProperty('additionalInfo');
29
- expect(doAuth.definition.inputSchema.properties).toHaveProperty('callbackUri');
30
23
  });
31
24
  });
32
25
 
33
- describe('execute - apiKey authentication', () => {
34
- test('should authenticate with API key successfully', async () => {
26
+ describe('execute', () => {
27
+ test('should create auth session successfully', async () => {
35
28
  // Arrange
36
- const mockManifest = {
37
- platforms: {
38
- testCRM: {
39
- name: 'testCRM',
40
- auth: { type: 'apiKey', apiKey: { name: 'apiKey' } },
41
- environment: { type: 'fixed' }
42
- }
43
- }
44
- };
45
-
46
- const mockUserInfo = {
47
- id: 'test-user-123',
48
- name: 'Test User'
49
- };
50
-
51
- authCore.onApiKeyLogin.mockResolvedValue({
52
- userInfo: mockUserInfo
53
- });
54
-
55
- jwt.generateJwt.mockReturnValue('mock-jwt-token');
29
+ createAuthSession.mockResolvedValue(undefined);
56
30
 
57
31
  // Act
58
32
  const result = await doAuth.execute({
59
- connectorManifest: mockManifest,
60
- connectorName: 'testCRM',
61
- hostname: 'test.crm.com',
62
- apiKey: 'test-api-key'
33
+ sessionId: 'session-abc',
34
+ connectorName: 'pipedrive',
35
+ hostname: 'mycompany.pipedrive.com'
63
36
  });
64
37
 
65
38
  // Assert
66
- expect(result).toEqual({
67
- success: true,
68
- data: {
69
- jwtToken: 'mock-jwt-token',
70
- message: expect.stringContaining('IMPORTANT')
71
- }
72
- });
73
- expect(authCore.onApiKeyLogin).toHaveBeenCalledWith({
74
- platform: 'testCRM',
75
- hostname: 'test.crm.com',
76
- apiKey: 'test-api-key',
77
- additionalInfo: undefined
78
- });
79
- expect(jwt.generateJwt).toHaveBeenCalledWith({
80
- id: 'test-user-123',
81
- platform: 'testCRM'
39
+ expect(result).toEqual({ success: true });
40
+ expect(createAuthSession).toHaveBeenCalledWith('session-abc', {
41
+ platform: 'pipedrive',
42
+ hostname: 'mycompany.pipedrive.com'
82
43
  });
83
44
  });
84
45
 
85
- test('should handle apiKey authentication with additional info', async () => {
46
+ test('should create auth session with empty hostname when not provided', async () => {
86
47
  // Arrange
87
- const mockManifest = {
88
- platforms: {
89
- testCRM: {
90
- name: 'testCRM',
91
- auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
92
- }
93
- }
94
- };
95
-
96
- const additionalInfo = {
97
- username: 'testuser',
98
- password: 'testpass',
99
- apiUrl: 'https://api.test.com'
100
- };
101
-
102
- const mockUserInfo = {
103
- id: 'test-user-456',
104
- name: 'Test User'
105
- };
106
-
107
- authCore.onApiKeyLogin.mockResolvedValue({
108
- userInfo: mockUserInfo
109
- });
110
-
111
- jwt.generateJwt.mockReturnValue('mock-jwt-token-2');
48
+ createAuthSession.mockResolvedValue(undefined);
112
49
 
113
50
  // Act
114
51
  const result = await doAuth.execute({
115
- connectorManifest: mockManifest,
116
- connectorName: 'testCRM',
117
- hostname: 'test.crm.com',
118
- apiKey: 'test-api-key',
119
- additionalInfo
52
+ sessionId: 'session-xyz',
53
+ connectorName: 'clio'
120
54
  });
121
55
 
122
56
  // Assert
123
- expect(result.success).toBe(true);
124
- expect(authCore.onApiKeyLogin).toHaveBeenCalledWith({
125
- platform: 'testCRM',
126
- hostname: 'test.crm.com',
127
- apiKey: 'test-api-key',
128
- additionalInfo
57
+ expect(result).toEqual({ success: true });
58
+ expect(createAuthSession).toHaveBeenCalledWith('session-xyz', {
59
+ platform: 'clio',
60
+ hostname: ''
129
61
  });
130
62
  });
131
63
 
132
- test('should return error when user info not found', async () => {
133
- // Arrange
134
- const mockManifest = {
135
- platforms: {
136
- testCRM: {
137
- name: 'testCRM',
138
- auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
139
- }
140
- }
141
- };
142
-
143
- authCore.onApiKeyLogin.mockResolvedValue({
144
- userInfo: null
145
- });
146
-
64
+ test('should return error when sessionId is missing', async () => {
147
65
  // Act
148
- const result = await doAuth.execute({
149
- connectorManifest: mockManifest,
150
- connectorName: 'testCRM',
151
- hostname: 'test.crm.com',
152
- apiKey: 'invalid-api-key'
153
- });
66
+ const result = await doAuth.execute({ connectorName: 'pipedrive' });
154
67
 
155
68
  // Assert
156
69
  expect(result).toEqual({
157
70
  success: false,
158
- error: 'Authentication failed',
159
- errorDetails: 'User info not found'
160
- });
161
- });
162
- });
163
-
164
- describe('execute - OAuth authentication', () => {
165
- test('should return auth URI when callback not provided', async () => {
166
- // Arrange
167
- const mockManifest = {
168
- platforms: {
169
- salesforce: {
170
- name: 'salesforce',
171
- auth: {
172
- type: 'oauth',
173
- oauth: {
174
- authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
175
- clientId: 'test-client-id',
176
- scope: 'api refresh_token',
177
- customState: ''
178
- }
179
- }
180
- }
181
- }
182
- };
183
-
184
- // Act
185
- const result = await doAuth.execute({
186
- connectorManifest: mockManifest,
187
- connectorName: 'salesforce'
71
+ error: 'Missing required fields: sessionId, connectorName'
188
72
  });
189
-
190
- // Assert
191
- expect(result.success).toBe(true);
192
- expect(result.data.authUri).toContain('https://login.salesforce.com');
193
- expect(result.data.authUri).toContain('client_id=test-client-id');
194
- expect(result.data.authUri).toContain('response_type=code');
195
- expect(result.data.message).toContain('IMPORTANT');
73
+ expect(createAuthSession).not.toHaveBeenCalled();
196
74
  });
197
75
 
198
- test('should handle OAuth callback successfully', async () => {
199
- // Arrange
200
- const mockManifest = {
201
- platforms: {
202
- salesforce: {
203
- name: 'salesforce',
204
- auth: {
205
- type: 'oauth',
206
- oauth: {
207
- authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
208
- clientId: 'test-client-id'
209
- }
210
- }
211
- }
212
- }
213
- };
214
-
215
- const mockUserInfo = {
216
- id: 'sf-user-123',
217
- name: 'SF User'
218
- };
219
-
220
- authCore.onOAuthCallback.mockResolvedValue({
221
- userInfo: mockUserInfo
222
- });
223
-
224
- jwt.generateJwt.mockReturnValue('mock-jwt-token-oauth');
225
-
76
+ test('should return error when connectorName is missing', async () => {
226
77
  // Act
227
- const result = await doAuth.execute({
228
- connectorManifest: mockManifest,
229
- connectorName: 'salesforce',
230
- hostname: 'login.salesforce.com',
231
- callbackUri: 'https://redirect.com?code=test-code&state=test-state'
232
- });
233
-
234
- // Assert
235
- expect(result).toEqual({
236
- success: true,
237
- data: {
238
- jwtToken: 'mock-jwt-token-oauth',
239
- message: expect.stringContaining('IMPORTANT')
240
- }
241
- });
242
- expect(authCore.onOAuthCallback).toHaveBeenCalledWith({
243
- platform: 'salesforce',
244
- hostname: 'login.salesforce.com',
245
- callbackUri: 'https://redirect.com?code=test-code&state=test-state',
246
- query: expect.objectContaining({
247
- hostname: 'login.salesforce.com'
248
- })
249
- });
250
- });
251
-
252
- test('should return error when OAuth callback fails', async () => {
253
- // Arrange
254
- const mockManifest = {
255
- platforms: {
256
- salesforce: {
257
- name: 'salesforce',
258
- auth: {
259
- type: 'oauth',
260
- oauth: {
261
- authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
262
- clientId: 'test-client-id'
263
- }
264
- }
265
- }
266
- }
267
- };
268
-
269
- authCore.onOAuthCallback.mockResolvedValue({
270
- userInfo: null
271
- });
272
-
273
- // Act - callbackUri needs code= and state= to be treated as OAuth callback
274
- const result = await doAuth.execute({
275
- connectorManifest: mockManifest,
276
- connectorName: 'salesforce',
277
- hostname: 'login.salesforce.com',
278
- callbackUri: 'https://redirect.com?code=invalid-code&state=test-state'
279
- });
78
+ const result = await doAuth.execute({ sessionId: 'session-abc' });
280
79
 
281
80
  // Assert
282
81
  expect(result).toEqual({
283
82
  success: false,
284
- error: 'Authentication failed',
285
- errorDetails: 'User info not found'
83
+ error: 'Missing required fields: sessionId, connectorName'
286
84
  });
85
+ expect(createAuthSession).not.toHaveBeenCalled();
287
86
  });
288
87
 
289
- test('should include custom state in auth URI', async () => {
290
- // Arrange
291
- const mockManifest = {
292
- platforms: {
293
- customCRM: {
294
- name: 'customCRM',
295
- auth: {
296
- type: 'oauth',
297
- oauth: {
298
- authUrl: 'https://custom.com/oauth',
299
- clientId: 'custom-client-id',
300
- scope: '',
301
- customState: 'custom=state&other=value'
302
- }
303
- }
304
- }
305
- }
306
- };
307
-
308
- // Act
309
- const result = await doAuth.execute({
310
- connectorManifest: mockManifest,
311
- connectorName: 'customCRM'
312
- });
313
-
314
- // Assert - state is now URL-encoded and includes sessionId, platform, hostname, plus customState
315
- expect(result.success).toBe(true);
316
- // The state parameter now contains session info and custom state appended
317
- // Decode and verify custom state is included
318
- const stateMatch = result.data.authUri.match(/state=([^&]+)/);
319
- expect(stateMatch).toBeTruthy();
320
- const decodedState = decodeURIComponent(stateMatch[1]);
321
- expect(decodedState).toContain('custom=state&other=value');
322
- expect(decodedState).toContain('sessionId=');
323
- expect(decodedState).toContain('platform=customCRM');
324
- });
325
- });
326
-
327
- describe('error handling', () => {
328
- test('should handle authentication errors gracefully', async () => {
329
- // Arrange
330
- const mockManifest = {
331
- platforms: {
332
- testCRM: {
333
- name: 'testCRM',
334
- auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
335
- }
336
- }
337
- };
338
-
339
- authCore.onApiKeyLogin.mockRejectedValue(
340
- new Error('Invalid credentials')
341
- );
342
-
88
+ test('should return error when both sessionId and connectorName are missing', async () => {
343
89
  // Act
344
- const result = await doAuth.execute({
345
- connectorManifest: mockManifest,
346
- connectorName: 'testCRM',
347
- hostname: 'test.crm.com',
348
- apiKey: 'bad-key'
349
- });
90
+ const result = await doAuth.execute({});
350
91
 
351
92
  // Assert
352
93
  expect(result.success).toBe(false);
353
- expect(result.error).toBe('Invalid credentials');
354
- expect(result.errorDetails).toBeDefined();
94
+ expect(result.error).toContain('Missing required fields');
355
95
  });
356
96
 
357
- test('should handle missing platform in manifest', async () => {
97
+ test('should handle unexpected errors gracefully', async () => {
358
98
  // Arrange
359
- const mockManifest = {
360
- platforms: {}
361
- };
99
+ createAuthSession.mockRejectedValue(new Error('DB write failed'));
362
100
 
363
101
  // Act
364
102
  const result = await doAuth.execute({
365
- connectorManifest: mockManifest,
366
- connectorName: 'nonExistent',
367
- apiKey: 'test-key'
103
+ sessionId: 'session-abc',
104
+ connectorName: 'pipedrive'
368
105
  });
369
106
 
370
107
  // Assert
371
108
  expect(result.success).toBe(false);
372
- expect(result.error).toBeDefined();
109
+ expect(result.error).toBe('DB write failed');
110
+ expect(result.errorDetails).toBeDefined();
373
111
  });
374
112
  });
375
113
  });
376
-
@@ -17,12 +17,12 @@ describe('MCP Tool: findContactByName', () => {
17
17
  test('should have correct tool definition', () => {
18
18
  expect(findContactByName.definition).toBeDefined();
19
19
  expect(findContactByName.definition.name).toBe('findContactByName');
20
- expect(findContactByName.definition.description).toContain('REQUIRES AUTHENTICATION');
20
+ expect(findContactByName.definition.description).toContain('REQUIRES CRM CONNECTION');
21
21
  expect(findContactByName.definition.inputSchema).toBeDefined();
22
22
  });
23
23
 
24
- test('should require jwtToken and name parameters', () => {
25
- expect(findContactByName.definition.inputSchema.required).toContain('jwtToken');
24
+ test('should require name parameter (jwtToken is server-injected)', () => {
25
+ expect(findContactByName.definition.inputSchema.required).not.toContain('jwtToken');
26
26
  expect(findContactByName.definition.inputSchema.required).toContain('name');
27
27
  });
28
28
  });
@@ -17,12 +17,12 @@ describe('MCP Tool: findContactByPhone', () => {
17
17
  test('should have correct tool definition', () => {
18
18
  expect(findContactByPhone.definition).toBeDefined();
19
19
  expect(findContactByPhone.definition.name).toBe('findContactByPhone');
20
- expect(findContactByPhone.definition.description).toContain('REQUIRES AUTHENTICATION');
20
+ expect(findContactByPhone.definition.description).toContain('REQUIRES CRM CONNECTION');
21
21
  expect(findContactByPhone.definition.inputSchema).toBeDefined();
22
22
  });
23
23
 
24
- test('should require jwtToken and phoneNumber parameters', () => {
25
- expect(findContactByPhone.definition.inputSchema.required).toContain('jwtToken');
24
+ test('should require phoneNumber parameter (jwtToken is server-injected)', () => {
25
+ expect(findContactByPhone.definition.inputSchema.required).not.toContain('jwtToken');
26
26
  expect(findContactByPhone.definition.inputSchema.required).toContain('phoneNumber');
27
27
  });
28
28
 
@@ -18,13 +18,13 @@ describe('MCP Tool: getGoogleFilePicker', () => {
18
18
  test('should have correct tool definition', () => {
19
19
  expect(getGoogleFilePicker.definition).toBeDefined();
20
20
  expect(getGoogleFilePicker.definition.name).toBe('getGoogleFilePicker');
21
- expect(getGoogleFilePicker.definition.description).toContain('REQUIRES AUTHENTICATION');
21
+ expect(getGoogleFilePicker.definition.description).toContain('REQUIRES CRM CONNECTION');
22
22
  expect(getGoogleFilePicker.definition.description).toContain('Google Sheets file picker');
23
23
  expect(getGoogleFilePicker.definition.inputSchema).toBeDefined();
24
24
  });
25
25
 
26
- test('should require jwtToken parameter', () => {
27
- expect(getGoogleFilePicker.definition.inputSchema.required).toContain('jwtToken');
26
+ test('should not require jwtToken in schema (it is server-injected)', () => {
27
+ expect(getGoogleFilePicker.definition.inputSchema.required).not.toContain('jwtToken');
28
28
  });
29
29
 
30
30
  test('should have optional sheetName parameter', () => {
@@ -62,7 +62,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
62
62
  expect(result).toEqual({
63
63
  success: true,
64
64
  data: {
65
- filePickerUrl: 'https://test-app-server.com/googleSheets/filePicker?token=mock-jwt-token}',
65
+ filePickerUrl: 'https://test-app-server.com/googleSheets/filePicker?token=mock-jwt-token',
66
66
  message: expect.stringContaining('Please open this URL')
67
67
  }
68
68
  });
@@ -149,7 +149,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
149
149
  // Assert
150
150
  expect(result).toEqual({
151
151
  success: false,
152
- error: 'JWT token is required. Please authenticate with googleSheets platform first using the doAuth tool.'
152
+ error: 'JWT token is required. Please connect to the CRM first using getPublicConnectors.'
153
153
  });
154
154
  });
155
155
 
@@ -162,7 +162,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
162
162
  // Assert
163
163
  expect(result).toEqual({
164
164
  success: false,
165
- error: 'JWT token is required. Please authenticate with googleSheets platform first using the doAuth tool.'
165
+ error: 'JWT token is required. Please connect to the CRM first using getPublicConnectors.'
166
166
  });
167
167
  });
168
168
 
@@ -217,7 +217,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
217
217
  // Assert
218
218
  expect(result).toEqual({
219
219
  success: false,
220
- error: 'User not found. Please authenticate with googleSheets platform first.'
220
+ error: 'User not found. Please connect to the CRM first using getPublicConnectors.'
221
221
  });
222
222
  expect(UserModel.findByPk).toHaveBeenCalledWith('nonexistent-user');
223
223
  });
@@ -1,20 +1,19 @@
1
1
  const getPublicConnectors = require('../../../mcp/tools/getPublicConnectors');
2
- const developerPortal = require('../../../connector/developerPortal');
2
+ const axios = require('axios');
3
3
 
4
- // Mock the developerPortal module
5
- jest.mock('../../../connector/developerPortal');
4
+ jest.mock('axios');
6
5
 
7
6
  describe('MCP Tool: getPublicConnectors', () => {
8
7
  beforeEach(() => {
9
8
  jest.clearAllMocks();
10
- delete process.env.RC_ACCOUNT_ID;
9
+ process.env.APP_SERVER = 'https://test-server.com';
11
10
  });
12
11
 
13
12
  describe('tool definition', () => {
14
13
  test('should have correct tool definition', () => {
15
14
  expect(getPublicConnectors.definition).toBeDefined();
16
15
  expect(getPublicConnectors.definition.name).toBe('getPublicConnectors');
17
- expect(getPublicConnectors.definition.description).toContain('Auth flow step.1');
16
+ expect(getPublicConnectors.definition.description).toContain('connectors');
18
17
  expect(getPublicConnectors.definition.inputSchema).toBeDefined();
19
18
  expect(getPublicConnectors.definition.inputSchema.type).toBe('object');
20
19
  });
@@ -25,104 +24,84 @@ describe('MCP Tool: getPublicConnectors', () => {
25
24
  });
26
25
 
27
26
  describe('execute', () => {
28
- test('should return public connectors successfully', async () => {
29
- // Arrange - use supported platform names: 'googleSheets' and 'clio'
30
- const mockConnectors = [
31
- { id: '1', name: 'googleSheets', displayName: 'Google Sheets' },
32
- { id: '2', name: 'clio', displayName: 'Clio' }
33
- ];
34
-
35
- developerPortal.getPublicConnectorList.mockResolvedValue({
36
- connectors: mockConnectors
37
- });
38
-
27
+ test('should return structuredContent with server URL when no rcAccessToken', async () => {
39
28
  // Act
40
- const result = await getPublicConnectors.execute();
29
+ const result = await getPublicConnectors.execute({});
41
30
 
42
31
  // Assert
43
32
  expect(result).toEqual({
44
- success: true,
45
- data: ['Google Sheets', 'Clio']
33
+ structuredContent: {
34
+ serverUrl: 'https://test-server.com',
35
+ rcExtensionId: null,
36
+ rcAccountId: null,
37
+ openaiSessionId: null,
38
+ }
46
39
  });
47
- expect(developerPortal.getPublicConnectorList).toHaveBeenCalledTimes(1);
40
+ expect(axios.get).not.toHaveBeenCalled();
48
41
  });
49
42
 
50
- test('should include private connectors when RC_ACCOUNT_ID is set', async () => {
51
- // Arrange - use supported platform names
52
- process.env.RC_ACCOUNT_ID = 'test-account-id';
53
-
54
- const mockPublicConnectors = [
55
- { id: '1', name: 'googleSheets', displayName: 'Google Sheets' }
56
- ];
57
- const mockPrivateConnectors = [
58
- { id: '3', name: 'clio', displayName: 'Clio' }
59
- ];
60
-
61
- developerPortal.getPublicConnectorList.mockResolvedValue({
62
- connectors: mockPublicConnectors
63
- });
64
- developerPortal.getPrivateConnectorList.mockResolvedValue({
65
- privateConnectors: mockPrivateConnectors
43
+ test('should resolve RC account and extension IDs when rcAccessToken provided', async () => {
44
+ // Arrange
45
+ axios.get.mockResolvedValue({
46
+ data: { id: 'ext-456', account: { id: 'acc-789' } }
66
47
  });
67
48
 
68
49
  // Act
69
- const result = await getPublicConnectors.execute();
50
+ const result = await getPublicConnectors.execute({
51
+ rcAccessToken: 'valid-rc-token',
52
+ openaiSessionId: 'session-abc'
53
+ });
70
54
 
71
55
  // Assert
72
56
  expect(result).toEqual({
73
- success: true,
74
- data: ['Google Sheets', 'Clio']
57
+ structuredContent: {
58
+ serverUrl: 'https://test-server.com',
59
+ rcExtensionId: 'ext-456',
60
+ rcAccountId: 'acc-789',
61
+ openaiSessionId: 'session-abc',
62
+ }
75
63
  });
76
- expect(developerPortal.getPublicConnectorList).toHaveBeenCalledTimes(1);
77
- expect(developerPortal.getPrivateConnectorList).toHaveBeenCalledTimes(1);
64
+ expect(axios.get).toHaveBeenCalledWith(
65
+ 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
66
+ { headers: { Authorization: 'Bearer valid-rc-token' } }
67
+ );
78
68
  });
79
69
 
80
- test('should return empty array when no connectors available', async () => {
81
- // Arrange
82
- developerPortal.getPublicConnectorList.mockResolvedValue({
83
- connectors: []
84
- });
70
+ test('should return null RC IDs and continue when RC API call fails', async () => {
71
+ // Arrange — RC API failure is non-fatal: widget only shows public connectors
72
+ axios.get.mockRejectedValue(new Error('RC API unavailable'));
85
73
 
86
74
  // Act
87
- const result = await getPublicConnectors.execute();
75
+ const result = await getPublicConnectors.execute({ rcAccessToken: 'bad-token' });
88
76
 
89
- // Assert
77
+ // Assert — still returns structuredContent, just without RC IDs
90
78
  expect(result).toEqual({
91
- success: true,
92
- data: []
79
+ structuredContent: {
80
+ serverUrl: 'https://test-server.com',
81
+ rcExtensionId: null,
82
+ rcAccountId: null,
83
+ openaiSessionId: null,
84
+ }
93
85
  });
94
86
  });
95
87
 
96
- test('should handle errors gracefully', async () => {
97
- // Arrange
98
- const errorMessage = 'Failed to fetch connectors';
99
- developerPortal.getPublicConnectorList.mockRejectedValue(
100
- new Error(errorMessage)
101
- );
102
-
88
+ test('should include openaiSessionId when provided', async () => {
103
89
  // Act
104
- const result = await getPublicConnectors.execute();
90
+ const result = await getPublicConnectors.execute({ openaiSessionId: 'my-session' });
105
91
 
106
92
  // Assert
107
- expect(result.success).toBe(false);
108
- expect(result.error).toBe(errorMessage);
109
- expect(result.errorDetails).toBeDefined();
93
+ expect(result.structuredContent.openaiSessionId).toBe('my-session');
110
94
  });
111
95
 
112
- test('should handle network errors', async () => {
96
+ test('should use default server URL when APP_SERVER is not set', async () => {
113
97
  // Arrange
114
- const networkError = new Error('Network request failed');
115
- networkError.code = 'ECONNREFUSED';
116
- developerPortal.getPublicConnectorList.mockRejectedValue(networkError);
98
+ delete process.env.APP_SERVER;
117
99
 
118
100
  // Act
119
- const result = await getPublicConnectors.execute();
101
+ const result = await getPublicConnectors.execute({});
120
102
 
121
103
  // Assert
122
- expect(result.success).toBe(false);
123
- expect(result.error).toBe('Network request failed');
124
- expect(result.errorDetails).toBeDefined();
104
+ expect(result.structuredContent.serverUrl).toBe('https://localhost:6066');
125
105
  });
126
106
  });
127
107
  });
128
-