@app-connect/core 1.7.24 → 1.7.26

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 (137) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -42
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -163
  6. package/connector/proxy/index.js +500 -500
  7. package/connector/registry.js +252 -252
  8. package/docs/README.md +50 -50
  9. package/docs/architecture.md +93 -93
  10. package/docs/connectors.md +116 -117
  11. package/docs/handlers.md +125 -125
  12. package/docs/libraries.md +101 -101
  13. package/docs/models.md +144 -144
  14. package/docs/routes.md +115 -115
  15. package/docs/tests.md +73 -73
  16. package/handlers/admin.js +523 -523
  17. package/handlers/appointment.js +193 -0
  18. package/handlers/auth.js +296 -296
  19. package/handlers/calldown.js +99 -99
  20. package/handlers/contact.js +280 -280
  21. package/handlers/disposition.js +82 -80
  22. package/handlers/log.js +984 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/plugin.js +208 -208
  25. package/handlers/user.js +142 -142
  26. package/index.js +3140 -2652
  27. package/jest.config.js +56 -56
  28. package/lib/analytics.js +54 -54
  29. package/lib/authSession.js +109 -109
  30. package/lib/cacheCleanup.js +21 -0
  31. package/lib/callLogComposer.js +898 -898
  32. package/lib/callLogLookup.js +34 -0
  33. package/lib/constants.js +8 -8
  34. package/lib/debugTracer.js +177 -177
  35. package/lib/encode.js +30 -30
  36. package/lib/errorHandler.js +218 -206
  37. package/lib/generalErrorMessage.js +41 -41
  38. package/lib/jwt.js +18 -18
  39. package/lib/logger.js +190 -190
  40. package/lib/migrateCallLogsSchema.js +116 -0
  41. package/lib/ringcentral.js +266 -266
  42. package/lib/s3ErrorLogReport.js +65 -65
  43. package/lib/sharedSMSComposer.js +471 -471
  44. package/lib/util.js +67 -67
  45. package/mcp/README.md +412 -395
  46. package/mcp/lib/validator.js +91 -91
  47. package/mcp/mcpHandler.js +425 -425
  48. package/mcp/tools/cancelAppointment.js +101 -0
  49. package/mcp/tools/checkAuthStatus.js +105 -105
  50. package/mcp/tools/confirmAppointment.js +101 -0
  51. package/mcp/tools/createAppointment.js +157 -0
  52. package/mcp/tools/createCallLog.js +327 -316
  53. package/mcp/tools/createContact.js +117 -117
  54. package/mcp/tools/createMessageLog.js +287 -287
  55. package/mcp/tools/doAuth.js +60 -60
  56. package/mcp/tools/findContactByName.js +93 -93
  57. package/mcp/tools/findContactByPhone.js +101 -101
  58. package/mcp/tools/getCallLog.js +111 -102
  59. package/mcp/tools/getGoogleFilePicker.js +99 -99
  60. package/mcp/tools/getHelp.js +43 -43
  61. package/mcp/tools/getPublicConnectors.js +94 -94
  62. package/mcp/tools/getSessionInfo.js +90 -90
  63. package/mcp/tools/index.js +51 -41
  64. package/mcp/tools/listAppointments.js +163 -0
  65. package/mcp/tools/logout.js +96 -96
  66. package/mcp/tools/rcGetCallLogs.js +65 -65
  67. package/mcp/tools/updateAppointment.js +154 -0
  68. package/mcp/tools/updateCallLog.js +130 -126
  69. package/mcp/ui/App/App.tsx +358 -358
  70. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  71. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  72. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  73. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  74. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  75. package/mcp/ui/App/lib/callTool.ts +130 -130
  76. package/mcp/ui/App/lib/debugLog.ts +41 -41
  77. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  78. package/mcp/ui/App/main.css +5 -5
  79. package/mcp/ui/App/root.tsx +13 -13
  80. package/mcp/ui/index.html +13 -13
  81. package/mcp/ui/package-lock.json +6356 -6356
  82. package/mcp/ui/package.json +25 -25
  83. package/mcp/ui/tsconfig.json +26 -26
  84. package/mcp/ui/vite.config.ts +16 -16
  85. package/models/accountDataModel.js +33 -33
  86. package/models/adminConfigModel.js +35 -35
  87. package/models/cacheModel.js +30 -26
  88. package/models/callDownListModel.js +34 -34
  89. package/models/callLogModel.js +33 -27
  90. package/models/dynamo/connectorSchema.js +146 -146
  91. package/models/dynamo/lockSchema.js +24 -24
  92. package/models/dynamo/noteCacheSchema.js +29 -29
  93. package/models/llmSessionModel.js +17 -17
  94. package/models/messageLogModel.js +25 -25
  95. package/models/sequelize.js +16 -16
  96. package/models/userModel.js +45 -45
  97. package/package.json +72 -72
  98. package/releaseNotes.json +1093 -1073
  99. package/test/connector/proxy/engine.test.js +126 -93
  100. package/test/connector/proxy/index.test.js +279 -279
  101. package/test/connector/proxy/sample.json +161 -161
  102. package/test/connector/registry.test.js +415 -415
  103. package/test/handlers/admin.test.js +616 -616
  104. package/test/handlers/auth.test.js +1018 -1015
  105. package/test/handlers/contact.test.js +1014 -1014
  106. package/test/handlers/log.test.js +1298 -1160
  107. package/test/handlers/managedAuth.test.js +458 -458
  108. package/test/handlers/plugin.test.js +380 -380
  109. package/test/index.test.js +105 -105
  110. package/test/lib/cacheCleanup.test.js +42 -0
  111. package/test/lib/callLogComposer.test.js +1231 -1231
  112. package/test/lib/debugTracer.test.js +328 -328
  113. package/test/lib/jwt.test.js +176 -176
  114. package/test/lib/logger.test.js +206 -206
  115. package/test/lib/oauth.test.js +359 -359
  116. package/test/lib/ringcentral.test.js +467 -467
  117. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  118. package/test/lib/util.test.js +329 -329
  119. package/test/mcp/tools/checkAuthStatus.test.js +83 -82
  120. package/test/mcp/tools/createCallLog.test.js +436 -436
  121. package/test/mcp/tools/createContact.test.js +58 -58
  122. package/test/mcp/tools/createMessageLog.test.js +595 -595
  123. package/test/mcp/tools/doAuth.test.js +113 -113
  124. package/test/mcp/tools/findContactByName.test.js +275 -275
  125. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  126. package/test/mcp/tools/getCallLog.test.js +298 -298
  127. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  128. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  129. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  130. package/test/mcp/tools/logout.test.js +233 -233
  131. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  132. package/test/mcp/tools/updateCallLog.test.js +360 -360
  133. package/test/models/accountDataModel.test.js +98 -98
  134. package/test/models/dynamo/connectorSchema.test.js +189 -189
  135. package/test/models/models.test.js +568 -539
  136. package/test/routes/managedAuthRoutes.test.js +104 -129
  137. package/test/setup.js +178 -178
@@ -1,1014 +1,1014 @@
1
- // Use in-memory SQLite for isolated model tests
2
- jest.mock('../../models/sequelize', () => {
3
- const { Sequelize } = require('sequelize');
4
- return {
5
- sequelize: new Sequelize({
6
- dialect: 'sqlite',
7
- storage: ':memory:',
8
- logging: false,
9
- }),
10
- };
11
- });
12
-
13
- jest.mock('../../connector/registry');
14
- jest.mock('../../lib/oauth');
15
- jest.mock('../../models/dynamo/connectorSchema', () => ({
16
- Connector: {
17
- getProxyConfig: jest.fn()
18
- }
19
- }));
20
-
21
- const contactHandler = require('../../handlers/contact');
22
- const { UserModel } = require('../../models/userModel');
23
- const { AccountDataModel } = require('../../models/accountDataModel');
24
- const connectorRegistry = require('../../connector/registry');
25
- const oauth = require('../../lib/oauth');
26
- const { Connector } = require('../../models/dynamo/connectorSchema');
27
- const { sequelize } = require('../../models/sequelize');
28
-
29
- describe('Contact Handler', () => {
30
- beforeAll(async () => {
31
- await UserModel.sync({ force: true });
32
- await AccountDataModel.sync({ force: true });
33
- });
34
-
35
- afterEach(async () => {
36
- await UserModel.destroy({ where: {} });
37
- await AccountDataModel.destroy({ where: {} });
38
- jest.clearAllMocks();
39
- });
40
-
41
- afterAll(async () => {
42
- await sequelize.close();
43
- });
44
-
45
- describe('findContact', () => {
46
- const mockUser = {
47
- id: 'test-user-id',
48
- platform: 'testCRM',
49
- accessToken: 'test-access-token',
50
- rcAccountId: 'rc-account-123',
51
- platformAdditionalInfo: {}
52
- };
53
-
54
- test('should return warning when user not found', async () => {
55
- // Act
56
- const result = await contactHandler.findContact({
57
- platform: 'testCRM',
58
- userId: 'non-existent-user',
59
- phoneNumber: '+1234567890'
60
- });
61
-
62
- // Assert
63
- expect(result.successful).toBe(false);
64
- expect(result.returnMessage.message).toBe('Contact not found');
65
- expect(result.returnMessage.messageType).toBe('warning');
66
- });
67
-
68
- test('should return warning when user has no access token', async () => {
69
- // Arrange
70
- await UserModel.create({
71
- id: 'test-user-id',
72
- platform: 'testCRM',
73
- accessToken: null
74
- });
75
-
76
- // Act
77
- const result = await contactHandler.findContact({
78
- platform: 'testCRM',
79
- userId: 'test-user-id',
80
- phoneNumber: '+1234567890'
81
- });
82
-
83
- // Assert
84
- expect(result.successful).toBe(false);
85
- expect(result.returnMessage.message).toBe('Contact not found');
86
- });
87
-
88
- test('should return cached contact when available', async () => {
89
- // Arrange
90
- await UserModel.create(mockUser);
91
-
92
- const cachedContact = [
93
- { id: 'contact-1', name: 'Cached Contact', type: 'Contact', phone: '+1234567890' }
94
- ];
95
- await AccountDataModel.create({
96
- rcAccountId: 'rc-account-123',
97
- platformName: 'testCRM',
98
- dataKey: 'contact-+1234567890',
99
- data: cachedContact
100
- });
101
-
102
- // Act
103
- const result = await contactHandler.findContact({
104
- platform: 'testCRM',
105
- userId: 'test-user-id',
106
- phoneNumber: '+1234567890',
107
- isForceRefreshAccountData: false
108
- });
109
-
110
- // Assert
111
- expect(result.successful).toBe(true);
112
- expect(result.contact).toEqual(cachedContact);
113
- // Should not call platform module
114
- expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
115
- });
116
-
117
- test('should bypass cache when isForceRefreshAccountData is true', async () => {
118
- // Arrange
119
- await UserModel.create(mockUser);
120
-
121
- const cachedContact = [
122
- { id: 'contact-1', name: 'Cached Contact', type: 'Contact', phone: '+1234567890' }
123
- ];
124
- await AccountDataModel.create({
125
- rcAccountId: 'rc-account-123',
126
- platformName: 'testCRM',
127
- dataKey: 'contact-+1234567890',
128
- data: cachedContact
129
- });
130
-
131
- const freshContact = [
132
- { id: 'contact-1', name: 'Fresh Contact', type: 'Contact', phone: '+1234567890' }
133
- ];
134
-
135
- const mockConnector = {
136
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
137
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
138
- findContact: jest.fn().mockResolvedValue({
139
- successful: true,
140
- matchedContactInfo: freshContact
141
- })
142
- };
143
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
144
-
145
- // Act
146
- const result = await contactHandler.findContact({
147
- platform: 'testCRM',
148
- userId: 'test-user-id',
149
- phoneNumber: '+1234567890',
150
- isForceRefreshAccountData: true
151
- });
152
-
153
- // Assert
154
- expect(result.successful).toBe(true);
155
- expect(result.contact).toEqual(freshContact);
156
- expect(mockConnector.findContact).toHaveBeenCalled();
157
- });
158
-
159
- test('should find contact with apiKey auth and cache result', async () => {
160
- // Arrange
161
- await UserModel.create(mockUser);
162
-
163
- const matchedContact = [
164
- { id: 'contact-new', name: 'New Contact', type: 'Contact', phone: '+9876543210' }
165
- ];
166
-
167
- const mockConnector = {
168
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
169
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
170
- findContact: jest.fn().mockResolvedValue({
171
- successful: true,
172
- matchedContactInfo: matchedContact
173
- })
174
- };
175
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
176
-
177
- // Act
178
- const result = await contactHandler.findContact({
179
- platform: 'testCRM',
180
- userId: 'test-user-id',
181
- phoneNumber: '+9876543210'
182
- });
183
-
184
- // Assert
185
- expect(result.successful).toBe(true);
186
- expect(result.contact).toEqual(matchedContact);
187
- expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'test-access-token' });
188
-
189
- // Verify contact was cached
190
- const cachedData = await AccountDataModel.findOne({
191
- where: {
192
- rcAccountId: 'rc-account-123',
193
- platformName: 'testCRM',
194
- dataKey: 'contact-+9876543210'
195
- }
196
- });
197
- expect(cachedData).not.toBeNull();
198
- expect(cachedData.data).toEqual(matchedContact);
199
- });
200
-
201
- test('should find contact with oauth auth', async () => {
202
- // Arrange
203
- await UserModel.create(mockUser);
204
-
205
- const matchedContact = [
206
- { id: 'oauth-contact', name: 'OAuth Contact', type: 'Lead' }
207
- ];
208
-
209
- const mockConnector = {
210
- getAuthType: jest.fn().mockResolvedValue('oauth'),
211
- getOauthInfo: jest.fn().mockResolvedValue({
212
- clientId: 'client-id',
213
- clientSecret: 'client-secret',
214
- accessTokenUri: 'https://token.url',
215
- authorizationUri: 'https://auth.url'
216
- }),
217
- findContact: jest.fn().mockResolvedValue({
218
- successful: true,
219
- matchedContactInfo: matchedContact
220
- })
221
- };
222
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
223
-
224
- const mockOAuthApp = {};
225
- oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
226
- oauth.checkAndRefreshAccessToken.mockResolvedValue({
227
- ...mockUser,
228
- accessToken: 'refreshed-token'
229
- });
230
-
231
- // Act
232
- const result = await contactHandler.findContact({
233
- platform: 'testCRM',
234
- userId: 'test-user-id',
235
- phoneNumber: '+1111111111'
236
- });
237
-
238
- // Assert
239
- expect(result.successful).toBe(true);
240
- expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
241
- });
242
-
243
- test('should use proxy config when proxyId is present', async () => {
244
- // Arrange
245
- const userWithProxy = {
246
- ...mockUser,
247
- platformAdditionalInfo: { proxyId: 'proxy-123' }
248
- };
249
- await UserModel.create(userWithProxy);
250
-
251
- const proxyConfig = { baseUrl: 'https://proxy.example.com' };
252
- Connector.getProxyConfig.mockResolvedValue(proxyConfig);
253
-
254
- const matchedContact = [{ id: 'proxy-contact', name: 'Proxy Contact' }];
255
-
256
- const mockConnector = {
257
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
258
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
259
- findContact: jest.fn().mockResolvedValue({
260
- successful: true,
261
- matchedContactInfo: matchedContact
262
- })
263
- };
264
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
265
-
266
- // Act
267
- const result = await contactHandler.findContact({
268
- platform: 'testCRM',
269
- userId: 'test-user-id',
270
- phoneNumber: '+2222222222'
271
- });
272
-
273
- // Assert
274
- expect(result.successful).toBe(true);
275
- expect(Connector.getProxyConfig).toHaveBeenCalledWith('proxy-123');
276
- expect(mockConnector.findContact).toHaveBeenCalledWith(
277
- expect.objectContaining({ proxyConfig })
278
- );
279
- });
280
-
281
- test('should return warning when no contacts found', async () => {
282
- // Arrange
283
- await UserModel.create(mockUser);
284
-
285
- const mockConnector = {
286
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
287
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
288
- findContact: jest.fn().mockResolvedValue({
289
- successful: false,
290
- matchedContactInfo: null
291
- })
292
- };
293
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
294
-
295
- // Act
296
- const result = await contactHandler.findContact({
297
- platform: 'testCRM',
298
- userId: 'test-user-id',
299
- phoneNumber: '+9999999999'
300
- });
301
-
302
- // Assert
303
- expect(result.successful).toBe(false);
304
- expect(result.returnMessage.message).toBe('Contact not found');
305
- expect(result.returnMessage.details[0].items[0].text).toContain('+9999999999');
306
- });
307
-
308
- test('should return platform returnMessage when provided', async () => {
309
- // Arrange
310
- await UserModel.create(mockUser);
311
-
312
- const customReturnMessage = {
313
- message: 'Custom platform message',
314
- messageType: 'info',
315
- ttl: 5000
316
- };
317
-
318
- const mockConnector = {
319
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
320
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
321
- findContact: jest.fn().mockResolvedValue({
322
- successful: false,
323
- matchedContactInfo: null,
324
- returnMessage: customReturnMessage
325
- })
326
- };
327
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
328
-
329
- // Act
330
- const result = await contactHandler.findContact({
331
- platform: 'testCRM',
332
- userId: 'test-user-id',
333
- phoneNumber: '+8888888888'
334
- });
335
-
336
- // Assert
337
- expect(result.returnMessage).toEqual(customReturnMessage);
338
- });
339
-
340
- test('should handle 429 rate limit error', async () => {
341
- // Arrange
342
- await UserModel.create(mockUser);
343
-
344
- const mockConnector = {
345
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
346
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
347
- findContact: jest.fn().mockRejectedValue({
348
- response: { status: 429 }
349
- })
350
- };
351
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
352
-
353
- // Act
354
- const result = await contactHandler.findContact({
355
- platform: 'testCRM',
356
- userId: 'test-user-id',
357
- phoneNumber: '+7777777777'
358
- });
359
-
360
- // Assert
361
- expect(result.successful).toBe(false);
362
- expect(result.extraDataTracking.statusCode).toBe(429);
363
- });
364
-
365
- test('should handle 401 authorization error', async () => {
366
- // Arrange
367
- await UserModel.create(mockUser);
368
-
369
- const mockConnector = {
370
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
371
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
372
- findContact: jest.fn().mockRejectedValue({
373
- response: { status: 401 }
374
- })
375
- };
376
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
377
-
378
- // Act
379
- const result = await contactHandler.findContact({
380
- platform: 'testCRM',
381
- userId: 'test-user-id',
382
- phoneNumber: '+6666666666'
383
- });
384
-
385
- // Assert
386
- expect(result.successful).toBe(false);
387
- expect(result.extraDataTracking.statusCode).toBe(401);
388
- });
389
-
390
- test('should update existing cached contact', async () => {
391
- // Arrange
392
- await UserModel.create(mockUser);
393
-
394
- // Create existing cached contact
395
- await AccountDataModel.create({
396
- rcAccountId: 'rc-account-123',
397
- platformName: 'testCRM',
398
- dataKey: 'contact-+5555555555',
399
- data: [{ id: 'old-contact', name: 'Old Name' }]
400
- });
401
-
402
- const updatedContact = [
403
- { id: 'old-contact', name: 'Updated Name', type: 'Contact' }
404
- ];
405
-
406
- const mockConnector = {
407
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
408
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
409
- findContact: jest.fn().mockResolvedValue({
410
- successful: true,
411
- matchedContactInfo: updatedContact
412
- })
413
- };
414
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
415
-
416
- // Act
417
- const result = await contactHandler.findContact({
418
- platform: 'testCRM',
419
- userId: 'test-user-id',
420
- phoneNumber: '+5555555555',
421
- isForceRefreshAccountData: true
422
- });
423
-
424
- // Assert
425
- expect(result.successful).toBe(true);
426
- expect(result.contact).toEqual(updatedContact);
427
-
428
- // Verify cache was updated
429
- const cachedData = await AccountDataModel.findOne({
430
- where: {
431
- rcAccountId: 'rc-account-123',
432
- platformName: 'testCRM',
433
- dataKey: 'contact-+5555555555'
434
- }
435
- });
436
- expect(cachedData.data).toEqual(updatedContact);
437
- });
438
-
439
- test('should remove cached contact when force refresh returns null', async () => {
440
- // Arrange
441
- await UserModel.create(mockUser);
442
- await AccountDataModel.create({
443
- rcAccountId: 'rc-account-123',
444
- platformName: 'testCRM',
445
- dataKey: 'contact-+3333333333',
446
- data: [{ id: 'stale-contact', name: 'Stale Contact' }]
447
- });
448
-
449
- const mockConnector = {
450
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
451
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
452
- findContact: jest.fn().mockResolvedValue({
453
- successful: false,
454
- matchedContactInfo: null
455
- })
456
- };
457
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
458
-
459
- // Act
460
- const result = await contactHandler.findContact({
461
- platform: 'testCRM',
462
- userId: 'test-user-id',
463
- phoneNumber: '+3333333333',
464
- isForceRefreshAccountData: true
465
- });
466
-
467
- // Assert
468
- expect(result.successful).toBe(false);
469
- const cachedData = await AccountDataModel.findOne({
470
- where: {
471
- rcAccountId: 'rc-account-123',
472
- platformName: 'testCRM',
473
- dataKey: 'contact-+3333333333'
474
- }
475
- });
476
- expect(cachedData).toBeNull();
477
- });
478
-
479
- test('should remove cached contact when force refresh returns empty array', async () => {
480
- // Arrange
481
- await UserModel.create(mockUser);
482
- await AccountDataModel.create({
483
- rcAccountId: 'rc-account-123',
484
- platformName: 'testCRM',
485
- dataKey: 'contact-+1212121212',
486
- data: [{ id: 'stale-contact', name: 'Stale Contact' }]
487
- });
488
-
489
- const mockConnector = {
490
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
491
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
492
- findContact: jest.fn().mockResolvedValue({
493
- successful: false,
494
- matchedContactInfo: []
495
- })
496
- };
497
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
498
-
499
- // Act
500
- await contactHandler.findContact({
501
- platform: 'testCRM',
502
- userId: 'test-user-id',
503
- phoneNumber: '+1212121212',
504
- isForceRefreshAccountData: true
505
- });
506
-
507
- // Assert
508
- const cachedData = await AccountDataModel.findOne({
509
- where: {
510
- rcAccountId: 'rc-account-123',
511
- platformName: 'testCRM',
512
- dataKey: 'contact-+1212121212'
513
- }
514
- });
515
- expect(cachedData).toBeNull();
516
- });
517
-
518
- test('should remove cached contact when force refresh returns only new contact placeholder', async () => {
519
- // Arrange
520
- await UserModel.create(mockUser);
521
- await AccountDataModel.create({
522
- rcAccountId: 'rc-account-123',
523
- platformName: 'testCRM',
524
- dataKey: 'contact-+3434343434',
525
- data: [{ id: 'stale-contact', name: 'Stale Contact' }]
526
- });
527
-
528
- const placeholderContact = [
529
- { id: 'new-contact', name: 'Create Contact', isNewContact: true }
530
- ];
531
- const mockConnector = {
532
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
533
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
534
- findContact: jest.fn().mockResolvedValue({
535
- successful: false,
536
- matchedContactInfo: placeholderContact
537
- })
538
- };
539
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
540
-
541
- // Act
542
- const result = await contactHandler.findContact({
543
- platform: 'testCRM',
544
- userId: 'test-user-id',
545
- phoneNumber: '+3434343434',
546
- isForceRefreshAccountData: true
547
- });
548
-
549
- // Assert
550
- expect(result.contact).toEqual(placeholderContact);
551
- const cachedData = await AccountDataModel.findOne({
552
- where: {
553
- rcAccountId: 'rc-account-123',
554
- platformName: 'testCRM',
555
- dataKey: 'contact-+3434343434'
556
- }
557
- });
558
- expect(cachedData).toBeNull();
559
- });
560
-
561
- test('should keep cached contact when force refresh connector call errors', async () => {
562
- // Arrange
563
- await UserModel.create(mockUser);
564
- await AccountDataModel.create({
565
- rcAccountId: 'rc-account-123',
566
- platformName: 'testCRM',
567
- dataKey: 'contact-+5656565656',
568
- data: [{ id: 'stale-contact', name: 'Stale Contact' }]
569
- });
570
-
571
- const mockConnector = {
572
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
573
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
574
- findContact: jest.fn().mockRejectedValue({
575
- response: { status: 500 }
576
- })
577
- };
578
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
579
-
580
- // Act
581
- const result = await contactHandler.findContact({
582
- platform: 'testCRM',
583
- userId: 'test-user-id',
584
- phoneNumber: '+5656565656',
585
- isForceRefreshAccountData: true
586
- });
587
-
588
- // Assert
589
- expect(result.successful).toBe(false);
590
- const cachedData = await AccountDataModel.findOne({
591
- where: {
592
- rcAccountId: 'rc-account-123',
593
- platformName: 'testCRM',
594
- dataKey: 'contact-+5656565656'
595
- }
596
- });
597
- expect(cachedData).not.toBeNull();
598
- expect(cachedData.data).toEqual([{ id: 'stale-contact', name: 'Stale Contact' }]);
599
- });
600
-
601
- test('should work with tracer when provided', async () => {
602
- // Arrange
603
- await UserModel.create(mockUser);
604
-
605
- const mockTracer = {
606
- trace: jest.fn(),
607
- traceError: jest.fn()
608
- };
609
-
610
- const matchedContact = [{ id: 'traced-contact', name: 'Traced Contact' }];
611
-
612
- const mockConnector = {
613
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
614
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
615
- findContact: jest.fn().mockResolvedValue({
616
- successful: true,
617
- matchedContactInfo: matchedContact
618
- })
619
- };
620
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
621
-
622
- // Act
623
- const result = await contactHandler.findContact({
624
- platform: 'testCRM',
625
- userId: 'test-user-id',
626
- phoneNumber: '+4444444444',
627
- tracer: mockTracer
628
- });
629
-
630
- // Assert
631
- expect(result.successful).toBe(true);
632
- expect(mockTracer.trace).toHaveBeenCalled();
633
- });
634
- });
635
-
636
- describe('createContact', () => {
637
- const mockUser = {
638
- id: 'test-user-id',
639
- platform: 'testCRM',
640
- accessToken: 'test-access-token',
641
- platformAdditionalInfo: {}
642
- };
643
-
644
- test('should return error when user not found', async () => {
645
- // Act
646
- const result = await contactHandler.createContact({
647
- platform: 'testCRM',
648
- userId: 'non-existent-user',
649
- phoneNumber: '+1234567890',
650
- newContactName: 'New Contact',
651
- newContactType: 'Contact'
652
- });
653
-
654
- // Assert
655
- expect(result.successful).toBe(false);
656
- expect(result.message).toBe('Contact not found');
657
- });
658
-
659
- test('should return error when user has no access token', async () => {
660
- // Arrange
661
- await UserModel.create({
662
- id: 'test-user-id',
663
- platform: 'testCRM',
664
- accessToken: null
665
- });
666
-
667
- // Act
668
- const result = await contactHandler.createContact({
669
- platform: 'testCRM',
670
- userId: 'test-user-id',
671
- phoneNumber: '+1234567890',
672
- newContactName: 'New Contact',
673
- newContactType: 'Contact'
674
- });
675
-
676
- // Assert
677
- expect(result.successful).toBe(false);
678
- });
679
-
680
- test('should successfully create contact with apiKey auth', async () => {
681
- // Arrange
682
- await UserModel.create(mockUser);
683
-
684
- const createdContact = {
685
- id: 'new-contact-123',
686
- name: 'New Contact',
687
- type: 'Contact',
688
- phone: '+1234567890'
689
- };
690
-
691
- const mockConnector = {
692
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
693
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
694
- createContact: jest.fn().mockResolvedValue({
695
- contactInfo: createdContact,
696
- returnMessage: { message: 'Contact created', messageType: 'success', ttl: 2000 }
697
- })
698
- };
699
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
700
-
701
- // Act
702
- const result = await contactHandler.createContact({
703
- platform: 'testCRM',
704
- userId: 'test-user-id',
705
- phoneNumber: '+1234567890',
706
- newContactName: 'New Contact',
707
- newContactType: 'Contact',
708
- additionalSubmission: {}
709
- });
710
-
711
- // Assert
712
- expect(result.successful).toBe(true);
713
- expect(result.contact).toEqual(createdContact);
714
- expect(mockConnector.createContact).toHaveBeenCalledWith({
715
- user: expect.any(Object),
716
- authHeader: 'Basic base64-encoded',
717
- phoneNumber: '+1234567890',
718
- newContactName: 'New Contact',
719
- newContactType: 'Contact',
720
- additionalSubmission: {},
721
- proxyConfig: null
722
- });
723
- });
724
-
725
- test('should successfully create contact with oauth auth', async () => {
726
- // Arrange
727
- await UserModel.create(mockUser);
728
-
729
- const createdContact = {
730
- id: 'oauth-contact-123',
731
- name: 'OAuth Contact'
732
- };
733
-
734
- const mockConnector = {
735
- getAuthType: jest.fn().mockResolvedValue('oauth'),
736
- getOauthInfo: jest.fn().mockResolvedValue({
737
- clientId: 'client-id',
738
- clientSecret: 'client-secret',
739
- accessTokenUri: 'https://token.url',
740
- authorizationUri: 'https://auth.url'
741
- }),
742
- createContact: jest.fn().mockResolvedValue({
743
- contactInfo: createdContact,
744
- returnMessage: { message: 'Created', messageType: 'success' }
745
- })
746
- };
747
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
748
-
749
- oauth.getOAuthApp.mockReturnValue({});
750
- oauth.checkAndRefreshAccessToken.mockResolvedValue({
751
- ...mockUser,
752
- accessToken: 'refreshed-token'
753
- });
754
-
755
- // Act
756
- const result = await contactHandler.createContact({
757
- platform: 'testCRM',
758
- userId: 'test-user-id',
759
- phoneNumber: '+1234567890',
760
- newContactName: 'OAuth Contact',
761
- newContactType: 'Lead'
762
- });
763
-
764
- // Assert
765
- expect(result.successful).toBe(true);
766
- expect(result.contact).toEqual(createdContact);
767
- expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
768
- });
769
-
770
- test('should return unsuccessful when platform returns null contactInfo', async () => {
771
- // Arrange
772
- await UserModel.create(mockUser);
773
-
774
- const mockConnector = {
775
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
776
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
777
- createContact: jest.fn().mockResolvedValue({
778
- contactInfo: null,
779
- returnMessage: { message: 'Failed to create', messageType: 'error' }
780
- })
781
- };
782
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
783
-
784
- // Act
785
- const result = await contactHandler.createContact({
786
- platform: 'testCRM',
787
- userId: 'test-user-id',
788
- phoneNumber: '+1234567890',
789
- newContactName: 'Failed Contact',
790
- newContactType: 'Contact'
791
- });
792
-
793
- // Assert
794
- expect(result.successful).toBe(false);
795
- expect(result.returnMessage.message).toBe('Failed to create');
796
- });
797
-
798
- test('should handle 429 rate limit error', async () => {
799
- // Arrange
800
- await UserModel.create(mockUser);
801
-
802
- const mockConnector = {
803
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
804
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
805
- createContact: jest.fn().mockRejectedValue({
806
- response: { status: 429 }
807
- })
808
- };
809
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
810
-
811
- // Act
812
- const result = await contactHandler.createContact({
813
- platform: 'testCRM',
814
- userId: 'test-user-id',
815
- phoneNumber: '+1234567890',
816
- newContactName: 'Rate Limited',
817
- newContactType: 'Contact'
818
- });
819
-
820
- // Assert
821
- expect(result.successful).toBe(false);
822
- });
823
-
824
- test('should handle 401 authorization error', async () => {
825
- // Arrange
826
- await UserModel.create(mockUser);
827
-
828
- const mockConnector = {
829
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
830
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
831
- createContact: jest.fn().mockRejectedValue({
832
- response: { status: 401 }
833
- })
834
- };
835
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
836
-
837
- // Act
838
- const result = await contactHandler.createContact({
839
- platform: 'testCRM',
840
- userId: 'test-user-id',
841
- phoneNumber: '+1234567890',
842
- newContactName: 'Unauthorized',
843
- newContactType: 'Contact'
844
- });
845
-
846
- // Assert
847
- expect(result.successful).toBe(false);
848
- expect(result.extraDataTracking.statusCode).toBe(401);
849
- });
850
-
851
- test('should handle generic errors', async () => {
852
- // Arrange
853
- await UserModel.create(mockUser);
854
-
855
- const mockConnector = {
856
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
857
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
858
- createContact: jest.fn().mockRejectedValue(new Error('Unknown error'))
859
- };
860
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
861
-
862
- // Act
863
- const result = await contactHandler.createContact({
864
- platform: 'testCRM',
865
- userId: 'test-user-id',
866
- phoneNumber: '+1234567890',
867
- newContactName: 'Error Contact',
868
- newContactType: 'Contact'
869
- });
870
-
871
- // Assert
872
- expect(result.successful).toBe(false);
873
- expect(result.returnMessage.message).toBe('Error creating contact');
874
- });
875
- });
876
-
877
- describe('findContactWithName', () => {
878
- const mockUser = {
879
- id: 'test-user-id',
880
- platform: 'testCRM',
881
- accessToken: 'test-access-token',
882
- platformAdditionalInfo: {}
883
- };
884
-
885
- test('should return warning when user not found', async () => {
886
- // Act
887
- const result = await contactHandler.findContactWithName({
888
- platform: 'testCRM',
889
- userId: 'non-existent-user',
890
- name: 'John Doe'
891
- });
892
-
893
- // Assert
894
- expect(result.successful).toBe(false);
895
- expect(result.returnMessage.message).toContain('No contact found with name');
896
- });
897
-
898
- test('should find contact by name with apiKey auth', async () => {
899
- // Arrange
900
- await UserModel.create(mockUser);
901
-
902
- const matchedContacts = [
903
- { id: 'contact-1', name: 'John Doe', type: 'Contact' }
904
- ];
905
-
906
- const mockConnector = {
907
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
908
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
909
- findContactWithName: jest.fn().mockResolvedValue({
910
- successful: true,
911
- matchedContactInfo: matchedContacts
912
- })
913
- };
914
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
915
-
916
- // Act
917
- const result = await contactHandler.findContactWithName({
918
- platform: 'testCRM',
919
- userId: 'test-user-id',
920
- name: 'John Doe'
921
- });
922
-
923
- // Assert
924
- expect(result.successful).toBe(true);
925
- expect(result.contact).toEqual(matchedContacts);
926
- });
927
-
928
- test('should return warning when no contacts found by name', async () => {
929
- // Arrange
930
- await UserModel.create(mockUser);
931
-
932
- const mockConnector = {
933
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
934
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
935
- findContactWithName: jest.fn().mockResolvedValue({
936
- successful: false,
937
- matchedContactInfo: null
938
- })
939
- };
940
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
941
-
942
- // Act
943
- const result = await contactHandler.findContactWithName({
944
- platform: 'testCRM',
945
- userId: 'test-user-id',
946
- name: 'Unknown Person'
947
- });
948
-
949
- // Assert
950
- expect(result.successful).toBe(false);
951
- expect(result.returnMessage.message).toContain('No contact found with name');
952
- });
953
-
954
- test('should handle 429 rate limit error', async () => {
955
- // Arrange
956
- await UserModel.create(mockUser);
957
-
958
- const mockConnector = {
959
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
960
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
961
- findContactWithName: jest.fn().mockRejectedValue({
962
- response: { status: 429 }
963
- })
964
- };
965
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
966
-
967
- // Act
968
- const result = await contactHandler.findContactWithName({
969
- platform: 'testCRM',
970
- userId: 'test-user-id',
971
- name: 'Rate Limited'
972
- });
973
-
974
- // Assert
975
- expect(result.successful).toBe(false);
976
- });
977
-
978
- test('should use proxy config when proxyId is present', async () => {
979
- // Arrange
980
- const userWithProxy = {
981
- ...mockUser,
982
- platformAdditionalInfo: { proxyId: 'proxy-123' }
983
- };
984
- await UserModel.create(userWithProxy);
985
-
986
- const proxyConfig = { baseUrl: 'https://proxy.example.com' };
987
- Connector.getProxyConfig.mockResolvedValue(proxyConfig);
988
-
989
- const matchedContacts = [{ id: 'proxy-contact', name: 'Proxy Contact' }];
990
-
991
- const mockConnector = {
992
- getAuthType: jest.fn().mockResolvedValue('apiKey'),
993
- getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
994
- findContactWithName: jest.fn().mockResolvedValue({
995
- successful: true,
996
- matchedContactInfo: matchedContacts
997
- })
998
- };
999
- connectorRegistry.getConnector.mockReturnValue(mockConnector);
1000
-
1001
- // Act
1002
- const result = await contactHandler.findContactWithName({
1003
- platform: 'testCRM',
1004
- userId: 'test-user-id',
1005
- name: 'Proxy Contact'
1006
- });
1007
-
1008
- // Assert
1009
- expect(result.successful).toBe(true);
1010
- expect(Connector.getProxyConfig).toHaveBeenCalledWith('proxy-123');
1011
- });
1012
- });
1013
- });
1014
-
1
+ // Use in-memory SQLite for isolated model tests
2
+ jest.mock('../../models/sequelize', () => {
3
+ const { Sequelize } = require('sequelize');
4
+ return {
5
+ sequelize: new Sequelize({
6
+ dialect: 'sqlite',
7
+ storage: ':memory:',
8
+ logging: false,
9
+ }),
10
+ };
11
+ });
12
+
13
+ jest.mock('../../connector/registry');
14
+ jest.mock('../../lib/oauth');
15
+ jest.mock('../../models/dynamo/connectorSchema', () => ({
16
+ Connector: {
17
+ getProxyConfig: jest.fn()
18
+ }
19
+ }));
20
+
21
+ const contactHandler = require('../../handlers/contact');
22
+ const { UserModel } = require('../../models/userModel');
23
+ const { AccountDataModel } = require('../../models/accountDataModel');
24
+ const connectorRegistry = require('../../connector/registry');
25
+ const oauth = require('../../lib/oauth');
26
+ const { Connector } = require('../../models/dynamo/connectorSchema');
27
+ const { sequelize } = require('../../models/sequelize');
28
+
29
+ describe('Contact Handler', () => {
30
+ beforeAll(async () => {
31
+ await UserModel.sync({ force: true });
32
+ await AccountDataModel.sync({ force: true });
33
+ });
34
+
35
+ afterEach(async () => {
36
+ await UserModel.destroy({ where: {} });
37
+ await AccountDataModel.destroy({ where: {} });
38
+ jest.clearAllMocks();
39
+ });
40
+
41
+ afterAll(async () => {
42
+ await sequelize.close();
43
+ });
44
+
45
+ describe('findContact', () => {
46
+ const mockUser = {
47
+ id: 'test-user-id',
48
+ platform: 'testCRM',
49
+ accessToken: 'test-access-token',
50
+ rcAccountId: 'rc-account-123',
51
+ platformAdditionalInfo: {}
52
+ };
53
+
54
+ test('should return warning when user not found', async () => {
55
+ // Act
56
+ const result = await contactHandler.findContact({
57
+ platform: 'testCRM',
58
+ userId: 'non-existent-user',
59
+ phoneNumber: '+1234567890'
60
+ });
61
+
62
+ // Assert
63
+ expect(result.successful).toBe(false);
64
+ expect(result.returnMessage.message).toBe('Contact not found');
65
+ expect(result.returnMessage.messageType).toBe('warning');
66
+ });
67
+
68
+ test('should return warning when user has no access token', async () => {
69
+ // Arrange
70
+ await UserModel.create({
71
+ id: 'test-user-id',
72
+ platform: 'testCRM',
73
+ accessToken: null
74
+ });
75
+
76
+ // Act
77
+ const result = await contactHandler.findContact({
78
+ platform: 'testCRM',
79
+ userId: 'test-user-id',
80
+ phoneNumber: '+1234567890'
81
+ });
82
+
83
+ // Assert
84
+ expect(result.successful).toBe(false);
85
+ expect(result.returnMessage.message).toBe('Contact not found');
86
+ });
87
+
88
+ test('should return cached contact when available', async () => {
89
+ // Arrange
90
+ await UserModel.create(mockUser);
91
+
92
+ const cachedContact = [
93
+ { id: 'contact-1', name: 'Cached Contact', type: 'Contact', phone: '+1234567890' }
94
+ ];
95
+ await AccountDataModel.create({
96
+ rcAccountId: 'rc-account-123',
97
+ platformName: 'testCRM',
98
+ dataKey: 'contact-+1234567890',
99
+ data: cachedContact
100
+ });
101
+
102
+ // Act
103
+ const result = await contactHandler.findContact({
104
+ platform: 'testCRM',
105
+ userId: 'test-user-id',
106
+ phoneNumber: '+1234567890',
107
+ isForceRefreshAccountData: false
108
+ });
109
+
110
+ // Assert
111
+ expect(result.successful).toBe(true);
112
+ expect(result.contact).toEqual(cachedContact);
113
+ // Should not call platform module
114
+ expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test('should bypass cache when isForceRefreshAccountData is true', async () => {
118
+ // Arrange
119
+ await UserModel.create(mockUser);
120
+
121
+ const cachedContact = [
122
+ { id: 'contact-1', name: 'Cached Contact', type: 'Contact', phone: '+1234567890' }
123
+ ];
124
+ await AccountDataModel.create({
125
+ rcAccountId: 'rc-account-123',
126
+ platformName: 'testCRM',
127
+ dataKey: 'contact-+1234567890',
128
+ data: cachedContact
129
+ });
130
+
131
+ const freshContact = [
132
+ { id: 'contact-1', name: 'Fresh Contact', type: 'Contact', phone: '+1234567890' }
133
+ ];
134
+
135
+ const mockConnector = {
136
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
137
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
138
+ findContact: jest.fn().mockResolvedValue({
139
+ successful: true,
140
+ matchedContactInfo: freshContact
141
+ })
142
+ };
143
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
144
+
145
+ // Act
146
+ const result = await contactHandler.findContact({
147
+ platform: 'testCRM',
148
+ userId: 'test-user-id',
149
+ phoneNumber: '+1234567890',
150
+ isForceRefreshAccountData: true
151
+ });
152
+
153
+ // Assert
154
+ expect(result.successful).toBe(true);
155
+ expect(result.contact).toEqual(freshContact);
156
+ expect(mockConnector.findContact).toHaveBeenCalled();
157
+ });
158
+
159
+ test('should find contact with apiKey auth and cache result', async () => {
160
+ // Arrange
161
+ await UserModel.create(mockUser);
162
+
163
+ const matchedContact = [
164
+ { id: 'contact-new', name: 'New Contact', type: 'Contact', phone: '+9876543210' }
165
+ ];
166
+
167
+ const mockConnector = {
168
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
169
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
170
+ findContact: jest.fn().mockResolvedValue({
171
+ successful: true,
172
+ matchedContactInfo: matchedContact
173
+ })
174
+ };
175
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
176
+
177
+ // Act
178
+ const result = await contactHandler.findContact({
179
+ platform: 'testCRM',
180
+ userId: 'test-user-id',
181
+ phoneNumber: '+9876543210'
182
+ });
183
+
184
+ // Assert
185
+ expect(result.successful).toBe(true);
186
+ expect(result.contact).toEqual(matchedContact);
187
+ expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'test-access-token' });
188
+
189
+ // Verify contact was cached
190
+ const cachedData = await AccountDataModel.findOne({
191
+ where: {
192
+ rcAccountId: 'rc-account-123',
193
+ platformName: 'testCRM',
194
+ dataKey: 'contact-+9876543210'
195
+ }
196
+ });
197
+ expect(cachedData).not.toBeNull();
198
+ expect(cachedData.data).toEqual(matchedContact);
199
+ });
200
+
201
+ test('should find contact with oauth auth', async () => {
202
+ // Arrange
203
+ await UserModel.create(mockUser);
204
+
205
+ const matchedContact = [
206
+ { id: 'oauth-contact', name: 'OAuth Contact', type: 'Lead' }
207
+ ];
208
+
209
+ const mockConnector = {
210
+ getAuthType: jest.fn().mockResolvedValue('oauth'),
211
+ getOauthInfo: jest.fn().mockResolvedValue({
212
+ clientId: 'client-id',
213
+ clientSecret: 'client-secret',
214
+ accessTokenUri: 'https://token.url',
215
+ authorizationUri: 'https://auth.url'
216
+ }),
217
+ findContact: jest.fn().mockResolvedValue({
218
+ successful: true,
219
+ matchedContactInfo: matchedContact
220
+ })
221
+ };
222
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
223
+
224
+ const mockOAuthApp = {};
225
+ oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
226
+ oauth.checkAndRefreshAccessToken.mockResolvedValue({
227
+ ...mockUser,
228
+ accessToken: 'refreshed-token'
229
+ });
230
+
231
+ // Act
232
+ const result = await contactHandler.findContact({
233
+ platform: 'testCRM',
234
+ userId: 'test-user-id',
235
+ phoneNumber: '+1111111111'
236
+ });
237
+
238
+ // Assert
239
+ expect(result.successful).toBe(true);
240
+ expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
241
+ });
242
+
243
+ test('should use proxy config when proxyId is present', async () => {
244
+ // Arrange
245
+ const userWithProxy = {
246
+ ...mockUser,
247
+ platformAdditionalInfo: { proxyId: 'proxy-123' }
248
+ };
249
+ await UserModel.create(userWithProxy);
250
+
251
+ const proxyConfig = { baseUrl: 'https://proxy.example.com' };
252
+ Connector.getProxyConfig.mockResolvedValue(proxyConfig);
253
+
254
+ const matchedContact = [{ id: 'proxy-contact', name: 'Proxy Contact' }];
255
+
256
+ const mockConnector = {
257
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
258
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
259
+ findContact: jest.fn().mockResolvedValue({
260
+ successful: true,
261
+ matchedContactInfo: matchedContact
262
+ })
263
+ };
264
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
265
+
266
+ // Act
267
+ const result = await contactHandler.findContact({
268
+ platform: 'testCRM',
269
+ userId: 'test-user-id',
270
+ phoneNumber: '+2222222222'
271
+ });
272
+
273
+ // Assert
274
+ expect(result.successful).toBe(true);
275
+ expect(Connector.getProxyConfig).toHaveBeenCalledWith('proxy-123');
276
+ expect(mockConnector.findContact).toHaveBeenCalledWith(
277
+ expect.objectContaining({ proxyConfig })
278
+ );
279
+ });
280
+
281
+ test('should return warning when no contacts found', async () => {
282
+ // Arrange
283
+ await UserModel.create(mockUser);
284
+
285
+ const mockConnector = {
286
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
287
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
288
+ findContact: jest.fn().mockResolvedValue({
289
+ successful: false,
290
+ matchedContactInfo: null
291
+ })
292
+ };
293
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
294
+
295
+ // Act
296
+ const result = await contactHandler.findContact({
297
+ platform: 'testCRM',
298
+ userId: 'test-user-id',
299
+ phoneNumber: '+9999999999'
300
+ });
301
+
302
+ // Assert
303
+ expect(result.successful).toBe(false);
304
+ expect(result.returnMessage.message).toBe('Contact not found');
305
+ expect(result.returnMessage.details[0].items[0].text).toContain('+9999999999');
306
+ });
307
+
308
+ test('should return platform returnMessage when provided', async () => {
309
+ // Arrange
310
+ await UserModel.create(mockUser);
311
+
312
+ const customReturnMessage = {
313
+ message: 'Custom platform message',
314
+ messageType: 'info',
315
+ ttl: 5000
316
+ };
317
+
318
+ const mockConnector = {
319
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
320
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
321
+ findContact: jest.fn().mockResolvedValue({
322
+ successful: false,
323
+ matchedContactInfo: null,
324
+ returnMessage: customReturnMessage
325
+ })
326
+ };
327
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
328
+
329
+ // Act
330
+ const result = await contactHandler.findContact({
331
+ platform: 'testCRM',
332
+ userId: 'test-user-id',
333
+ phoneNumber: '+8888888888'
334
+ });
335
+
336
+ // Assert
337
+ expect(result.returnMessage).toEqual(customReturnMessage);
338
+ });
339
+
340
+ test('should handle 429 rate limit error', async () => {
341
+ // Arrange
342
+ await UserModel.create(mockUser);
343
+
344
+ const mockConnector = {
345
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
346
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
347
+ findContact: jest.fn().mockRejectedValue({
348
+ response: { status: 429 }
349
+ })
350
+ };
351
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
352
+
353
+ // Act
354
+ const result = await contactHandler.findContact({
355
+ platform: 'testCRM',
356
+ userId: 'test-user-id',
357
+ phoneNumber: '+7777777777'
358
+ });
359
+
360
+ // Assert
361
+ expect(result.successful).toBe(false);
362
+ expect(result.extraDataTracking.statusCode).toBe(429);
363
+ });
364
+
365
+ test('should handle 401 authorization error', async () => {
366
+ // Arrange
367
+ await UserModel.create(mockUser);
368
+
369
+ const mockConnector = {
370
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
371
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
372
+ findContact: jest.fn().mockRejectedValue({
373
+ response: { status: 401 }
374
+ })
375
+ };
376
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
377
+
378
+ // Act
379
+ const result = await contactHandler.findContact({
380
+ platform: 'testCRM',
381
+ userId: 'test-user-id',
382
+ phoneNumber: '+6666666666'
383
+ });
384
+
385
+ // Assert
386
+ expect(result.successful).toBe(false);
387
+ expect(result.extraDataTracking.statusCode).toBe(401);
388
+ });
389
+
390
+ test('should update existing cached contact', async () => {
391
+ // Arrange
392
+ await UserModel.create(mockUser);
393
+
394
+ // Create existing cached contact
395
+ await AccountDataModel.create({
396
+ rcAccountId: 'rc-account-123',
397
+ platformName: 'testCRM',
398
+ dataKey: 'contact-+5555555555',
399
+ data: [{ id: 'old-contact', name: 'Old Name' }]
400
+ });
401
+
402
+ const updatedContact = [
403
+ { id: 'old-contact', name: 'Updated Name', type: 'Contact' }
404
+ ];
405
+
406
+ const mockConnector = {
407
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
408
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
409
+ findContact: jest.fn().mockResolvedValue({
410
+ successful: true,
411
+ matchedContactInfo: updatedContact
412
+ })
413
+ };
414
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
415
+
416
+ // Act
417
+ const result = await contactHandler.findContact({
418
+ platform: 'testCRM',
419
+ userId: 'test-user-id',
420
+ phoneNumber: '+5555555555',
421
+ isForceRefreshAccountData: true
422
+ });
423
+
424
+ // Assert
425
+ expect(result.successful).toBe(true);
426
+ expect(result.contact).toEqual(updatedContact);
427
+
428
+ // Verify cache was updated
429
+ const cachedData = await AccountDataModel.findOne({
430
+ where: {
431
+ rcAccountId: 'rc-account-123',
432
+ platformName: 'testCRM',
433
+ dataKey: 'contact-+5555555555'
434
+ }
435
+ });
436
+ expect(cachedData.data).toEqual(updatedContact);
437
+ });
438
+
439
+ test('should remove cached contact when force refresh returns null', async () => {
440
+ // Arrange
441
+ await UserModel.create(mockUser);
442
+ await AccountDataModel.create({
443
+ rcAccountId: 'rc-account-123',
444
+ platformName: 'testCRM',
445
+ dataKey: 'contact-+3333333333',
446
+ data: [{ id: 'stale-contact', name: 'Stale Contact' }]
447
+ });
448
+
449
+ const mockConnector = {
450
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
451
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
452
+ findContact: jest.fn().mockResolvedValue({
453
+ successful: false,
454
+ matchedContactInfo: null
455
+ })
456
+ };
457
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
458
+
459
+ // Act
460
+ const result = await contactHandler.findContact({
461
+ platform: 'testCRM',
462
+ userId: 'test-user-id',
463
+ phoneNumber: '+3333333333',
464
+ isForceRefreshAccountData: true
465
+ });
466
+
467
+ // Assert
468
+ expect(result.successful).toBe(false);
469
+ const cachedData = await AccountDataModel.findOne({
470
+ where: {
471
+ rcAccountId: 'rc-account-123',
472
+ platformName: 'testCRM',
473
+ dataKey: 'contact-+3333333333'
474
+ }
475
+ });
476
+ expect(cachedData).toBeNull();
477
+ });
478
+
479
+ test('should remove cached contact when force refresh returns empty array', async () => {
480
+ // Arrange
481
+ await UserModel.create(mockUser);
482
+ await AccountDataModel.create({
483
+ rcAccountId: 'rc-account-123',
484
+ platformName: 'testCRM',
485
+ dataKey: 'contact-+1212121212',
486
+ data: [{ id: 'stale-contact', name: 'Stale Contact' }]
487
+ });
488
+
489
+ const mockConnector = {
490
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
491
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
492
+ findContact: jest.fn().mockResolvedValue({
493
+ successful: false,
494
+ matchedContactInfo: []
495
+ })
496
+ };
497
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
498
+
499
+ // Act
500
+ await contactHandler.findContact({
501
+ platform: 'testCRM',
502
+ userId: 'test-user-id',
503
+ phoneNumber: '+1212121212',
504
+ isForceRefreshAccountData: true
505
+ });
506
+
507
+ // Assert
508
+ const cachedData = await AccountDataModel.findOne({
509
+ where: {
510
+ rcAccountId: 'rc-account-123',
511
+ platformName: 'testCRM',
512
+ dataKey: 'contact-+1212121212'
513
+ }
514
+ });
515
+ expect(cachedData).toBeNull();
516
+ });
517
+
518
+ test('should remove cached contact when force refresh returns only new contact placeholder', async () => {
519
+ // Arrange
520
+ await UserModel.create(mockUser);
521
+ await AccountDataModel.create({
522
+ rcAccountId: 'rc-account-123',
523
+ platformName: 'testCRM',
524
+ dataKey: 'contact-+3434343434',
525
+ data: [{ id: 'stale-contact', name: 'Stale Contact' }]
526
+ });
527
+
528
+ const placeholderContact = [
529
+ { id: 'new-contact', name: 'Create Contact', isNewContact: true }
530
+ ];
531
+ const mockConnector = {
532
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
533
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
534
+ findContact: jest.fn().mockResolvedValue({
535
+ successful: false,
536
+ matchedContactInfo: placeholderContact
537
+ })
538
+ };
539
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
540
+
541
+ // Act
542
+ const result = await contactHandler.findContact({
543
+ platform: 'testCRM',
544
+ userId: 'test-user-id',
545
+ phoneNumber: '+3434343434',
546
+ isForceRefreshAccountData: true
547
+ });
548
+
549
+ // Assert
550
+ expect(result.contact).toEqual(placeholderContact);
551
+ const cachedData = await AccountDataModel.findOne({
552
+ where: {
553
+ rcAccountId: 'rc-account-123',
554
+ platformName: 'testCRM',
555
+ dataKey: 'contact-+3434343434'
556
+ }
557
+ });
558
+ expect(cachedData).toBeNull();
559
+ });
560
+
561
+ test('should keep cached contact when force refresh connector call errors', async () => {
562
+ // Arrange
563
+ await UserModel.create(mockUser);
564
+ await AccountDataModel.create({
565
+ rcAccountId: 'rc-account-123',
566
+ platformName: 'testCRM',
567
+ dataKey: 'contact-+5656565656',
568
+ data: [{ id: 'stale-contact', name: 'Stale Contact' }]
569
+ });
570
+
571
+ const mockConnector = {
572
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
573
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
574
+ findContact: jest.fn().mockRejectedValue({
575
+ response: { status: 500 }
576
+ })
577
+ };
578
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
579
+
580
+ // Act
581
+ const result = await contactHandler.findContact({
582
+ platform: 'testCRM',
583
+ userId: 'test-user-id',
584
+ phoneNumber: '+5656565656',
585
+ isForceRefreshAccountData: true
586
+ });
587
+
588
+ // Assert
589
+ expect(result.successful).toBe(false);
590
+ const cachedData = await AccountDataModel.findOne({
591
+ where: {
592
+ rcAccountId: 'rc-account-123',
593
+ platformName: 'testCRM',
594
+ dataKey: 'contact-+5656565656'
595
+ }
596
+ });
597
+ expect(cachedData).not.toBeNull();
598
+ expect(cachedData.data).toEqual([{ id: 'stale-contact', name: 'Stale Contact' }]);
599
+ });
600
+
601
+ test('should work with tracer when provided', async () => {
602
+ // Arrange
603
+ await UserModel.create(mockUser);
604
+
605
+ const mockTracer = {
606
+ trace: jest.fn(),
607
+ traceError: jest.fn()
608
+ };
609
+
610
+ const matchedContact = [{ id: 'traced-contact', name: 'Traced Contact' }];
611
+
612
+ const mockConnector = {
613
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
614
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
615
+ findContact: jest.fn().mockResolvedValue({
616
+ successful: true,
617
+ matchedContactInfo: matchedContact
618
+ })
619
+ };
620
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
621
+
622
+ // Act
623
+ const result = await contactHandler.findContact({
624
+ platform: 'testCRM',
625
+ userId: 'test-user-id',
626
+ phoneNumber: '+4444444444',
627
+ tracer: mockTracer
628
+ });
629
+
630
+ // Assert
631
+ expect(result.successful).toBe(true);
632
+ expect(mockTracer.trace).toHaveBeenCalled();
633
+ });
634
+ });
635
+
636
+ describe('createContact', () => {
637
+ const mockUser = {
638
+ id: 'test-user-id',
639
+ platform: 'testCRM',
640
+ accessToken: 'test-access-token',
641
+ platformAdditionalInfo: {}
642
+ };
643
+
644
+ test('should return error when user not found', async () => {
645
+ // Act
646
+ const result = await contactHandler.createContact({
647
+ platform: 'testCRM',
648
+ userId: 'non-existent-user',
649
+ phoneNumber: '+1234567890',
650
+ newContactName: 'New Contact',
651
+ newContactType: 'Contact'
652
+ });
653
+
654
+ // Assert
655
+ expect(result.successful).toBe(false);
656
+ expect(result.message).toBe('Contact not found');
657
+ });
658
+
659
+ test('should return error when user has no access token', async () => {
660
+ // Arrange
661
+ await UserModel.create({
662
+ id: 'test-user-id',
663
+ platform: 'testCRM',
664
+ accessToken: null
665
+ });
666
+
667
+ // Act
668
+ const result = await contactHandler.createContact({
669
+ platform: 'testCRM',
670
+ userId: 'test-user-id',
671
+ phoneNumber: '+1234567890',
672
+ newContactName: 'New Contact',
673
+ newContactType: 'Contact'
674
+ });
675
+
676
+ // Assert
677
+ expect(result.successful).toBe(false);
678
+ });
679
+
680
+ test('should successfully create contact with apiKey auth', async () => {
681
+ // Arrange
682
+ await UserModel.create(mockUser);
683
+
684
+ const createdContact = {
685
+ id: 'new-contact-123',
686
+ name: 'New Contact',
687
+ type: 'Contact',
688
+ phone: '+1234567890'
689
+ };
690
+
691
+ const mockConnector = {
692
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
693
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
694
+ createContact: jest.fn().mockResolvedValue({
695
+ contactInfo: createdContact,
696
+ returnMessage: { message: 'Contact created', messageType: 'success', ttl: 2000 }
697
+ })
698
+ };
699
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
700
+
701
+ // Act
702
+ const result = await contactHandler.createContact({
703
+ platform: 'testCRM',
704
+ userId: 'test-user-id',
705
+ phoneNumber: '+1234567890',
706
+ newContactName: 'New Contact',
707
+ newContactType: 'Contact',
708
+ additionalSubmission: {}
709
+ });
710
+
711
+ // Assert
712
+ expect(result.successful).toBe(true);
713
+ expect(result.contact).toEqual(createdContact);
714
+ expect(mockConnector.createContact).toHaveBeenCalledWith({
715
+ user: expect.any(Object),
716
+ authHeader: 'Basic base64-encoded',
717
+ phoneNumber: '+1234567890',
718
+ newContactName: 'New Contact',
719
+ newContactType: 'Contact',
720
+ additionalSubmission: {},
721
+ proxyConfig: null
722
+ });
723
+ });
724
+
725
+ test('should successfully create contact with oauth auth', async () => {
726
+ // Arrange
727
+ await UserModel.create(mockUser);
728
+
729
+ const createdContact = {
730
+ id: 'oauth-contact-123',
731
+ name: 'OAuth Contact'
732
+ };
733
+
734
+ const mockConnector = {
735
+ getAuthType: jest.fn().mockResolvedValue('oauth'),
736
+ getOauthInfo: jest.fn().mockResolvedValue({
737
+ clientId: 'client-id',
738
+ clientSecret: 'client-secret',
739
+ accessTokenUri: 'https://token.url',
740
+ authorizationUri: 'https://auth.url'
741
+ }),
742
+ createContact: jest.fn().mockResolvedValue({
743
+ contactInfo: createdContact,
744
+ returnMessage: { message: 'Created', messageType: 'success' }
745
+ })
746
+ };
747
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
748
+
749
+ oauth.getOAuthApp.mockReturnValue({});
750
+ oauth.checkAndRefreshAccessToken.mockResolvedValue({
751
+ ...mockUser,
752
+ accessToken: 'refreshed-token'
753
+ });
754
+
755
+ // Act
756
+ const result = await contactHandler.createContact({
757
+ platform: 'testCRM',
758
+ userId: 'test-user-id',
759
+ phoneNumber: '+1234567890',
760
+ newContactName: 'OAuth Contact',
761
+ newContactType: 'Lead'
762
+ });
763
+
764
+ // Assert
765
+ expect(result.successful).toBe(true);
766
+ expect(result.contact).toEqual(createdContact);
767
+ expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
768
+ });
769
+
770
+ test('should return unsuccessful when platform returns null contactInfo', async () => {
771
+ // Arrange
772
+ await UserModel.create(mockUser);
773
+
774
+ const mockConnector = {
775
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
776
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
777
+ createContact: jest.fn().mockResolvedValue({
778
+ contactInfo: null,
779
+ returnMessage: { message: 'Failed to create', messageType: 'error' }
780
+ })
781
+ };
782
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
783
+
784
+ // Act
785
+ const result = await contactHandler.createContact({
786
+ platform: 'testCRM',
787
+ userId: 'test-user-id',
788
+ phoneNumber: '+1234567890',
789
+ newContactName: 'Failed Contact',
790
+ newContactType: 'Contact'
791
+ });
792
+
793
+ // Assert
794
+ expect(result.successful).toBe(false);
795
+ expect(result.returnMessage.message).toBe('Failed to create');
796
+ });
797
+
798
+ test('should handle 429 rate limit error', async () => {
799
+ // Arrange
800
+ await UserModel.create(mockUser);
801
+
802
+ const mockConnector = {
803
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
804
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
805
+ createContact: jest.fn().mockRejectedValue({
806
+ response: { status: 429 }
807
+ })
808
+ };
809
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
810
+
811
+ // Act
812
+ const result = await contactHandler.createContact({
813
+ platform: 'testCRM',
814
+ userId: 'test-user-id',
815
+ phoneNumber: '+1234567890',
816
+ newContactName: 'Rate Limited',
817
+ newContactType: 'Contact'
818
+ });
819
+
820
+ // Assert
821
+ expect(result.successful).toBe(false);
822
+ });
823
+
824
+ test('should handle 401 authorization error', async () => {
825
+ // Arrange
826
+ await UserModel.create(mockUser);
827
+
828
+ const mockConnector = {
829
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
830
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
831
+ createContact: jest.fn().mockRejectedValue({
832
+ response: { status: 401 }
833
+ })
834
+ };
835
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
836
+
837
+ // Act
838
+ const result = await contactHandler.createContact({
839
+ platform: 'testCRM',
840
+ userId: 'test-user-id',
841
+ phoneNumber: '+1234567890',
842
+ newContactName: 'Unauthorized',
843
+ newContactType: 'Contact'
844
+ });
845
+
846
+ // Assert
847
+ expect(result.successful).toBe(false);
848
+ expect(result.extraDataTracking.statusCode).toBe(401);
849
+ });
850
+
851
+ test('should handle generic errors', async () => {
852
+ // Arrange
853
+ await UserModel.create(mockUser);
854
+
855
+ const mockConnector = {
856
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
857
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
858
+ createContact: jest.fn().mockRejectedValue(new Error('Unknown error'))
859
+ };
860
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
861
+
862
+ // Act
863
+ const result = await contactHandler.createContact({
864
+ platform: 'testCRM',
865
+ userId: 'test-user-id',
866
+ phoneNumber: '+1234567890',
867
+ newContactName: 'Error Contact',
868
+ newContactType: 'Contact'
869
+ });
870
+
871
+ // Assert
872
+ expect(result.successful).toBe(false);
873
+ expect(result.returnMessage.message).toBe('Error creating contact');
874
+ });
875
+ });
876
+
877
+ describe('findContactWithName', () => {
878
+ const mockUser = {
879
+ id: 'test-user-id',
880
+ platform: 'testCRM',
881
+ accessToken: 'test-access-token',
882
+ platformAdditionalInfo: {}
883
+ };
884
+
885
+ test('should return warning when user not found', async () => {
886
+ // Act
887
+ const result = await contactHandler.findContactWithName({
888
+ platform: 'testCRM',
889
+ userId: 'non-existent-user',
890
+ name: 'John Doe'
891
+ });
892
+
893
+ // Assert
894
+ expect(result.successful).toBe(false);
895
+ expect(result.returnMessage.message).toContain('No contact found with name');
896
+ });
897
+
898
+ test('should find contact by name with apiKey auth', async () => {
899
+ // Arrange
900
+ await UserModel.create(mockUser);
901
+
902
+ const matchedContacts = [
903
+ { id: 'contact-1', name: 'John Doe', type: 'Contact' }
904
+ ];
905
+
906
+ const mockConnector = {
907
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
908
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
909
+ findContactWithName: jest.fn().mockResolvedValue({
910
+ successful: true,
911
+ matchedContactInfo: matchedContacts
912
+ })
913
+ };
914
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
915
+
916
+ // Act
917
+ const result = await contactHandler.findContactWithName({
918
+ platform: 'testCRM',
919
+ userId: 'test-user-id',
920
+ name: 'John Doe'
921
+ });
922
+
923
+ // Assert
924
+ expect(result.successful).toBe(true);
925
+ expect(result.contact).toEqual(matchedContacts);
926
+ });
927
+
928
+ test('should return warning when no contacts found by name', async () => {
929
+ // Arrange
930
+ await UserModel.create(mockUser);
931
+
932
+ const mockConnector = {
933
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
934
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
935
+ findContactWithName: jest.fn().mockResolvedValue({
936
+ successful: false,
937
+ matchedContactInfo: null
938
+ })
939
+ };
940
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
941
+
942
+ // Act
943
+ const result = await contactHandler.findContactWithName({
944
+ platform: 'testCRM',
945
+ userId: 'test-user-id',
946
+ name: 'Unknown Person'
947
+ });
948
+
949
+ // Assert
950
+ expect(result.successful).toBe(false);
951
+ expect(result.returnMessage.message).toContain('No contact found with name');
952
+ });
953
+
954
+ test('should handle 429 rate limit error', async () => {
955
+ // Arrange
956
+ await UserModel.create(mockUser);
957
+
958
+ const mockConnector = {
959
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
960
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
961
+ findContactWithName: jest.fn().mockRejectedValue({
962
+ response: { status: 429 }
963
+ })
964
+ };
965
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
966
+
967
+ // Act
968
+ const result = await contactHandler.findContactWithName({
969
+ platform: 'testCRM',
970
+ userId: 'test-user-id',
971
+ name: 'Rate Limited'
972
+ });
973
+
974
+ // Assert
975
+ expect(result.successful).toBe(false);
976
+ });
977
+
978
+ test('should use proxy config when proxyId is present', async () => {
979
+ // Arrange
980
+ const userWithProxy = {
981
+ ...mockUser,
982
+ platformAdditionalInfo: { proxyId: 'proxy-123' }
983
+ };
984
+ await UserModel.create(userWithProxy);
985
+
986
+ const proxyConfig = { baseUrl: 'https://proxy.example.com' };
987
+ Connector.getProxyConfig.mockResolvedValue(proxyConfig);
988
+
989
+ const matchedContacts = [{ id: 'proxy-contact', name: 'Proxy Contact' }];
990
+
991
+ const mockConnector = {
992
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
993
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
994
+ findContactWithName: jest.fn().mockResolvedValue({
995
+ successful: true,
996
+ matchedContactInfo: matchedContacts
997
+ })
998
+ };
999
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
1000
+
1001
+ // Act
1002
+ const result = await contactHandler.findContactWithName({
1003
+ platform: 'testCRM',
1004
+ userId: 'test-user-id',
1005
+ name: 'Proxy Contact'
1006
+ });
1007
+
1008
+ // Assert
1009
+ expect(result.successful).toBe(true);
1010
+ expect(Connector.getProxyConfig).toHaveBeenCalledWith('proxy-123');
1011
+ });
1012
+ });
1013
+ });
1014
+