@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.
Files changed (49) 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 +18 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +22 -11
  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 +44 -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/auth.test.js +6 -2
  25. package/test/handlers/calldown.test.js +310 -0
  26. package/test/handlers/disposition.test.js +396 -0
  27. package/test/handlers/log.test.js +327 -3
  28. package/test/handlers/managedOAuth.test.js +262 -0
  29. package/test/handlers/plugin.test.js +305 -0
  30. package/test/handlers/user.test.js +381 -0
  31. package/test/index.test.js +166 -1
  32. package/test/lib/analytics.test.js +146 -0
  33. package/test/lib/authSession.test.js +173 -0
  34. package/test/lib/encode.test.js +59 -0
  35. package/test/lib/errorHandler.test.js +246 -0
  36. package/test/lib/generalErrorMessage.test.js +82 -0
  37. package/test/lib/s3ErrorLogReport.test.js +187 -0
  38. package/test/mcp/mcpHandlerMore.test.js +384 -0
  39. package/test/mcp/tools/appointmentTools.test.js +362 -0
  40. package/test/models/callDownListModel.test.js +125 -0
  41. package/test/models/dynamo/lockSchema.test.js +37 -0
  42. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  43. package/test/models/llmSessionModel.test.js +91 -0
  44. package/test/models/models.test.js +92 -0
  45. package/test/routes/calldownRoutes.test.js +224 -0
  46. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  47. package/test/routes/dispositionRoutes.test.js +192 -0
  48. package/test/routes/managedAuthRoutes.test.js +151 -0
  49. package/test/routes/pluginRoutes.test.js +262 -0
@@ -0,0 +1,855 @@
1
+ const express = require('express');
2
+ const request = require('supertest');
3
+
4
+ jest.mock('../../handlers/log', () => ({
5
+ saveNoteCache: jest.fn(),
6
+ getCallLog: jest.fn(),
7
+ createCallLog: jest.fn(),
8
+ updateCallLog: jest.fn(),
9
+ createMessageLog: jest.fn(),
10
+ handleAsyncPluginCallback: jest.fn(),
11
+ }));
12
+ jest.mock('../../handlers/contact', () => ({
13
+ findContact: jest.fn(),
14
+ createContact: jest.fn(),
15
+ findContactWithName: jest.fn(),
16
+ }));
17
+ jest.mock('../../handlers/appointment', () => ({
18
+ listAppointments: jest.fn(),
19
+ createAppointment: jest.fn(),
20
+ updateAppointment: jest.fn(),
21
+ refreshAppointment: jest.fn(),
22
+ confirmAppointment: jest.fn(),
23
+ cancelAppointment: jest.fn(),
24
+ }));
25
+ jest.mock('../../handlers/auth', () => ({
26
+ getLicenseStatus: jest.fn(),
27
+ authValidation: jest.fn(),
28
+ onOAuthCallback: jest.fn(),
29
+ onApiKeyLogin: jest.fn(),
30
+ onRingcentralOAuthCallback: jest.fn(),
31
+ }));
32
+ jest.mock('../../handlers/admin', () => ({
33
+ validateRcUserToken: jest.fn(),
34
+ validateAdminRole: jest.fn(),
35
+ upsertAdminSettings: jest.fn(),
36
+ getAdminSettings: jest.fn(),
37
+ getUserMapping: jest.fn(),
38
+ reinitializeUserMapping: jest.fn(),
39
+ getServerLoggingSettings: jest.fn(),
40
+ updateServerLoggingSettings: jest.fn(),
41
+ getAdminReport: jest.fn(),
42
+ getUserReport: jest.fn(),
43
+ }));
44
+ jest.mock('../../handlers/user', () => ({
45
+ getUserSettingsByAdmin: jest.fn(),
46
+ refreshUserInfo: jest.fn(),
47
+ getUserSettings: jest.fn(),
48
+ updateUserSettings: jest.fn(),
49
+ }));
50
+ jest.mock('../../handlers/disposition', () => ({
51
+ upsertCallDisposition: jest.fn(),
52
+ }));
53
+ jest.mock('../../handlers/calldown', () => ({
54
+ schedule: jest.fn(),
55
+ list: jest.fn(),
56
+ remove: jest.fn(),
57
+ update: jest.fn(),
58
+ }));
59
+ jest.mock('../../handlers/plugin', () => ({
60
+ registerPluginAccount: jest.fn(),
61
+ unregisterPluginAccount: jest.fn(),
62
+ getPluginLicenseStatus: jest.fn(),
63
+ }));
64
+ jest.mock('../../handlers/managedAuth', () => ({
65
+ getManagedAuthState: jest.fn(),
66
+ getManagedAuthAdminSettings: jest.fn(),
67
+ upsertUserManagedAuthValues: jest.fn(),
68
+ upsertOrgManagedAuthValues: jest.fn(),
69
+ }));
70
+ jest.mock('../../handlers/managedOAuth', () => ({
71
+ getManagedOAuthState: jest.fn(),
72
+ upsertPendingManagedOAuth: jest.fn(),
73
+ clearPendingManagedOAuth: jest.fn(),
74
+ resetManagedOAuth: jest.fn(),
75
+ }));
76
+ jest.mock('../../connector/mock', () => ({
77
+ createUser: jest.fn(),
78
+ deleteUser: jest.fn(),
79
+ getCallLog: jest.fn(),
80
+ createCallLog: jest.fn(),
81
+ cleanUpMockLogs: jest.fn(),
82
+ }));
83
+ jest.mock('../../connector/registry', () => ({
84
+ getManifest: jest.fn(),
85
+ getReleaseNotes: jest.fn(),
86
+ getConnector: jest.fn(),
87
+ }));
88
+ jest.mock('../../lib/analytics', () => ({
89
+ init: jest.fn(),
90
+ track: jest.fn(),
91
+ }));
92
+ jest.mock('../../lib/jwt', () => ({
93
+ decodeJwt: jest.fn(),
94
+ generateJwt: jest.fn(),
95
+ }));
96
+ jest.mock('../../lib/util', () => ({
97
+ getHashValue: jest.fn((value) => `hash-${value}`),
98
+ }));
99
+ jest.mock('../../lib/s3ErrorLogReport', () => ({
100
+ getUploadUrl: jest.fn(),
101
+ }));
102
+ jest.mock('../../lib/authSession', () => ({
103
+ updateAuthSession: jest.fn(),
104
+ }));
105
+ jest.mock('../../mcp/mcpHandler', () => ({
106
+ handleMcpRequest: jest.fn((req, res) => res.status(200).json({ jsonrpc: '2.0', result: 'mcp-ok' })),
107
+ handleWidgetToolCall: jest.fn((req, res) => res.status(200).json({ successful: true })),
108
+ }));
109
+ jest.mock('../../models/userModel', () => ({
110
+ UserModel: {
111
+ findByPk: jest.fn(),
112
+ },
113
+ }));
114
+
115
+ const logCore = require('../../handlers/log');
116
+ const contactCore = require('../../handlers/contact');
117
+ const appointmentCore = require('../../handlers/appointment');
118
+ const authCore = require('../../handlers/auth');
119
+ const adminCore = require('../../handlers/admin');
120
+ const userCore = require('../../handlers/user');
121
+ const dispositionCore = require('../../handlers/disposition');
122
+ const calldown = require('../../handlers/calldown');
123
+ const pluginCore = require('../../handlers/plugin');
124
+ const managedAuthCore = require('../../handlers/managedAuth');
125
+ const managedOAuthCore = require('../../handlers/managedOAuth');
126
+ const mockConnector = require('../../connector/mock');
127
+ const connectorRegistry = require('../../connector/registry');
128
+ const analytics = require('../../lib/analytics');
129
+ const jwt = require('../../lib/jwt');
130
+ const s3ErrorLogReport = require('../../lib/s3ErrorLogReport');
131
+ const { updateAuthSession } = require('../../lib/authSession');
132
+ const mcpHandler = require('../../mcp/mcpHandler');
133
+ const { UserModel } = require('../../models/userModel');
134
+ const {
135
+ createCoreRouter,
136
+ createCoreApp,
137
+ createCoreMiddleware,
138
+ initializeCore,
139
+ } = require('../../index');
140
+
141
+ describe('Core router broad route coverage', () => {
142
+ const decodedJwt = {
143
+ id: 'user-1',
144
+ platform: 'testCRM',
145
+ exp: Math.floor(Date.now() / 1000) + 60 * 60,
146
+ };
147
+ const mockUser = {
148
+ id: 'user-1',
149
+ platform: 'testCRM',
150
+ hostname: 'crm.example.com',
151
+ rcAccountId: 'rc-account-1',
152
+ userSettings: {},
153
+ };
154
+ let app;
155
+
156
+ function authQuery() {
157
+ return { jwtToken: 'valid-crm-jwt' };
158
+ }
159
+
160
+ beforeEach(() => {
161
+ jest.clearAllMocks();
162
+ process.env.HASH_KEY = 'hash-key';
163
+ process.env.APP_SERVER = 'https://app.example.com';
164
+ process.env.RINGCENTRAL_SERVER = 'https://platform.example.com';
165
+ process.env.RINGCENTRAL_CLIENT_ID = 'rc-client-id';
166
+ process.env.RINGCENTRAL_CLIENT_SECRET = 'rc-client-secret';
167
+ process.env.CHATGPT_VERIFICATION_CODE = 'verify-code';
168
+ process.env.APP_SERVER_SECRET_KEY = 'secret-key';
169
+ process.env.IS_PROD = 'false';
170
+
171
+ jwt.decodeJwt.mockReturnValue(decodedJwt);
172
+ jwt.generateJwt.mockReturnValue('generated-crm-jwt');
173
+ UserModel.findByPk.mockResolvedValue(mockUser);
174
+ connectorRegistry.getReleaseNotes.mockReturnValue({
175
+ '1.0.0': { testCRM: { notes: ['connector note'] } },
176
+ });
177
+ connectorRegistry.getManifest.mockReturnValue({
178
+ author: { name: 'Test Author' },
179
+ version: '1.0.0',
180
+ platforms: {
181
+ testCRM: {
182
+ serverSideLogging: {
183
+ url: 'https://logging.example.com',
184
+ },
185
+ },
186
+ },
187
+ });
188
+ connectorRegistry.getConnector.mockReturnValue({
189
+ getAuthType: jest.fn(() => 'oauth'),
190
+ getOauthInfo: jest.fn(),
191
+ getUserInfo: jest.fn(),
192
+ createCallLog: jest.fn(),
193
+ updateCallLog: jest.fn(),
194
+ getCallLog: jest.fn(),
195
+ createMessageLog: jest.fn(),
196
+ updateMessageLog: jest.fn(),
197
+ createContact: jest.fn(),
198
+ findContact: jest.fn(),
199
+ listAppointments: jest.fn(),
200
+ createAppointment: jest.fn(),
201
+ updateAppointment: jest.fn(),
202
+ refreshAppointment: jest.fn(),
203
+ confirmAppointment: jest.fn(),
204
+ cancelAppointment: jest.fn(),
205
+ unAuthorize: jest.fn().mockResolvedValue({
206
+ returnMessage: {
207
+ messageType: 'success',
208
+ message: 'Disconnected',
209
+ },
210
+ }),
211
+ upsertCallDisposition: jest.fn(),
212
+ findContactWithName: jest.fn(),
213
+ getUserList: jest.fn(),
214
+ getLicenseStatus: jest.fn(),
215
+ getLogFormatType: jest.fn(),
216
+ refreshUserInfo: jest.fn(),
217
+ });
218
+
219
+ adminCore.validateRcUserToken.mockResolvedValue({
220
+ rcAccountId: 'rc-account-1',
221
+ rcExtensionId: 'rc-extension-1',
222
+ });
223
+ adminCore.validateAdminRole.mockResolvedValue({
224
+ isValidated: true,
225
+ rcAccountId: 'rc-account-1',
226
+ });
227
+ adminCore.upsertAdminSettings.mockResolvedValue();
228
+ adminCore.getAdminSettings.mockResolvedValue({ userSettings: { theme: 'dark' } });
229
+ adminCore.getUserMapping.mockResolvedValue({ users: ['mapped-user'] });
230
+ adminCore.reinitializeUserMapping.mockResolvedValue({ users: ['remapped-user'] });
231
+ adminCore.getServerLoggingSettings.mockResolvedValue({ enabled: true });
232
+ adminCore.updateServerLoggingSettings.mockResolvedValue({
233
+ successful: true,
234
+ returnMessage: { messageType: 'success', message: 'Updated' },
235
+ });
236
+ adminCore.getAdminReport.mockResolvedValue({ rows: [{ id: 'admin-row' }] });
237
+ adminCore.getUserReport.mockResolvedValue({ rows: [{ id: 'user-row' }] });
238
+
239
+ managedAuthCore.getManagedAuthState.mockResolvedValue({ hasManagedAuth: true });
240
+ managedAuthCore.getManagedAuthAdminSettings.mockResolvedValue({ shared: true });
241
+ managedAuthCore.upsertUserManagedAuthValues.mockResolvedValue();
242
+ managedAuthCore.upsertOrgManagedAuthValues.mockResolvedValue();
243
+ managedOAuthCore.getManagedOAuthState.mockResolvedValue({ isConfigured: true });
244
+ managedOAuthCore.upsertPendingManagedOAuth.mockResolvedValue();
245
+ managedOAuthCore.clearPendingManagedOAuth.mockResolvedValue();
246
+ managedOAuthCore.resetManagedOAuth.mockResolvedValue();
247
+
248
+ authCore.getLicenseStatus.mockResolvedValue({ isLicenseValid: true });
249
+ authCore.authValidation.mockResolvedValue({
250
+ successful: true,
251
+ returnMessage: { messageType: 'success', message: 'Valid' },
252
+ failReason: '',
253
+ status: 200,
254
+ });
255
+ authCore.onOAuthCallback.mockResolvedValue({
256
+ userInfo: { id: 'user-1', name: 'CRM User' },
257
+ returnMessage: { messageType: 'success', message: 'Connected' },
258
+ });
259
+ authCore.onApiKeyLogin.mockResolvedValue({
260
+ userInfo: { id: 'user-1', name: 'CRM User' },
261
+ returnMessage: { messageType: 'success', message: 'Connected' },
262
+ });
263
+ authCore.onRingcentralOAuthCallback.mockResolvedValue();
264
+
265
+ userCore.getUserSettingsByAdmin.mockResolvedValue({ fields: [] });
266
+ userCore.refreshUserInfo.mockResolvedValue({
267
+ successful: true,
268
+ returnMessage: { messageType: 'success', message: 'Refreshed' },
269
+ });
270
+ userCore.getUserSettings.mockResolvedValue({ timezone: 'UTC' });
271
+ userCore.updateUserSettings.mockResolvedValue({ userSettings: { timezone: 'UTC' } });
272
+
273
+ contactCore.findContact.mockResolvedValue({
274
+ successful: true,
275
+ returnMessage: { messageType: 'success', message: 'Found' },
276
+ contact: [{ id: 'contact-1', isNewContact: false }],
277
+ extraDataTracking: { source: 'contact' },
278
+ });
279
+ contactCore.createContact.mockResolvedValue({
280
+ successful: true,
281
+ returnMessage: { messageType: 'success', message: 'Created' },
282
+ contact: { id: 'contact-2' },
283
+ extraDataTracking: { source: 'create-contact' },
284
+ });
285
+ contactCore.findContactWithName.mockResolvedValue({
286
+ successful: true,
287
+ returnMessage: { messageType: 'success', message: 'Found' },
288
+ contact: [{ id: 'contact-3' }],
289
+ });
290
+
291
+ appointmentCore.listAppointments.mockResolvedValue({
292
+ successful: true,
293
+ appointments: [{ id: 'appt-1' }],
294
+ extraDataTracking: { source: 'appointments' },
295
+ });
296
+ appointmentCore.createAppointment.mockResolvedValue({
297
+ successful: true,
298
+ appointmentId: 'appt-2',
299
+ appointment: { id: 'appt-2' },
300
+ returnMessage: { messageType: 'success', message: 'Created' },
301
+ extraDataTracking: { source: 'create-appointment' },
302
+ });
303
+ appointmentCore.updateAppointment.mockResolvedValue({
304
+ successful: true,
305
+ appointment: { id: 'appt-2' },
306
+ returnMessage: { messageType: 'success', message: 'Updated' },
307
+ });
308
+ appointmentCore.refreshAppointment.mockResolvedValue({
309
+ successful: true,
310
+ appointment: { id: 'appt-2' },
311
+ returnMessage: { messageType: 'success', message: 'Refreshed' },
312
+ });
313
+ appointmentCore.confirmAppointment.mockResolvedValue({
314
+ successful: true,
315
+ appointment: { id: 'appt-2' },
316
+ returnMessage: { messageType: 'success', message: 'Confirmed' },
317
+ });
318
+ appointmentCore.cancelAppointment.mockResolvedValue({
319
+ successful: true,
320
+ appointment: { id: 'appt-2' },
321
+ returnMessage: { messageType: 'success', message: 'Cancelled' },
322
+ });
323
+
324
+ logCore.saveNoteCache.mockResolvedValue({
325
+ successful: true,
326
+ returnMessage: { messageType: 'success', message: 'Cached' },
327
+ extraDataTracking: { cache: true },
328
+ });
329
+ logCore.getCallLog.mockResolvedValue({
330
+ successful: true,
331
+ logs: [{ sessionId: 'session-1' }],
332
+ returnMessage: { messageType: 'success', message: 'Found' },
333
+ extraDataTracking: { logs: 1 },
334
+ });
335
+ logCore.createCallLog.mockResolvedValue({
336
+ successful: true,
337
+ logId: 'log-1',
338
+ returnMessage: { messageType: 'success', message: 'Logged' },
339
+ extraDataTracking: { created: true },
340
+ });
341
+ logCore.updateCallLog.mockResolvedValue({
342
+ successful: true,
343
+ logId: 'log-1',
344
+ updatedNote: 'updated',
345
+ returnMessage: { messageType: 'success', message: 'Updated' },
346
+ extraDataTracking: { updated: true },
347
+ });
348
+ logCore.createMessageLog.mockResolvedValue({
349
+ successful: true,
350
+ returnMessage: { messageType: 'success', message: 'Message logged' },
351
+ logIds: ['msg-1'],
352
+ extraDataTracking: { messages: 1 },
353
+ });
354
+ logCore.handleAsyncPluginCallback.mockResolvedValue({
355
+ statusCode: 202,
356
+ body: { successful: true },
357
+ });
358
+
359
+ dispositionCore.upsertCallDisposition.mockResolvedValue({
360
+ successful: true,
361
+ returnMessage: { messageType: 'success', message: 'Disposition saved' },
362
+ extraDataTracking: { disposition: true },
363
+ });
364
+ calldown.schedule.mockResolvedValue({ id: 'calldown-1' });
365
+ calldown.list.mockResolvedValue({ items: [{ id: 'calldown-1' }] });
366
+ calldown.remove.mockResolvedValue();
367
+ calldown.update.mockResolvedValue();
368
+ pluginCore.registerPluginAccount.mockResolvedValue({ successful: true });
369
+ pluginCore.unregisterPluginAccount.mockResolvedValue({ successful: true });
370
+ pluginCore.getPluginLicenseStatus.mockResolvedValue({ licenseStatus: true });
371
+ mockConnector.createUser.mockResolvedValue({ id: 'mockUser' });
372
+ mockConnector.deleteUser.mockResolvedValue(true);
373
+ mockConnector.getCallLog.mockResolvedValue([{ sessionId: 'session-1', matched: true }]);
374
+ mockConnector.createCallLog.mockResolvedValue();
375
+ mockConnector.cleanUpMockLogs.mockResolvedValue();
376
+ s3ErrorLogReport.getUploadUrl.mockResolvedValue('https://upload.example.com/report');
377
+
378
+ app = express();
379
+ app.use(express.json());
380
+ app.use('/', createCoreRouter());
381
+ });
382
+
383
+ afterEach(() => {
384
+ delete process.env.IS_PROD;
385
+ });
386
+
387
+ test('serves manifest, release, implementation, metadata, mock, and MCP routes', async () => {
388
+ await expect(request(app).get('/isAlive')).resolves.toMatchObject({ status: 200, text: 'OK' });
389
+ expect((await request(app).get('/releaseNotes')).status).toBe(200);
390
+ const manifestResponse = await request(app)
391
+ .get('/crmManifest')
392
+ .query({ platformName: 'testCRM' });
393
+ expect(manifestResponse.status).toBe(200);
394
+ expect(manifestResponse.body.author.name).toBe('Test Author');
395
+ expect((await request(app).get('/serverVersionInfo')).body).toEqual({ version: '1.0.0' });
396
+
397
+ const interfacesResponse = await request(app)
398
+ .get('/implementedInterfaces')
399
+ .query({ platform: 'testCRM' });
400
+ expect(interfacesResponse.status).toBe(200);
401
+ expect(interfacesResponse.body.createCallLog).toBe(true);
402
+
403
+ await expect(request(app).get('/.well-known/openai-apps-challenge')).resolves.toMatchObject({ text: 'verify-code' });
404
+ expect((await request(app).get('/.well-known/oauth-protected-resource')).body.resource).toBe('https://app.example.com');
405
+ expect((await request(app).get('/.well-known/oauth-authorization-server')).body.registration_endpoint).toBe('https://app.example.com/oauth/register');
406
+ expect((await request(app).post('/oauth/register')).body).toEqual({
407
+ client_id: 'rc-client-id',
408
+ client_secret: 'rc-client-secret',
409
+ });
410
+ const redirectResponse = await request(app)
411
+ .get('/oauth/authorize_shim')
412
+ .query({
413
+ response_type: 'code',
414
+ client_id: 'client-id',
415
+ redirect_uri: 'https://chat.example.com/callback',
416
+ state: 'state-1',
417
+ scope: 'ReadAccounts',
418
+ });
419
+ expect(redirectResponse.status).toBe(302);
420
+ expect(redirectResponse.headers.location).toContain('/restapi/oauth/authorize?');
421
+
422
+ await expect(request(app).post('/registerMockUser').query({ secretKey: 'secret-key' }).send({ userName: 'A' })).resolves.toMatchObject({ status: 200 });
423
+ await expect(request(app).delete('/deleteMockUser').query({ secretKey: 'secret-key', userName: 'A' })).resolves.toMatchObject({ status: 200 });
424
+ await expect(request(app).get('/mockCallLog').query({ secretKey: 'secret-key', sessionIds: 's1' })).resolves.toMatchObject({ status: 200 });
425
+ await expect(request(app).post('/mockCallLog').query({ secretKey: 'secret-key' }).send({ sessionId: 's1' })).resolves.toMatchObject({ status: 200 });
426
+ await expect(request(app).delete('/mockCallLog').query({ secretKey: 'secret-key' })).resolves.toMatchObject({ status: 200 });
427
+
428
+ expect((await request(app).options('/mcp')).status).toBe(200);
429
+ expect((await request(app).post('/mcp').send({ method: 'tools/list' })).body).toEqual({ jsonrpc: '2.0', result: 'mcp-ok' });
430
+ expect(mcpHandler.handleMcpRequest).toHaveBeenCalled();
431
+ expect((await request(app).options('/mcp/widget-tool-call')).status).toBe(200);
432
+ expect((await request(app).post('/mcp/widget-tool-call').send({ name: 'tool' })).body).toEqual({ successful: true });
433
+ });
434
+
435
+ test('covers auth, admin, managed auth, managed OAuth, and user routes', async () => {
436
+ expect((await request(app).get('/licenseStatus').query(authQuery())).body).toEqual({ isLicenseValid: true });
437
+ expect((await request(app).get('/authValidation').query(authQuery())).body).toEqual({
438
+ successful: true,
439
+ returnMessage: { messageType: 'success', message: 'Valid' },
440
+ });
441
+ expect((await request(app).get('/apiKeyManagedAuthState').query({ platform: 'testCRM', rcAccessToken: 'rc-token' })).body).toEqual({ hasManagedAuth: true });
442
+ expect((await request(app).get('/oauthManagedAuthState').query({ platform: 'testCRM', rcAccessToken: 'rc-token' })).body).toEqual({ isConfigured: true });
443
+
444
+ await expect(request(app).post('/admin/settings').query({ rcAccessToken: 'rc-token' }).send({ adminSettings: { a: 1 } })).resolves.toMatchObject({ status: 200 });
445
+ expect((await request(app).get('/admin/settings').query({ ...authQuery(), rcAccessToken: 'rc-token' })).body).toEqual({ userSettings: { theme: 'dark' } });
446
+ expect((await request(app).get('/admin/managedAuth').query({ ...authQuery(), rcAccessToken: 'rc-token', connectorId: 'connector-1' })).body).toEqual({ shared: true });
447
+ await expect(request(app).post('/admin/managedAuth').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ scope: 'user', rcExtensionId: 'ext-1', values: { key: 'value' } })).resolves.toMatchObject({ status: 200 });
448
+ await expect(request(app).post('/admin/managedAuth').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ scope: 'org', values: { key: 'value' } })).resolves.toMatchObject({ status: 200 });
449
+ await expect(request(app).post('/admin/managedOAuth/cache').query({ rcAccessToken: 'rc-token' }).send({ values: { clientId: 'id' } })).resolves.toMatchObject({ status: 200 });
450
+ await expect(request(app).delete('/admin/managedOAuth/cache').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 200 });
451
+ await expect(request(app).delete('/admin/managedOAuth/account').query({ rcAccessToken: 'rc-token', platform: 'testCRM' })).resolves.toMatchObject({ status: 200 });
452
+ expect((await request(app).post('/admin/userMapping').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ rcExtensionList: ['100'] })).body).toEqual({ users: ['mapped-user'] });
453
+ expect((await request(app).post('/admin/reinitializeUserMapping').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ rcExtensionList: ['100'] })).body).toEqual({ users: ['remapped-user'] });
454
+ expect((await request(app).get('/admin/serverLoggingSettings').query(authQuery())).body).toEqual({ enabled: true });
455
+ expect((await request(app).post('/admin/serverLoggingSettings').query(authQuery()).send({ additionalFieldValues: { enabled: true } })).body).toEqual({
456
+ successful: true,
457
+ returnMessage: { messageType: 'success', message: 'Updated' },
458
+ });
459
+
460
+ expect((await request(app).get('/user/preloadSettings').query({ rcAccessToken: 'rc-token' })).body).toEqual({ fields: [] });
461
+ expect((await request(app).post('/user/refreshInfo').query(authQuery()).send({})).body).toEqual({
462
+ successful: true,
463
+ returnMessage: { messageType: 'success', message: 'Refreshed' },
464
+ });
465
+ expect((await request(app).get('/user/settings').query({ ...authQuery(), rcAccessToken: 'rc-token' })).body).toEqual({ timezone: 'UTC' });
466
+ expect((await request(app).post('/user/settings').query(authQuery()).send({ userSettings: { timezone: 'UTC' } })).body).toEqual({ userSettings: { timezone: 'UTC' } });
467
+ await expect(request(app).get('/hostname').query(authQuery())).resolves.toMatchObject({ status: 200, text: 'crm.example.com' });
468
+ expect((await request(app).get('/userInfoHash').query({ extensionId: 'ext', accountId: 'acc' })).body).toEqual({
469
+ extensionId: 'hash-ext',
470
+ accountId: 'hash-acc',
471
+ });
472
+ });
473
+
474
+ test('covers login, contact, appointment, log, report, and plugin routes', async () => {
475
+ const callbackState = encodeURIComponent('platform=testCRM&hostname=crm.example.com');
476
+ const callbackResponse = await request(app)
477
+ .get('/oauth-callback')
478
+ .query({
479
+ callbackUri: `https://redirect.example.com/callback?state=${callbackState}`,
480
+ code: 'oauth-code',
481
+ });
482
+ expect(callbackResponse.body).toEqual({
483
+ jwtToken: 'generated-crm-jwt',
484
+ name: 'CRM User',
485
+ returnMessage: { messageType: 'success', message: 'Connected' },
486
+ });
487
+
488
+ const mcpState = encodeURIComponent('platform=testCRM&hostname=crm.example.com&sessionId=session-1');
489
+ await expect(request(app).get('/oauth-callback').query({
490
+ callbackUri: `https://redirect.example.com/callback?state=${mcpState}`,
491
+ code: 'oauth-code',
492
+ })).resolves.toMatchObject({ status: 200, text: 'Authentication successful. Please go back to AI Agent and confirm it.' });
493
+ expect(updateAuthSession).toHaveBeenCalledWith('session-1', expect.objectContaining({ status: 'completed' }));
494
+
495
+ expect((await request(app).post('/apiKeyLogin').send({ platform: 'testCRM', apiKey: 'api-key', rcAccessToken: 'rc-token' })).body).toEqual({
496
+ jwtToken: 'generated-crm-jwt',
497
+ name: 'CRM User',
498
+ returnMessage: { messageType: 'success', message: 'Connected' },
499
+ });
500
+ expect((await request(app).post('/unAuthorize').query(authQuery()).send({})).body).toEqual({
501
+ messageType: 'success',
502
+ message: 'Disconnected',
503
+ });
504
+
505
+ expect((await request(app).get('/contact').query({ ...authQuery(), phoneNumber: '+15551234567' })).body).toEqual({
506
+ successful: true,
507
+ returnMessage: { messageType: 'success', message: 'Found' },
508
+ contact: [{ id: 'contact-1', isNewContact: false }],
509
+ });
510
+ expect((await request(app).post('/contact').query(authQuery()).send({ phoneNumber: '+1555', newContactName: 'Alice' })).body.contact).toEqual({ id: 'contact-2' });
511
+ expect((await request(app).get('/custom/contact/search').query({ ...authQuery(), name: 'Alice' })).body.contact).toEqual([{ id: 'contact-3' }]);
512
+
513
+ expect((await request(app).get('/appointments').query(authQuery())).body.appointments).toEqual([{ id: 'appt-1' }]);
514
+ expect((await request(app).post('/appointments').query(authQuery()).send({ payload: { title: 'Meet' } })).body.appointmentId).toBe('appt-2');
515
+ expect((await request(app).patch('/appointments/appt-2').query(authQuery()).send({ patch: { title: 'Updated' } })).body.appointmentId).toBe('appt-2');
516
+ expect((await request(app).get('/appointments/appt-2/refresh').query(authQuery())).body.appointmentId).toBe('appt-2');
517
+ expect((await request(app).post('/appointments/appt-2/confirm').query(authQuery())).body.appointmentId).toBe('appt-2');
518
+ expect((await request(app).post('/appointments/appt-2/cancel').query(authQuery())).body.appointmentId).toBe('appt-2');
519
+
520
+ expect((await request(app).post('/callLog/cacheNote').query(authQuery()).send({ sessionId: 's1', note: 'note' })).body.successful).toBe(true);
521
+ expect((await request(app).get('/callLog').query({ ...authQuery(), sessionIds: 's1', requireDetails: 'true' })).body.logs).toEqual([{ sessionId: 'session-1' }]);
522
+ expect((await request(app).post('/callLog').query(authQuery()).send({ logInfo: { accountId: 'acc' } })).body.logId).toBe('log-1');
523
+ expect((await request(app).patch('/callLog').query(authQuery()).send({ accountId: 'acc' })).body.updatedNote).toBe('updated');
524
+ expect((await request(app).put('/callDisposition').query(authQuery()).send({ sessionId: 's1', dispositions: ['left voicemail'] })).body.successful).toBe(true);
525
+ expect((await request(app).post('/messageLog').query(authQuery()).send({ messages: [] })).body.logIds).toEqual(['msg-1']);
526
+
527
+ expect((await request(app).get('/ringcentral/admin/report').query({ ...authQuery(), timezone: 'UTC' })).body).toEqual({ rows: [{ id: 'admin-row' }] });
528
+ expect((await request(app).get('/ringcentral/admin/userReport').query({ ...authQuery(), rcExtensionId: 'ext-1' })).body).toEqual({ rows: [{ id: 'user-row' }] });
529
+ await expect(request(app).get('/ringcentral/oauth/callback').query({ ...authQuery(), code: 'rc-code' })).resolves.toMatchObject({ status: 200 });
530
+ expect((await request(app).get('/debug/report/url').query(authQuery())).body).toEqual({ presignedUrl: 'https://upload.example.com/report' });
531
+ expect((await request(app).post('/plugin/async-callback/task-1').send({ successful: true })).body).toEqual({ successful: true });
532
+ await expect(request(app).post('/plugin/register').query({ rcAccessToken: 'rc-token' }).send({ pluginId: 'p1', rcAccountId: 'rc-account-1' })).resolves.toMatchObject({ status: 200 });
533
+ await expect(request(app).delete('/plugin/unregister').query({ rcAccessToken: 'rc-token', pluginId: 'p1', rcAccountId: 'rc-account-1' })).resolves.toMatchObject({ status: 200 });
534
+ expect((await request(app).get('/plugin/licenseStatus').query({ ...authQuery(), rcAccountId: 'rc-account-1', pluginId: 'p1' })).body).toEqual({ licenseStatus: true });
535
+ });
536
+
537
+ test('covers no-token and missing-parameter validation branches', async () => {
538
+ const noTokenCases = [
539
+ ['get', '/authValidation'],
540
+ ['get', '/admin/settings'],
541
+ ['get', '/admin/managedAuth'],
542
+ ['post', '/admin/managedAuth', { scope: 'org' }],
543
+ ['post', '/admin/userMapping', { rcExtensionList: ['100'] }],
544
+ ['post', '/admin/reinitializeUserMapping', { rcExtensionList: ['100'] }],
545
+ ['get', '/admin/serverLoggingSettings'],
546
+ ['post', '/admin/serverLoggingSettings', { additionalFieldValues: { enabled: true } }],
547
+ ['post', '/user/refreshInfo', {}],
548
+ ['get', '/user/settings'],
549
+ ['post', '/user/settings', { userSettings: {} }],
550
+ ['get', '/hostname'],
551
+ ['post', '/unAuthorize', {}],
552
+ ['get', '/contact'],
553
+ ['post', '/contact', { phoneNumber: '+1555', newContactName: 'Alice' }],
554
+ ['get', '/appointments'],
555
+ ['post', '/appointments', { payload: { title: 'Meet' } }],
556
+ ['patch', '/appointments/appt-2', { patch: { title: 'Meet' } }],
557
+ ['get', '/appointments/appt-2/refresh'],
558
+ ['post', '/appointments/appt-2/confirm', {}],
559
+ ['post', '/appointments/appt-2/cancel', {}],
560
+ ['get', '/callLog'],
561
+ ['post', '/callLog', { logInfo: { accountId: 'acc' } }],
562
+ ['patch', '/callLog', { accountId: 'acc' }],
563
+ ['put', '/callDisposition', { sessionId: 's1' }],
564
+ ['post', '/messageLog', { messages: [] }],
565
+ ['post', '/calldown', { contactId: 'c1' }],
566
+ ['get', '/calldown'],
567
+ ['delete', '/calldown/item-1'],
568
+ ['patch', '/calldown/item-1', { status: 'called' }],
569
+ ['get', '/custom/contact/search'],
570
+ ['get', '/ringcentral/admin/report'],
571
+ ['get', '/ringcentral/admin/userReport'],
572
+ ['get', '/ringcentral/oauth/callback'],
573
+ ['get', '/debug/report/url'],
574
+ ];
575
+
576
+ for (const [method, path, body] of noTokenCases) {
577
+ const req = request(app)[method](path);
578
+ const response = body === undefined ? await req : await req.send(body);
579
+ expect(response.status).toBe(400);
580
+ }
581
+
582
+ await expect(request(app).get('/implementedInterfaces')).resolves.toMatchObject({ status: 400 });
583
+ await expect(request(app).get('/apiKeyManagedAuthState').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
584
+ await expect(request(app).get('/apiKeyManagedAuthState').query({ platform: 'testCRM' })).resolves.toMatchObject({ status: 400 });
585
+ await expect(request(app).get('/oauthManagedAuthState').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
586
+ await expect(request(app).get('/oauthManagedAuthState').query({ platform: 'testCRM' })).resolves.toMatchObject({ status: 400 });
587
+ await expect(request(app).post('/admin/serverLoggingSettings').query(authQuery()).send({})).resolves.toMatchObject({ status: 400 });
588
+ await expect(request(app).get('/user/preloadSettings')).resolves.toMatchObject({ status: 400 });
589
+ await expect(request(app).get('/oauth-callback')).resolves.toMatchObject({ status: 400 });
590
+ await expect(request(app).get('/oauth-callback').query({ callbackUri: 'https://redirect.example.com/callback' })).resolves.toMatchObject({ status: 400 });
591
+ await expect(request(app).post('/apiKeyLogin').send({ apiKey: 'api-key' })).resolves.toMatchObject({ status: 400 });
592
+ await expect(request(app).delete('/admin/managedOAuth/account').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
593
+ });
594
+
595
+ test('covers invalid JWT and revoke-session branches', async () => {
596
+ async function expectInvalidJwt(requestPromise, expectedText = null) {
597
+ jwt.decodeJwt.mockReturnValue(null);
598
+ const response = await requestPromise();
599
+ expect(response.status).toBe(400);
600
+ if (expectedText) {
601
+ expect(response.text).toContain(expectedText);
602
+ }
603
+ jwt.decodeJwt.mockReturnValue(decodedJwt);
604
+ }
605
+
606
+ await expectInvalidJwt(() => request(app).get('/licenseStatus').query({ jwtToken: 'bad' }), 'Invalid JWT token');
607
+ await expectInvalidJwt(() => request(app).get('/authValidation').query({ jwtToken: 'bad' }));
608
+ await expectInvalidJwt(() => request(app).get('/contact').query({ jwtToken: 'bad', phoneNumber: '+1555' }));
609
+ await expectInvalidJwt(() => request(app).post('/contact').query({ jwtToken: 'bad' }).send({ phoneNumber: '+1555' }));
610
+ await expectInvalidJwt(() => request(app).get('/appointments').query({ jwtToken: 'bad' }));
611
+ await expectInvalidJwt(() => request(app).post('/appointments').query({ jwtToken: 'bad' }).send({ payload: {} }));
612
+ await expectInvalidJwt(() => request(app).patch('/appointments/appt-2').query({ jwtToken: 'bad' }).send({ patch: {} }));
613
+ await expectInvalidJwt(() => request(app).get('/appointments/appt-2/refresh').query({ jwtToken: 'bad' }));
614
+ await expectInvalidJwt(() => request(app).post('/appointments/appt-2/confirm').query({ jwtToken: 'bad' }));
615
+ await expectInvalidJwt(() => request(app).post('/appointments/appt-2/cancel').query({ jwtToken: 'bad' }));
616
+ await expectInvalidJwt(() => request(app).get('/callLog').query({ jwtToken: 'bad' }));
617
+ await expectInvalidJwt(() => request(app).post('/callLog').query({ jwtToken: 'bad' }).send({ logInfo: {} }));
618
+ await expectInvalidJwt(() => request(app).patch('/callLog').query({ jwtToken: 'bad' }).send({}));
619
+ await expectInvalidJwt(() => request(app).put('/callDisposition').query({ jwtToken: 'bad' }).send({ sessionId: 's1' }), 'Invalid JWT token');
620
+ await expectInvalidJwt(() => request(app).post('/messageLog').query({ jwtToken: 'bad' }).send({ messages: [] }));
621
+ await expectInvalidJwt(() => request(app).get('/custom/contact/search').query({ jwtToken: 'bad', name: 'Alice' }), 'Invalid JWT token');
622
+
623
+ contactCore.findContact.mockResolvedValueOnce({
624
+ successful: false,
625
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
626
+ isRevokeUserSession: true,
627
+ });
628
+ expect((await request(app).get('/contact').query({ ...authQuery(), phoneNumber: '+1555' })).status).toBe(401);
629
+
630
+ contactCore.createContact.mockResolvedValueOnce({
631
+ successful: false,
632
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
633
+ isRevokeUserSession: true,
634
+ });
635
+ expect((await request(app).post('/contact').query(authQuery()).send({ phoneNumber: '+1555' })).status).toBe(401);
636
+
637
+ for (const [method, path, mockFn, body] of [
638
+ ['get', '/appointments', appointmentCore.listAppointments],
639
+ ['post', '/appointments', appointmentCore.createAppointment, { payload: {} }],
640
+ ['patch', '/appointments/appt-2', appointmentCore.updateAppointment, { patch: {} }],
641
+ ['get', '/appointments/appt-2/refresh', appointmentCore.refreshAppointment],
642
+ ['post', '/appointments/appt-2/confirm', appointmentCore.confirmAppointment, {}],
643
+ ['post', '/appointments/appt-2/cancel', appointmentCore.cancelAppointment, {}],
644
+ ]) {
645
+ mockFn.mockResolvedValueOnce({
646
+ successful: false,
647
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
648
+ isRevokeUserSession: true,
649
+ });
650
+ const req = request(app)[method](path).query(authQuery());
651
+ const response = body === undefined ? await req : await req.send(body);
652
+ expect(response.status).toBe(401);
653
+ }
654
+
655
+ logCore.getCallLog.mockResolvedValueOnce({
656
+ successful: false,
657
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
658
+ isRevokeUserSession: true,
659
+ });
660
+ expect((await request(app).get('/callLog').query(authQuery())).status).toBe(401);
661
+
662
+ logCore.createCallLog.mockResolvedValueOnce({
663
+ successful: false,
664
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
665
+ isRevokeUserSession: true,
666
+ });
667
+ expect((await request(app).post('/callLog').query(authQuery()).send({ logInfo: { accountId: 'acc' } })).status).toBe(401);
668
+
669
+ dispositionCore.upsertCallDisposition.mockResolvedValueOnce({
670
+ successful: false,
671
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
672
+ isRevokeUserSession: true,
673
+ });
674
+ expect((await request(app).put('/callDisposition').query(authQuery()).send({ sessionId: 's1' })).status).toBe(401);
675
+
676
+ logCore.createMessageLog.mockResolvedValueOnce({
677
+ successful: false,
678
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
679
+ isRevokeUserSession: true,
680
+ });
681
+ expect((await request(app).post('/messageLog').query(authQuery()).send({ messages: [] })).status).toBe(401);
682
+
683
+ contactCore.findContactWithName.mockResolvedValueOnce({
684
+ successful: false,
685
+ returnMessage: { messageType: 'warning', message: 'Reconnect' },
686
+ isRevokeUserSession: true,
687
+ });
688
+ expect((await request(app).get('/custom/contact/search').query({ ...authQuery(), name: 'Alice' })).status).toBe(401);
689
+ });
690
+
691
+ test('covers route catch branches for mocked handler failures', async () => {
692
+ connectorRegistry.getConnector.mockImplementationOnce(() => {
693
+ throw new Error('connector unavailable');
694
+ });
695
+ await expect(request(app).get('/implementedInterfaces').query({ platform: 'testCRM' })).resolves.toMatchObject({ status: 400 });
696
+
697
+ authCore.getLicenseStatus.mockRejectedValueOnce(new Error('license failed'));
698
+ expect((await request(app).get('/licenseStatus').query(authQuery())).body.licenseStatus).toBe('Invalid (Connect to get license status)');
699
+
700
+ authCore.authValidation.mockRejectedValueOnce({ response: { status: 503 }, message: 'validation failed' });
701
+ await expect(request(app).get('/authValidation').query(authQuery())).resolves.toMatchObject({ status: 400 });
702
+
703
+ adminCore.validateRcUserToken.mockRejectedValueOnce(new Error('rc token invalid'));
704
+ await expect(request(app).get('/apiKeyManagedAuthState').query({ platform: 'testCRM', rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
705
+
706
+ adminCore.validateAdminRole.mockRejectedValueOnce(new Error('admin validation failed'));
707
+ await expect(request(app).get('/oauthManagedAuthState').query({ platform: 'testCRM', rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
708
+
709
+ UserModel.findByPk.mockResolvedValueOnce(null);
710
+ await expect(request(app).get('/admin/settings').query({ ...authQuery(), rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
711
+
712
+ adminCore.getAdminSettings.mockResolvedValueOnce(null);
713
+ expect((await request(app).get('/admin/settings').query({ ...authQuery(), rcAccessToken: 'rc-token' })).body).toEqual({
714
+ customConnector: null,
715
+ userSettings: {},
716
+ });
717
+
718
+ adminCore.validateAdminRole.mockResolvedValueOnce({ isValidated: false, rcAccountId: 'rc-account-1' });
719
+ await expect(request(app).post('/admin/settings').query({ rcAccessToken: 'rc-token' }).send({ adminSettings: {} })).resolves.toMatchObject({ status: 403 });
720
+
721
+ UserModel.findByPk.mockResolvedValueOnce(null);
722
+ await expect(request(app).get('/admin/managedAuth').query({ ...authQuery(), rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
723
+
724
+ adminCore.validateAdminRole.mockResolvedValueOnce({ isValidated: false, rcAccountId: 'rc-account-1' });
725
+ await expect(request(app).post('/admin/managedOAuth/cache').query({ rcAccessToken: 'rc-token' }).send({ values: {} })).resolves.toMatchObject({ status: 403 });
726
+
727
+ managedOAuthCore.upsertPendingManagedOAuth.mockRejectedValueOnce(new Error('cache failed'));
728
+ await expect(request(app).post('/admin/managedOAuth/cache').query({ rcAccessToken: 'rc-token' }).send({ values: {} })).resolves.toMatchObject({ status: 400 });
729
+
730
+ managedOAuthCore.clearPendingManagedOAuth.mockRejectedValueOnce(new Error('clear failed'));
731
+ await expect(request(app).delete('/admin/managedOAuth/cache').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
732
+
733
+ managedOAuthCore.resetManagedOAuth.mockRejectedValueOnce(new Error('reset failed'));
734
+ await expect(request(app).delete('/admin/managedOAuth/account').query({ rcAccessToken: 'rc-token', platform: 'testCRM' })).resolves.toMatchObject({ status: 400 });
735
+
736
+ adminCore.getUserMapping.mockResolvedValueOnce({ isRevokeUserSession: true });
737
+ await expect(request(app).post('/admin/userMapping').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ rcExtensionList: ['100'] })).resolves.toMatchObject({ status: 401 });
738
+
739
+ adminCore.reinitializeUserMapping.mockResolvedValueOnce({ isRevokeUserSession: true });
740
+ await expect(request(app).post('/admin/reinitializeUserMapping').query({ ...authQuery(), rcAccessToken: 'rc-token' }).send({ rcExtensionList: ['100'] })).resolves.toMatchObject({ status: 401 });
741
+
742
+ UserModel.findByPk.mockResolvedValueOnce(null);
743
+ await expect(request(app).get('/admin/serverLoggingSettings').query(authQuery())).resolves.toMatchObject({ status: 400 });
744
+
745
+ adminCore.updateServerLoggingSettings.mockRejectedValueOnce(new Error('settings failed'));
746
+ await expect(request(app).post('/admin/serverLoggingSettings').query(authQuery()).send({ additionalFieldValues: { enabled: true } })).resolves.toMatchObject({ status: 400 });
747
+
748
+ userCore.getUserSettingsByAdmin.mockRejectedValueOnce(new Error('preload failed'));
749
+ await expect(request(app).get('/user/preloadSettings').query({ rcAccessToken: 'rc-token' })).resolves.toMatchObject({ status: 400 });
750
+
751
+ userCore.refreshUserInfo.mockRejectedValueOnce(new Error('refresh failed'));
752
+ await expect(request(app).post('/user/refreshInfo').query(authQuery()).send({})).resolves.toMatchObject({ status: 400 });
753
+
754
+ UserModel.findByPk.mockResolvedValueOnce(null);
755
+ await expect(request(app).get('/user/settings').query(authQuery())).resolves.toMatchObject({ status: 400 });
756
+
757
+ jwt.decodeJwt.mockReturnValueOnce({ id: 'user-1' }).mockReturnValueOnce({ id: 'user-1' });
758
+ await expect(request(app).post('/user/settings').query({ jwtToken: 'valid-crm-jwt' }).send({ userSettings: {} })).resolves.toMatchObject({ status: 400 });
759
+ jwt.decodeJwt.mockReturnValue(decodedJwt);
760
+
761
+ UserModel.findByPk.mockResolvedValueOnce(null);
762
+ await expect(request(app).get('/hostname').query(authQuery())).resolves.toMatchObject({ status: 400 });
763
+
764
+ authCore.onOAuthCallback.mockRejectedValueOnce(new Error('oauth failed'));
765
+ const failedState = encodeURIComponent('platform=testCRM&hostname=crm.example.com&sessionId=session-failed');
766
+ await expect(request(app).get('/oauth-callback').query({
767
+ callbackUri: `https://redirect.example.com/callback?state=${failedState}`,
768
+ code: 'oauth-code',
769
+ })).resolves.toMatchObject({ status: 400 });
770
+
771
+ authCore.onApiKeyLogin.mockResolvedValueOnce({
772
+ userInfo: null,
773
+ returnMessage: { messageType: 'warning', message: 'Rejected' },
774
+ });
775
+ await expect(request(app).post('/apiKeyLogin').send({ platform: 'testCRM', apiKey: 'api-key' })).resolves.toMatchObject({ status: 400 });
776
+
777
+ connectorRegistry.getConnector.mockReturnValueOnce({
778
+ unAuthorize: jest.fn().mockRejectedValue(new Error('logout failed')),
779
+ });
780
+ await expect(request(app).post('/unAuthorize').query(authQuery()).send({})).resolves.toMatchObject({ status: 400 });
781
+
782
+ contactCore.findContact.mockRejectedValueOnce({ response: { status: 500 }, message: 'find failed' });
783
+ await expect(request(app).get('/contact').query({ ...authQuery(), phoneNumber: '+1555' })).resolves.toMatchObject({ status: 400 });
784
+
785
+ contactCore.createContact.mockRejectedValueOnce({ response: { status: 500 }, message: 'create failed' });
786
+ await expect(request(app).post('/contact').query(authQuery()).send({ phoneNumber: '+1555' })).resolves.toMatchObject({ status: 400 });
787
+
788
+ appointmentCore.listAppointments.mockRejectedValueOnce({ response: { status: 500 }, message: 'list failed' });
789
+ await expect(request(app).get('/appointments').query(authQuery())).resolves.toMatchObject({ status: 400 });
790
+
791
+ appointmentCore.createAppointment.mockRejectedValueOnce({ response: { status: 500 }, message: 'create appointment failed' });
792
+ await expect(request(app).post('/appointments').query(authQuery()).send({ payload: {} })).resolves.toMatchObject({ status: 400 });
793
+
794
+ appointmentCore.updateAppointment.mockRejectedValueOnce({ response: { status: 500 }, message: 'update appointment failed' });
795
+ await expect(request(app).patch('/appointments/appt-2').query(authQuery()).send({ patch: {} })).resolves.toMatchObject({ status: 400 });
796
+
797
+ appointmentCore.refreshAppointment.mockRejectedValueOnce({ response: { status: 500 }, message: 'refresh appointment failed' });
798
+ await expect(request(app).get('/appointments/appt-2/refresh').query(authQuery())).resolves.toMatchObject({ status: 400 });
799
+
800
+ appointmentCore.confirmAppointment.mockRejectedValueOnce({ response: { status: 500 }, message: 'confirm appointment failed' });
801
+ await expect(request(app).post('/appointments/appt-2/confirm').query(authQuery())).resolves.toMatchObject({ status: 400 });
802
+
803
+ appointmentCore.cancelAppointment.mockRejectedValueOnce({ response: { status: 500 }, message: 'cancel appointment failed' });
804
+ await expect(request(app).post('/appointments/appt-2/cancel').query(authQuery())).resolves.toMatchObject({ status: 400 });
805
+
806
+ logCore.saveNoteCache.mockRejectedValueOnce({ response: { status: 500 }, message: 'cache failed' });
807
+ await expect(request(app).post('/callLog/cacheNote').query(authQuery()).send({ sessionId: 's1' })).resolves.toMatchObject({ status: 400 });
808
+
809
+ logCore.getCallLog.mockRejectedValueOnce({ response: { status: 500 }, message: 'get log failed' });
810
+ await expect(request(app).get('/callLog').query(authQuery())).resolves.toMatchObject({ status: 400 });
811
+
812
+ logCore.createCallLog.mockRejectedValueOnce({ response: { status: 500 }, message: 'create log failed' });
813
+ await expect(request(app).post('/callLog').query(authQuery()).send({ logInfo: { accountId: 'acc' } })).resolves.toMatchObject({ status: 400 });
814
+
815
+ logCore.updateCallLog.mockRejectedValueOnce({ response: { status: 500 }, message: 'update log failed' });
816
+ await expect(request(app).patch('/callLog').query(authQuery()).send({ accountId: 'acc' })).resolves.toMatchObject({ status: 400 });
817
+
818
+ dispositionCore.upsertCallDisposition.mockRejectedValueOnce({ response: { status: 500 }, message: 'disposition failed' });
819
+ await expect(request(app).put('/callDisposition').query(authQuery()).send({ sessionId: 's1' })).resolves.toMatchObject({ status: 400 });
820
+
821
+ logCore.createMessageLog.mockRejectedValueOnce({ response: { status: 500 }, message: 'message failed' });
822
+ await expect(request(app).post('/messageLog').query(authQuery()).send({ messages: [] })).resolves.toMatchObject({ status: 400 });
823
+
824
+ calldown.list.mockRejectedValueOnce({ response: { status: 500 }, message: 'calldown failed' });
825
+ await expect(request(app).get('/calldown').query(authQuery())).resolves.toMatchObject({ status: 400 });
826
+
827
+ contactCore.findContactWithName.mockRejectedValueOnce({ response: { status: 500 }, message: 'search failed' });
828
+ await expect(request(app).get('/custom/contact/search').query({ ...authQuery(), name: 'Alice' })).resolves.toMatchObject({ status: 400 });
829
+
830
+ UserModel.findByPk.mockResolvedValueOnce(null);
831
+ await expect(request(app).get('/ringcentral/admin/report').query(authQuery())).resolves.toMatchObject({ status: 400 });
832
+
833
+ UserModel.findByPk.mockResolvedValueOnce(null);
834
+ await expect(request(app).get('/ringcentral/admin/userReport').query(authQuery())).resolves.toMatchObject({ status: 400 });
835
+
836
+ UserModel.findByPk.mockResolvedValueOnce(null);
837
+ await expect(request(app).get('/ringcentral/oauth/callback').query({ ...authQuery(), code: 'rc-code' })).resolves.toMatchObject({ status: 400 });
838
+
839
+ logCore.handleAsyncPluginCallback.mockRejectedValueOnce(new Error('plugin failed'));
840
+ await expect(request(app).post('/plugin/async-callback/task-1').send({ successful: true })).resolves.toMatchObject({ status: 500 });
841
+ });
842
+
843
+ test('covers exported app and initialization helpers', async () => {
844
+ const middleware = createCoreMiddleware();
845
+ expect(middleware).toHaveLength(3);
846
+
847
+ await initializeCore({ skipDatabaseInit: true });
848
+ expect(analytics.init).toHaveBeenCalled();
849
+
850
+ const fullApp = createCoreApp({ skipDatabaseInit: true, skipAnalyticsInit: true });
851
+ const response = await request(fullApp).get('/isAlive');
852
+ expect(response.status).toBe(200);
853
+ expect(response.text).toBe('OK');
854
+ });
855
+ });