@app-connect/core 1.7.5 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/admin.js +29 -17
- package/handlers/auth.js +10 -4
- package/handlers/calldown.js +33 -0
- package/handlers/contact.js +54 -3
- package/handlers/log.js +4 -4
- package/index.js +377 -163
- package/lib/debugTracer.js +159 -0
- package/lib/oauth.js +26 -18
- package/lib/ringcentral.js +2 -2
- package/lib/s3ErrorLogReport.js +66 -0
- package/models/accountDataModel.js +34 -0
- package/package.json +70 -67
- package/releaseNotes.json +68 -0
- package/test/connector/registry.test.js +145 -0
- package/test/handlers/admin.test.js +583 -0
- package/test/handlers/auth.test.js +355 -0
- package/test/handlers/contact.test.js +852 -0
- package/test/handlers/log.test.js +868 -0
- package/test/lib/callLogComposer.test.js +1231 -0
- package/test/lib/debugTracer.test.js +328 -0
- package/test/lib/oauth.test.js +359 -0
- package/test/lib/ringcentral.test.js +473 -0
- package/test/lib/util.test.js +282 -0
- package/test/models/accountDataModel.test.js +98 -0
- package/test/models/dynamo/connectorSchema.test.js +189 -0
- package/test/models/models.test.js +539 -0
- package/test/setup.js +176 -176
|
@@ -0,0 +1,868 @@
|
|
|
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('../../lib/callLogComposer');
|
|
16
|
+
jest.mock('../../models/dynamo/noteCacheSchema', () => ({
|
|
17
|
+
NoteCache: {
|
|
18
|
+
get: jest.fn(),
|
|
19
|
+
create: jest.fn()
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('../../models/dynamo/connectorSchema', () => ({
|
|
23
|
+
Connector: {
|
|
24
|
+
getProxyConfig: jest.fn()
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const logHandler = require('../../handlers/log');
|
|
29
|
+
const { CallLogModel } = require('../../models/callLogModel');
|
|
30
|
+
const { MessageLogModel } = require('../../models/messageLogModel');
|
|
31
|
+
const { UserModel } = require('../../models/userModel');
|
|
32
|
+
const connectorRegistry = require('../../connector/registry');
|
|
33
|
+
const oauth = require('../../lib/oauth');
|
|
34
|
+
const { composeCallLog } = require('../../lib/callLogComposer');
|
|
35
|
+
const { NoteCache } = require('../../models/dynamo/noteCacheSchema');
|
|
36
|
+
const { sequelize } = require('../../models/sequelize');
|
|
37
|
+
|
|
38
|
+
describe('Log Handler', () => {
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
await CallLogModel.sync({ force: true });
|
|
41
|
+
await MessageLogModel.sync({ force: true });
|
|
42
|
+
await UserModel.sync({ force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await CallLogModel.destroy({ where: {} });
|
|
47
|
+
await MessageLogModel.destroy({ where: {} });
|
|
48
|
+
await UserModel.destroy({ where: {} });
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await sequelize.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('createCallLog', () => {
|
|
57
|
+
const mockUser = {
|
|
58
|
+
id: 'test-user-id',
|
|
59
|
+
platform: 'testCRM',
|
|
60
|
+
accessToken: 'test-access-token',
|
|
61
|
+
platformAdditionalInfo: {}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const mockIncomingData = {
|
|
65
|
+
logInfo: {
|
|
66
|
+
sessionId: 'session-123',
|
|
67
|
+
telephonySessionId: 'tel-session-123',
|
|
68
|
+
id: 'call-id-123',
|
|
69
|
+
direction: 'Outbound',
|
|
70
|
+
startTime: new Date().toISOString(),
|
|
71
|
+
duration: 120,
|
|
72
|
+
result: 'Completed',
|
|
73
|
+
from: { phoneNumber: '+1234567890' },
|
|
74
|
+
to: { phoneNumber: '+0987654321' },
|
|
75
|
+
recording: { link: 'https://recording.link' }
|
|
76
|
+
},
|
|
77
|
+
contactId: 'contact-123',
|
|
78
|
+
contactType: 'Contact',
|
|
79
|
+
contactName: 'Test Contact',
|
|
80
|
+
note: 'Test note',
|
|
81
|
+
aiNote: '',
|
|
82
|
+
transcript: '',
|
|
83
|
+
additionalSubmission: {}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
test('should return warning when call log already exists for session', async () => {
|
|
87
|
+
// Arrange
|
|
88
|
+
await CallLogModel.create({
|
|
89
|
+
id: 'existing-log',
|
|
90
|
+
sessionId: 'session-123',
|
|
91
|
+
platform: 'testCRM',
|
|
92
|
+
thirdPartyLogId: 'third-party-123',
|
|
93
|
+
userId: 'test-user-id'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Act
|
|
97
|
+
const result = await logHandler.createCallLog({
|
|
98
|
+
platform: 'testCRM',
|
|
99
|
+
userId: 'test-user-id',
|
|
100
|
+
incomingData: mockIncomingData,
|
|
101
|
+
hashedAccountId: 'hashed-123',
|
|
102
|
+
isFromSSCL: false
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
expect(result.successful).toBe(false);
|
|
107
|
+
expect(result.returnMessage.messageType).toBe('warning');
|
|
108
|
+
expect(result.returnMessage.message).toContain('Existing log for session');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should return warning when user not found', async () => {
|
|
112
|
+
// Act
|
|
113
|
+
const result = await logHandler.createCallLog({
|
|
114
|
+
platform: 'testCRM',
|
|
115
|
+
userId: 'non-existent-user',
|
|
116
|
+
incomingData: mockIncomingData,
|
|
117
|
+
hashedAccountId: 'hashed-123',
|
|
118
|
+
isFromSSCL: false
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(result.successful).toBe(false);
|
|
123
|
+
expect(result.returnMessage.message).toBe('User not found');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should return warning when user has no access token', async () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
await UserModel.create({
|
|
129
|
+
id: 'test-user-id',
|
|
130
|
+
platform: 'testCRM',
|
|
131
|
+
accessToken: null
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
const result = await logHandler.createCallLog({
|
|
136
|
+
platform: 'testCRM',
|
|
137
|
+
userId: 'test-user-id',
|
|
138
|
+
incomingData: mockIncomingData,
|
|
139
|
+
hashedAccountId: 'hashed-123',
|
|
140
|
+
isFromSSCL: false
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
expect(result.successful).toBe(false);
|
|
145
|
+
expect(result.returnMessage.message).toBe('User not found');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should return warning when contact not found', async () => {
|
|
149
|
+
// Arrange
|
|
150
|
+
await UserModel.create(mockUser);
|
|
151
|
+
|
|
152
|
+
const mockConnector = {
|
|
153
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
154
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
155
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
156
|
+
createCallLog: jest.fn()
|
|
157
|
+
};
|
|
158
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
159
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
160
|
+
|
|
161
|
+
const incomingDataNoContact = {
|
|
162
|
+
...mockIncomingData,
|
|
163
|
+
contactId: null
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const result = await logHandler.createCallLog({
|
|
168
|
+
platform: 'testCRM',
|
|
169
|
+
userId: 'test-user-id',
|
|
170
|
+
incomingData: incomingDataNoContact,
|
|
171
|
+
hashedAccountId: 'hashed-123',
|
|
172
|
+
isFromSSCL: false
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Assert
|
|
176
|
+
expect(result.successful).toBe(false);
|
|
177
|
+
expect(result.returnMessage.message).toContain('Contact not found for number');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should successfully create call log with apiKey auth', async () => {
|
|
181
|
+
// Arrange
|
|
182
|
+
await UserModel.create(mockUser);
|
|
183
|
+
|
|
184
|
+
const mockConnector = {
|
|
185
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
186
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
187
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
188
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
189
|
+
logId: 'new-log-123',
|
|
190
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
191
|
+
})
|
|
192
|
+
};
|
|
193
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
194
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
195
|
+
|
|
196
|
+
// Act
|
|
197
|
+
const result = await logHandler.createCallLog({
|
|
198
|
+
platform: 'testCRM',
|
|
199
|
+
userId: 'test-user-id',
|
|
200
|
+
incomingData: mockIncomingData,
|
|
201
|
+
hashedAccountId: 'hashed-123',
|
|
202
|
+
isFromSSCL: false
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Assert
|
|
206
|
+
expect(result.successful).toBe(true);
|
|
207
|
+
expect(result.logId).toBe('new-log-123');
|
|
208
|
+
expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'test-access-token' });
|
|
209
|
+
expect(mockConnector.createCallLog).toHaveBeenCalled();
|
|
210
|
+
|
|
211
|
+
// Verify call log was saved to database
|
|
212
|
+
const savedLog = await CallLogModel.findOne({ where: { sessionId: 'session-123' } });
|
|
213
|
+
expect(savedLog).not.toBeNull();
|
|
214
|
+
expect(savedLog.thirdPartyLogId).toBe('new-log-123');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('should successfully create call log with oauth auth', async () => {
|
|
218
|
+
// Arrange
|
|
219
|
+
const oauthUser = { ...mockUser };
|
|
220
|
+
await UserModel.create(oauthUser);
|
|
221
|
+
|
|
222
|
+
const mockConnector = {
|
|
223
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
224
|
+
getOauthInfo: jest.fn().mockResolvedValue({
|
|
225
|
+
clientId: 'client-id',
|
|
226
|
+
clientSecret: 'client-secret',
|
|
227
|
+
accessTokenUri: 'https://token.url',
|
|
228
|
+
authorizationUri: 'https://auth.url'
|
|
229
|
+
}),
|
|
230
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
231
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
232
|
+
logId: 'oauth-log-123',
|
|
233
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
234
|
+
})
|
|
235
|
+
};
|
|
236
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
237
|
+
|
|
238
|
+
const mockOAuthApp = {};
|
|
239
|
+
oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
|
|
240
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValue({
|
|
241
|
+
...oauthUser,
|
|
242
|
+
accessToken: 'refreshed-token'
|
|
243
|
+
});
|
|
244
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
245
|
+
|
|
246
|
+
// Act
|
|
247
|
+
const result = await logHandler.createCallLog({
|
|
248
|
+
platform: 'testCRM',
|
|
249
|
+
userId: 'test-user-id',
|
|
250
|
+
incomingData: mockIncomingData,
|
|
251
|
+
hashedAccountId: 'hashed-123',
|
|
252
|
+
isFromSSCL: false
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Assert
|
|
256
|
+
expect(result.successful).toBe(true);
|
|
257
|
+
expect(result.logId).toBe('oauth-log-123');
|
|
258
|
+
expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('should use cached note when USE_CACHE is enabled and isFromSSCL', async () => {
|
|
262
|
+
// Arrange
|
|
263
|
+
process.env.USE_CACHE = 'true';
|
|
264
|
+
await UserModel.create(mockUser);
|
|
265
|
+
|
|
266
|
+
NoteCache.get.mockResolvedValue({ note: 'Cached note' });
|
|
267
|
+
|
|
268
|
+
const mockConnector = {
|
|
269
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
270
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
271
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
272
|
+
createCallLog: jest.fn().mockResolvedValue({
|
|
273
|
+
logId: 'cached-log-123',
|
|
274
|
+
returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
|
|
275
|
+
})
|
|
276
|
+
};
|
|
277
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
278
|
+
composeCallLog.mockReturnValue('Composed log details');
|
|
279
|
+
|
|
280
|
+
// Act
|
|
281
|
+
const result = await logHandler.createCallLog({
|
|
282
|
+
platform: 'testCRM',
|
|
283
|
+
userId: 'test-user-id',
|
|
284
|
+
incomingData: mockIncomingData,
|
|
285
|
+
hashedAccountId: 'hashed-123',
|
|
286
|
+
isFromSSCL: true
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Assert
|
|
290
|
+
expect(result.successful).toBe(true);
|
|
291
|
+
expect(NoteCache.get).toHaveBeenCalledWith({ sessionId: 'session-123' });
|
|
292
|
+
|
|
293
|
+
// Clean up
|
|
294
|
+
delete process.env.USE_CACHE;
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('getCallLog', () => {
|
|
299
|
+
test('should return error when user not found', async () => {
|
|
300
|
+
// Act
|
|
301
|
+
const result = await logHandler.getCallLog({
|
|
302
|
+
userId: 'non-existent-user',
|
|
303
|
+
sessionIds: 'session-1,session-2',
|
|
304
|
+
platform: 'testCRM',
|
|
305
|
+
requireDetails: false
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Assert
|
|
309
|
+
expect(result.successful).toBe(false);
|
|
310
|
+
expect(result.message).toBe('Contact not found');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('should return error when no session IDs provided', async () => {
|
|
314
|
+
// Arrange
|
|
315
|
+
await UserModel.create({
|
|
316
|
+
id: 'test-user-id',
|
|
317
|
+
platform: 'testCRM',
|
|
318
|
+
accessToken: 'test-token'
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Act
|
|
322
|
+
const result = await logHandler.getCallLog({
|
|
323
|
+
userId: 'test-user-id',
|
|
324
|
+
sessionIds: null,
|
|
325
|
+
platform: 'testCRM',
|
|
326
|
+
requireDetails: false
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Assert
|
|
330
|
+
expect(result.successful).toBe(false);
|
|
331
|
+
expect(result.message).toBe('No session IDs provided');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('should return matched logs without details', async () => {
|
|
335
|
+
// Arrange
|
|
336
|
+
await UserModel.create({
|
|
337
|
+
id: 'test-user-id',
|
|
338
|
+
platform: 'testCRM',
|
|
339
|
+
accessToken: 'test-token'
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await CallLogModel.create({
|
|
343
|
+
id: 'call-1',
|
|
344
|
+
sessionId: 'session-1',
|
|
345
|
+
platform: 'testCRM',
|
|
346
|
+
thirdPartyLogId: 'log-1',
|
|
347
|
+
userId: 'test-user-id'
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Act
|
|
351
|
+
const result = await logHandler.getCallLog({
|
|
352
|
+
userId: 'test-user-id',
|
|
353
|
+
sessionIds: 'session-1,session-2',
|
|
354
|
+
platform: 'testCRM',
|
|
355
|
+
requireDetails: false
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Assert
|
|
359
|
+
expect(result.successful).toBe(true);
|
|
360
|
+
expect(result.logs).toHaveLength(2);
|
|
361
|
+
expect(result.logs[0]).toEqual({
|
|
362
|
+
sessionId: 'session-1',
|
|
363
|
+
matched: true,
|
|
364
|
+
logId: 'log-1'
|
|
365
|
+
});
|
|
366
|
+
expect(result.logs[1]).toEqual({
|
|
367
|
+
sessionId: 'session-2',
|
|
368
|
+
matched: false
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should return matched logs with details when requireDetails is true', async () => {
|
|
373
|
+
// Arrange
|
|
374
|
+
await UserModel.create({
|
|
375
|
+
id: 'test-user-id',
|
|
376
|
+
platform: 'testCRM',
|
|
377
|
+
accessToken: 'test-token',
|
|
378
|
+
platformAdditionalInfo: {}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await CallLogModel.create({
|
|
382
|
+
id: 'call-1',
|
|
383
|
+
sessionId: 'session-1',
|
|
384
|
+
platform: 'testCRM',
|
|
385
|
+
thirdPartyLogId: 'log-1',
|
|
386
|
+
userId: 'test-user-id',
|
|
387
|
+
contactId: 'contact-1'
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const mockConnector = {
|
|
391
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
392
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
393
|
+
getCallLog: jest.fn().mockResolvedValue({
|
|
394
|
+
callLogInfo: { subject: 'Test Call', note: 'Test note' },
|
|
395
|
+
returnMessage: { message: 'Success', messageType: 'success' }
|
|
396
|
+
})
|
|
397
|
+
};
|
|
398
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
399
|
+
|
|
400
|
+
// Act
|
|
401
|
+
const result = await logHandler.getCallLog({
|
|
402
|
+
userId: 'test-user-id',
|
|
403
|
+
sessionIds: 'session-1',
|
|
404
|
+
platform: 'testCRM',
|
|
405
|
+
requireDetails: true
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Assert
|
|
409
|
+
expect(result.successful).toBe(true);
|
|
410
|
+
expect(result.logs).toHaveLength(1);
|
|
411
|
+
expect(result.logs[0].matched).toBe(true);
|
|
412
|
+
expect(result.logs[0].logData).toEqual({ subject: 'Test Call', note: 'Test note' });
|
|
413
|
+
expect(mockConnector.getCallLog).toHaveBeenCalled();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('should limit session IDs to 5', async () => {
|
|
417
|
+
// Arrange
|
|
418
|
+
await UserModel.create({
|
|
419
|
+
id: 'test-user-id',
|
|
420
|
+
platform: 'testCRM',
|
|
421
|
+
accessToken: 'test-token'
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const sessionIds = 'session-1,session-2,session-3,session-4,session-5,session-6,session-7';
|
|
425
|
+
|
|
426
|
+
// Act
|
|
427
|
+
const result = await logHandler.getCallLog({
|
|
428
|
+
userId: 'test-user-id',
|
|
429
|
+
sessionIds,
|
|
430
|
+
platform: 'testCRM',
|
|
431
|
+
requireDetails: false
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Assert
|
|
435
|
+
expect(result.successful).toBe(true);
|
|
436
|
+
expect(result.logs).toHaveLength(5);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('should skip session ID 0', async () => {
|
|
440
|
+
// Arrange
|
|
441
|
+
await UserModel.create({
|
|
442
|
+
id: 'test-user-id',
|
|
443
|
+
platform: 'testCRM',
|
|
444
|
+
accessToken: 'test-token',
|
|
445
|
+
platformAdditionalInfo: {}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const mockConnector = {
|
|
449
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
450
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
451
|
+
getCallLog: jest.fn()
|
|
452
|
+
};
|
|
453
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
454
|
+
|
|
455
|
+
// Act
|
|
456
|
+
const result = await logHandler.getCallLog({
|
|
457
|
+
userId: 'test-user-id',
|
|
458
|
+
sessionIds: '0,session-1',
|
|
459
|
+
platform: 'testCRM',
|
|
460
|
+
requireDetails: true
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Assert
|
|
464
|
+
expect(result.successful).toBe(true);
|
|
465
|
+
expect(result.logs[0]).toEqual({ sessionId: '0', matched: false });
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('updateCallLog', () => {
|
|
470
|
+
test('should return unsuccessful when no existing call log found', async () => {
|
|
471
|
+
// Act
|
|
472
|
+
const result = await logHandler.updateCallLog({
|
|
473
|
+
platform: 'testCRM',
|
|
474
|
+
userId: 'test-user-id',
|
|
475
|
+
incomingData: { sessionId: 'non-existent-session' },
|
|
476
|
+
hashedAccountId: 'hashed-123',
|
|
477
|
+
isFromSSCL: false
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Assert
|
|
481
|
+
expect(result.successful).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('should return error when user not found for update', async () => {
|
|
485
|
+
// Arrange
|
|
486
|
+
await CallLogModel.create({
|
|
487
|
+
id: 'call-1',
|
|
488
|
+
sessionId: 'session-1',
|
|
489
|
+
platform: 'testCRM',
|
|
490
|
+
thirdPartyLogId: 'log-1',
|
|
491
|
+
userId: 'test-user-id'
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Act
|
|
495
|
+
const result = await logHandler.updateCallLog({
|
|
496
|
+
platform: 'testCRM',
|
|
497
|
+
userId: 'non-existent-user',
|
|
498
|
+
incomingData: { sessionId: 'session-1' },
|
|
499
|
+
hashedAccountId: 'hashed-123',
|
|
500
|
+
isFromSSCL: false
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Assert
|
|
504
|
+
expect(result.successful).toBe(false);
|
|
505
|
+
expect(result.message).toBe('Contact not found');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('should successfully update call log', async () => {
|
|
509
|
+
// Arrange
|
|
510
|
+
await UserModel.create({
|
|
511
|
+
id: 'test-user-id',
|
|
512
|
+
platform: 'testCRM',
|
|
513
|
+
accessToken: 'test-token',
|
|
514
|
+
platformAdditionalInfo: {}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await CallLogModel.create({
|
|
518
|
+
id: 'call-1',
|
|
519
|
+
sessionId: 'session-1',
|
|
520
|
+
platform: 'testCRM',
|
|
521
|
+
thirdPartyLogId: 'log-1',
|
|
522
|
+
userId: 'test-user-id',
|
|
523
|
+
contactId: 'contact-1'
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const mockConnector = {
|
|
527
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
528
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
529
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
530
|
+
getCallLog: jest.fn().mockResolvedValue({
|
|
531
|
+
callLogInfo: { fullBody: 'Existing body', note: 'Existing note' }
|
|
532
|
+
}),
|
|
533
|
+
updateCallLog: jest.fn().mockResolvedValue({
|
|
534
|
+
updatedNote: 'Updated note',
|
|
535
|
+
returnMessage: { message: 'Updated', messageType: 'success', ttl: 2000 }
|
|
536
|
+
})
|
|
537
|
+
};
|
|
538
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
539
|
+
composeCallLog.mockReturnValue('Updated composed log');
|
|
540
|
+
|
|
541
|
+
const incomingData = {
|
|
542
|
+
sessionId: 'session-1',
|
|
543
|
+
note: 'Updated note',
|
|
544
|
+
subject: 'Updated subject',
|
|
545
|
+
startTime: new Date().toISOString(),
|
|
546
|
+
duration: 180,
|
|
547
|
+
result: 'Completed'
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Act
|
|
551
|
+
const result = await logHandler.updateCallLog({
|
|
552
|
+
platform: 'testCRM',
|
|
553
|
+
userId: 'test-user-id',
|
|
554
|
+
incomingData,
|
|
555
|
+
hashedAccountId: 'hashed-123',
|
|
556
|
+
isFromSSCL: false
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Assert
|
|
560
|
+
expect(result.successful).toBe(true);
|
|
561
|
+
expect(result.logId).toBe('log-1');
|
|
562
|
+
expect(result.updatedNote).toBe('Updated note');
|
|
563
|
+
expect(mockConnector.updateCallLog).toHaveBeenCalled();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('createMessageLog', () => {
|
|
568
|
+
test('should return warning when no messages to log', async () => {
|
|
569
|
+
// Act
|
|
570
|
+
const result = await logHandler.createMessageLog({
|
|
571
|
+
platform: 'testCRM',
|
|
572
|
+
userId: 'test-user-id',
|
|
573
|
+
incomingData: {
|
|
574
|
+
logInfo: { messages: [] }
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Assert
|
|
579
|
+
expect(result.successful).toBe(false);
|
|
580
|
+
expect(result.returnMessage.message).toBe('No message to log.');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('should return warning when user not found', async () => {
|
|
584
|
+
// Act
|
|
585
|
+
const result = await logHandler.createMessageLog({
|
|
586
|
+
platform: 'testCRM',
|
|
587
|
+
userId: 'non-existent-user',
|
|
588
|
+
incomingData: {
|
|
589
|
+
logInfo: {
|
|
590
|
+
messages: [{ id: 'msg-1', subject: 'Test', creationTime: new Date() }],
|
|
591
|
+
correspondents: [{ phoneNumber: '+1234567890' }]
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Assert
|
|
597
|
+
expect(result.successful).toBe(false);
|
|
598
|
+
expect(result.returnMessage.message).toBe('Contact not found');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('should return warning when contact not found', async () => {
|
|
602
|
+
// Arrange
|
|
603
|
+
await UserModel.create({
|
|
604
|
+
id: 'test-user-id',
|
|
605
|
+
platform: 'testCRM',
|
|
606
|
+
accessToken: 'test-token',
|
|
607
|
+
platformAdditionalInfo: {}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const mockConnector = {
|
|
611
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
612
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded')
|
|
613
|
+
};
|
|
614
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
615
|
+
|
|
616
|
+
// Act
|
|
617
|
+
const result = await logHandler.createMessageLog({
|
|
618
|
+
platform: 'testCRM',
|
|
619
|
+
userId: 'test-user-id',
|
|
620
|
+
incomingData: {
|
|
621
|
+
logInfo: {
|
|
622
|
+
messages: [{ id: 'msg-1', subject: 'Test', creationTime: new Date() }],
|
|
623
|
+
correspondents: [{ phoneNumber: '+1234567890' }]
|
|
624
|
+
},
|
|
625
|
+
contactId: null
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Assert
|
|
630
|
+
expect(result.successful).toBe(false);
|
|
631
|
+
expect(result.returnMessage.message).toContain('Contact not found for number');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('should successfully create message log', async () => {
|
|
635
|
+
// Arrange
|
|
636
|
+
await UserModel.create({
|
|
637
|
+
id: 'test-user-id',
|
|
638
|
+
platform: 'testCRM',
|
|
639
|
+
accessToken: 'test-token',
|
|
640
|
+
platformAdditionalInfo: {}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const mockConnector = {
|
|
644
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
645
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
646
|
+
createMessageLog: jest.fn().mockResolvedValue({
|
|
647
|
+
logId: 'msg-log-123',
|
|
648
|
+
returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
|
|
649
|
+
}),
|
|
650
|
+
updateMessageLog: jest.fn()
|
|
651
|
+
};
|
|
652
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
653
|
+
|
|
654
|
+
const incomingData = {
|
|
655
|
+
logInfo: {
|
|
656
|
+
messages: [{ id: 'msg-1', subject: 'Test SMS', direction: 'Outbound', creationTime: new Date() }],
|
|
657
|
+
correspondents: [{ phoneNumber: '+1234567890' }],
|
|
658
|
+
conversationId: 'conv-123',
|
|
659
|
+
conversationLogId: 'conv-log-123'
|
|
660
|
+
},
|
|
661
|
+
contactId: 'contact-123',
|
|
662
|
+
contactType: 'Contact',
|
|
663
|
+
contactName: 'Test Contact',
|
|
664
|
+
additionalSubmission: {}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Act
|
|
668
|
+
const result = await logHandler.createMessageLog({
|
|
669
|
+
platform: 'testCRM',
|
|
670
|
+
userId: 'test-user-id',
|
|
671
|
+
incomingData
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Assert
|
|
675
|
+
expect(result.successful).toBe(true);
|
|
676
|
+
expect(result.logIds).toContain('msg-1');
|
|
677
|
+
expect(mockConnector.createMessageLog).toHaveBeenCalled();
|
|
678
|
+
|
|
679
|
+
// Verify message log was saved
|
|
680
|
+
const savedLog = await MessageLogModel.findOne({ where: { id: 'msg-1' } });
|
|
681
|
+
expect(savedLog).not.toBeNull();
|
|
682
|
+
expect(savedLog.thirdPartyLogId).toBe('msg-log-123');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('should skip already logged messages', async () => {
|
|
686
|
+
// Arrange
|
|
687
|
+
await UserModel.create({
|
|
688
|
+
id: 'test-user-id',
|
|
689
|
+
platform: 'testCRM',
|
|
690
|
+
accessToken: 'test-token',
|
|
691
|
+
platformAdditionalInfo: {}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
await MessageLogModel.create({
|
|
695
|
+
id: 'msg-1',
|
|
696
|
+
platform: 'testCRM',
|
|
697
|
+
conversationId: 'conv-123',
|
|
698
|
+
thirdPartyLogId: 'existing-log',
|
|
699
|
+
userId: 'test-user-id'
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const mockConnector = {
|
|
703
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
704
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
705
|
+
createMessageLog: jest.fn().mockResolvedValue({
|
|
706
|
+
logId: 'msg-log-new',
|
|
707
|
+
returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
|
|
708
|
+
})
|
|
709
|
+
};
|
|
710
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
711
|
+
|
|
712
|
+
const incomingData = {
|
|
713
|
+
logInfo: {
|
|
714
|
+
messages: [
|
|
715
|
+
{ id: 'msg-1', subject: 'Already logged', direction: 'Outbound', creationTime: new Date() },
|
|
716
|
+
{ id: 'msg-2', subject: 'New message', direction: 'Outbound', creationTime: new Date() }
|
|
717
|
+
],
|
|
718
|
+
correspondents: [{ phoneNumber: '+1234567890' }],
|
|
719
|
+
conversationId: 'conv-123',
|
|
720
|
+
conversationLogId: 'new-conv-log-123'
|
|
721
|
+
},
|
|
722
|
+
contactId: 'contact-123',
|
|
723
|
+
contactType: 'Contact',
|
|
724
|
+
additionalSubmission: {}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// Act
|
|
728
|
+
const result = await logHandler.createMessageLog({
|
|
729
|
+
platform: 'testCRM',
|
|
730
|
+
userId: 'test-user-id',
|
|
731
|
+
incomingData
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Assert
|
|
735
|
+
expect(result.successful).toBe(true);
|
|
736
|
+
// Only the new message should be logged
|
|
737
|
+
expect(mockConnector.createMessageLog).toHaveBeenCalledTimes(1);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
describe('saveNoteCache', () => {
|
|
742
|
+
test('should successfully save note cache', async () => {
|
|
743
|
+
// Arrange
|
|
744
|
+
NoteCache.create.mockResolvedValue({ sessionId: 'session-123', note: 'Test note' });
|
|
745
|
+
|
|
746
|
+
// Act
|
|
747
|
+
const result = await logHandler.saveNoteCache({
|
|
748
|
+
sessionId: 'session-123',
|
|
749
|
+
note: 'Test note'
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Assert
|
|
753
|
+
expect(result.successful).toBe(true);
|
|
754
|
+
expect(result.returnMessage).toBe('Note cache saved');
|
|
755
|
+
expect(NoteCache.create).toHaveBeenCalled();
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test('should handle errors when saving note cache', async () => {
|
|
759
|
+
// Arrange
|
|
760
|
+
NoteCache.create.mockRejectedValue(new Error('DynamoDB error'));
|
|
761
|
+
|
|
762
|
+
// Act
|
|
763
|
+
const result = await logHandler.saveNoteCache({
|
|
764
|
+
sessionId: 'session-123',
|
|
765
|
+
note: 'Test note'
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Assert
|
|
769
|
+
expect(result.successful).toBe(false);
|
|
770
|
+
expect(result.returnMessage).toBe('Error saving note cache');
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
describe('Error Handling', () => {
|
|
775
|
+
test('should handle 429 rate limit error in createCallLog', async () => {
|
|
776
|
+
// Arrange
|
|
777
|
+
await UserModel.create({
|
|
778
|
+
id: 'test-user-id',
|
|
779
|
+
platform: 'testCRM',
|
|
780
|
+
accessToken: 'test-token',
|
|
781
|
+
platformAdditionalInfo: {}
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const mockConnector = {
|
|
785
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
786
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
787
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
788
|
+
createCallLog: jest.fn().mockRejectedValue({
|
|
789
|
+
response: { status: 429 }
|
|
790
|
+
})
|
|
791
|
+
};
|
|
792
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
793
|
+
composeCallLog.mockReturnValue('Composed log');
|
|
794
|
+
|
|
795
|
+
const incomingData = {
|
|
796
|
+
logInfo: {
|
|
797
|
+
sessionId: 'session-rate-limit',
|
|
798
|
+
telephonySessionId: 'tel-session',
|
|
799
|
+
direction: 'Outbound',
|
|
800
|
+
from: { phoneNumber: '+1234567890' },
|
|
801
|
+
to: { phoneNumber: '+0987654321' }
|
|
802
|
+
},
|
|
803
|
+
contactId: 'contact-123',
|
|
804
|
+
note: 'Test'
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// Act
|
|
808
|
+
const result = await logHandler.createCallLog({
|
|
809
|
+
platform: 'testCRM',
|
|
810
|
+
userId: 'test-user-id',
|
|
811
|
+
incomingData,
|
|
812
|
+
hashedAccountId: 'hashed-123',
|
|
813
|
+
isFromSSCL: false
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Assert
|
|
817
|
+
expect(result.successful).toBe(false);
|
|
818
|
+
expect(result.returnMessage.messageType).toBe('warning');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test('should handle 401 authorization error in createCallLog', async () => {
|
|
822
|
+
// Arrange
|
|
823
|
+
await UserModel.create({
|
|
824
|
+
id: 'test-user-id',
|
|
825
|
+
platform: 'testCRM',
|
|
826
|
+
accessToken: 'test-token',
|
|
827
|
+
platformAdditionalInfo: {}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const mockConnector = {
|
|
831
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
832
|
+
getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
|
|
833
|
+
getLogFormatType: jest.fn().mockReturnValue('text/plain'),
|
|
834
|
+
createCallLog: jest.fn().mockRejectedValue({
|
|
835
|
+
response: { status: 401 }
|
|
836
|
+
})
|
|
837
|
+
};
|
|
838
|
+
connectorRegistry.getConnector.mockReturnValue(mockConnector);
|
|
839
|
+
composeCallLog.mockReturnValue('Composed log');
|
|
840
|
+
|
|
841
|
+
const incomingData = {
|
|
842
|
+
logInfo: {
|
|
843
|
+
sessionId: 'session-auth-error',
|
|
844
|
+
telephonySessionId: 'tel-session',
|
|
845
|
+
direction: 'Outbound',
|
|
846
|
+
from: { phoneNumber: '+1234567890' },
|
|
847
|
+
to: { phoneNumber: '+0987654321' }
|
|
848
|
+
},
|
|
849
|
+
contactId: 'contact-123',
|
|
850
|
+
note: 'Test'
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// Act
|
|
854
|
+
const result = await logHandler.createCallLog({
|
|
855
|
+
platform: 'testCRM',
|
|
856
|
+
userId: 'test-user-id',
|
|
857
|
+
incomingData,
|
|
858
|
+
hashedAccountId: 'hashed-123',
|
|
859
|
+
isFromSSCL: false
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Assert
|
|
863
|
+
expect(result.successful).toBe(false);
|
|
864
|
+
expect(result.extraDataTracking.statusCode).toBe(401);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|