@app-connect/core 1.7.25 → 1.7.27

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 (138) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -31
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -164
  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 -116
  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 +320 -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 +990 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/managedOAuth.js +247 -0
  25. package/handlers/plugin.js +208 -208
  26. package/handlers/user.js +142 -142
  27. package/index.js +3162 -2652
  28. package/jest.config.js +56 -56
  29. package/lib/analytics.js +54 -54
  30. package/lib/authSession.js +109 -109
  31. package/lib/cacheCleanup.js +21 -0
  32. package/lib/callLogComposer.js +898 -898
  33. package/lib/callLogLookup.js +34 -0
  34. package/lib/constants.js +8 -8
  35. package/lib/debugTracer.js +177 -177
  36. package/lib/encode.js +30 -30
  37. package/lib/errorHandler.js +218 -206
  38. package/lib/generalErrorMessage.js +41 -41
  39. package/lib/jwt.js +18 -18
  40. package/lib/logger.js +190 -190
  41. package/lib/migrateCallLogsSchema.js +116 -0
  42. package/lib/ringcentral.js +266 -266
  43. package/lib/s3ErrorLogReport.js +65 -65
  44. package/lib/sharedSMSComposer.js +471 -471
  45. package/lib/util.js +67 -67
  46. package/mcp/README.md +412 -395
  47. package/mcp/lib/validator.js +91 -91
  48. package/mcp/mcpHandler.js +425 -425
  49. package/mcp/tools/cancelAppointment.js +101 -0
  50. package/mcp/tools/checkAuthStatus.js +105 -105
  51. package/mcp/tools/confirmAppointment.js +101 -0
  52. package/mcp/tools/createAppointment.js +157 -0
  53. package/mcp/tools/createCallLog.js +327 -316
  54. package/mcp/tools/createContact.js +117 -117
  55. package/mcp/tools/createMessageLog.js +287 -287
  56. package/mcp/tools/doAuth.js +60 -60
  57. package/mcp/tools/findContactByName.js +93 -93
  58. package/mcp/tools/findContactByPhone.js +101 -101
  59. package/mcp/tools/getCallLog.js +111 -102
  60. package/mcp/tools/getGoogleFilePicker.js +99 -99
  61. package/mcp/tools/getHelp.js +43 -43
  62. package/mcp/tools/getPublicConnectors.js +94 -94
  63. package/mcp/tools/getSessionInfo.js +90 -90
  64. package/mcp/tools/index.js +51 -41
  65. package/mcp/tools/listAppointments.js +163 -0
  66. package/mcp/tools/logout.js +99 -96
  67. package/mcp/tools/rcGetCallLogs.js +65 -65
  68. package/mcp/tools/updateAppointment.js +154 -0
  69. package/mcp/tools/updateCallLog.js +130 -126
  70. package/mcp/ui/App/App.tsx +358 -358
  71. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  72. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  73. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  74. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  75. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  76. package/mcp/ui/App/lib/callTool.ts +130 -130
  77. package/mcp/ui/App/lib/debugLog.ts +41 -41
  78. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  79. package/mcp/ui/App/main.css +5 -5
  80. package/mcp/ui/App/root.tsx +13 -13
  81. package/mcp/ui/index.html +13 -13
  82. package/mcp/ui/package-lock.json +6356 -6356
  83. package/mcp/ui/package.json +25 -25
  84. package/mcp/ui/tsconfig.json +26 -26
  85. package/mcp/ui/vite.config.ts +16 -16
  86. package/models/accountDataModel.js +33 -33
  87. package/models/adminConfigModel.js +35 -35
  88. package/models/cacheModel.js +30 -26
  89. package/models/callDownListModel.js +34 -34
  90. package/models/callLogModel.js +33 -27
  91. package/models/dynamo/connectorSchema.js +146 -146
  92. package/models/dynamo/lockSchema.js +24 -24
  93. package/models/dynamo/noteCacheSchema.js +29 -29
  94. package/models/llmSessionModel.js +17 -17
  95. package/models/messageLogModel.js +25 -25
  96. package/models/sequelize.js +16 -16
  97. package/models/userModel.js +45 -45
  98. package/package.json +1 -1
  99. package/releaseNotes.json +1101 -1081
  100. package/test/connector/proxy/engine.test.js +126 -126
  101. package/test/connector/proxy/index.test.js +279 -279
  102. package/test/connector/proxy/sample.json +161 -161
  103. package/test/connector/registry.test.js +415 -415
  104. package/test/handlers/admin.test.js +616 -616
  105. package/test/handlers/auth.test.js +1092 -1018
  106. package/test/handlers/contact.test.js +1014 -1014
  107. package/test/handlers/log.test.js +1298 -1160
  108. package/test/handlers/managedAuth.test.js +457 -457
  109. package/test/handlers/plugin.test.js +380 -380
  110. package/test/index.test.js +105 -105
  111. package/test/lib/cacheCleanup.test.js +42 -0
  112. package/test/lib/callLogComposer.test.js +1231 -1231
  113. package/test/lib/debugTracer.test.js +328 -328
  114. package/test/lib/jwt.test.js +176 -176
  115. package/test/lib/logger.test.js +206 -206
  116. package/test/lib/oauth.test.js +359 -359
  117. package/test/lib/ringcentral.test.js +467 -467
  118. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  119. package/test/lib/util.test.js +329 -329
  120. package/test/mcp/tools/checkAuthStatus.test.js +83 -83
  121. package/test/mcp/tools/createCallLog.test.js +436 -436
  122. package/test/mcp/tools/createContact.test.js +58 -58
  123. package/test/mcp/tools/createMessageLog.test.js +595 -595
  124. package/test/mcp/tools/doAuth.test.js +113 -113
  125. package/test/mcp/tools/findContactByName.test.js +275 -275
  126. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  127. package/test/mcp/tools/getCallLog.test.js +298 -298
  128. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  129. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  130. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  131. package/test/mcp/tools/logout.test.js +233 -233
  132. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  133. package/test/mcp/tools/updateCallLog.test.js +360 -360
  134. package/test/models/accountDataModel.test.js +98 -98
  135. package/test/models/dynamo/connectorSchema.test.js +189 -189
  136. package/test/models/models.test.js +568 -539
  137. package/test/routes/managedAuthRoutes.test.js +235 -104
  138. 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
+