@app-connect/core 1.7.35 → 1.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/connector/mock.js +9 -4
  2. package/connector/proxy/engine.js +3 -2
  3. package/connector/proxy/index.js +2 -2
  4. package/docs/architecture.md +1 -0
  5. package/docs/handlers.md +1 -1
  6. package/docs/models.md +2 -0
  7. package/docs/routes.md +1 -1
  8. package/handlers/disposition.js +3 -2
  9. package/handlers/log.js +16 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +10 -4
  12. package/lib/callLogLookup.js +82 -9
  13. package/lib/migrateCallLogsSchema.js +128 -7
  14. package/models/callLogModel.js +6 -0
  15. package/package.json +1 -1
  16. package/releaseNotes.json +16 -0
  17. package/test/connector/developerPortal.test.js +166 -0
  18. package/test/connector/mock.test.js +131 -0
  19. package/test/connector/proxy/engine.test.js +85 -0
  20. package/test/connector/proxy/index.test.js +246 -0
  21. package/test/connector/proxy/sample.json +1 -0
  22. package/test/handlers/admin.test.js +344 -0
  23. package/test/handlers/appointment.test.js +260 -0
  24. package/test/handlers/calldown.test.js +310 -0
  25. package/test/handlers/disposition.test.js +396 -0
  26. package/test/handlers/log.test.js +324 -1
  27. package/test/handlers/managedOAuth.test.js +262 -0
  28. package/test/handlers/plugin.test.js +305 -0
  29. package/test/handlers/user.test.js +381 -0
  30. package/test/index.test.js +102 -0
  31. package/test/lib/analytics.test.js +146 -0
  32. package/test/lib/authSession.test.js +173 -0
  33. package/test/lib/encode.test.js +59 -0
  34. package/test/lib/errorHandler.test.js +246 -0
  35. package/test/lib/generalErrorMessage.test.js +82 -0
  36. package/test/lib/s3ErrorLogReport.test.js +187 -0
  37. package/test/mcp/mcpHandlerMore.test.js +384 -0
  38. package/test/mcp/tools/appointmentTools.test.js +362 -0
  39. package/test/models/callDownListModel.test.js +125 -0
  40. package/test/models/dynamo/lockSchema.test.js +37 -0
  41. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  42. package/test/models/llmSessionModel.test.js +91 -0
  43. package/test/models/models.test.js +92 -0
  44. package/test/routes/calldownRoutes.test.js +224 -0
  45. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  46. package/test/routes/dispositionRoutes.test.js +192 -0
  47. package/test/routes/managedAuthRoutes.test.js +151 -0
  48. package/test/routes/pluginRoutes.test.js +262 -0
@@ -16,6 +16,10 @@ const { UserModel } = require('../../models/userModel');
16
16
  const { CacheModel } = require('../../models/cacheModel');
17
17
  const { AdminConfigModel } = require('../../models/adminConfigModel');
18
18
  const { sequelize } = require('../../models/sequelize');
19
+ const {
20
+ ensureCallLogsHashedExtensionIdSchema,
21
+ sqliteCallLogsPkIncludesHashedExtension,
22
+ } = require('../../lib/migrateCallLogsSchema');
19
23
 
20
24
  describe('Core Models', () => {
21
25
  beforeAll(async () => {
@@ -208,6 +212,94 @@ describe('Core Models', () => {
208
212
  expect(logs).toHaveLength(2);
209
213
  });
210
214
 
215
+ test('should use hashedExtensionId as part of call log primary key', async () => {
216
+ // Arrange & Act
217
+ await CallLogModel.create({
218
+ id: 'call-shared-hash',
219
+ sessionId: 'session-shared-hash',
220
+ extensionNumber: '',
221
+ hashedExtensionId: 'hashed-1',
222
+ platform: 'testCRM',
223
+ thirdPartyLogId: 'third-party-hash-1',
224
+ userId: 'user-1'
225
+ });
226
+ await CallLogModel.create({
227
+ id: 'call-shared-hash',
228
+ sessionId: 'session-shared-hash',
229
+ extensionNumber: '',
230
+ hashedExtensionId: 'hashed-2',
231
+ platform: 'testCRM',
232
+ thirdPartyLogId: 'third-party-hash-2',
233
+ userId: 'user-1'
234
+ });
235
+
236
+ // Assert
237
+ const logs = await CallLogModel.findAll({
238
+ where: {
239
+ id: 'call-shared-hash',
240
+ sessionId: 'session-shared-hash'
241
+ }
242
+ });
243
+ expect(logs).toHaveLength(2);
244
+ });
245
+
246
+ test('should migrate legacy call log schema to hashed extension identity key', async () => {
247
+ await CallLogModel.drop();
248
+ await sequelize.query(`
249
+ CREATE TABLE callLogs (
250
+ id VARCHAR(255) NOT NULL,
251
+ sessionId VARCHAR(255) NOT NULL,
252
+ extensionNumber VARCHAR(255) NOT NULL DEFAULT '',
253
+ platform VARCHAR(255),
254
+ thirdPartyLogId VARCHAR(255),
255
+ userId VARCHAR(255),
256
+ contactId VARCHAR(255),
257
+ createdAt DATETIME NOT NULL,
258
+ updatedAt DATETIME NOT NULL,
259
+ PRIMARY KEY (id, sessionId, extensionNumber)
260
+ );
261
+ `);
262
+ await sequelize.query(`
263
+ INSERT INTO callLogs (
264
+ id,
265
+ sessionId,
266
+ extensionNumber,
267
+ platform,
268
+ thirdPartyLogId,
269
+ userId,
270
+ contactId,
271
+ createdAt,
272
+ updatedAt
273
+ ) VALUES (
274
+ 'legacy-call',
275
+ 'legacy-session',
276
+ '101',
277
+ 'testCRM',
278
+ 'third-party-legacy',
279
+ 'user-1',
280
+ 'contact-1',
281
+ '2026-01-01T00:00:00.000Z',
282
+ '2026-01-01T00:00:00.000Z'
283
+ );
284
+ `);
285
+
286
+ await ensureCallLogsHashedExtensionIdSchema(sequelize);
287
+
288
+ const tableDescription = await sequelize.getQueryInterface().describeTable('callLogs');
289
+ expect(tableDescription.hashedExtensionId).toBeDefined();
290
+ await expect(sqliteCallLogsPkIncludesHashedExtension(sequelize)).resolves.toBe(true);
291
+ const migratedLog = await CallLogModel.findOne({
292
+ where: {
293
+ id: 'legacy-call',
294
+ sessionId: 'legacy-session',
295
+ extensionNumber: '101',
296
+ hashedExtensionId: ''
297
+ }
298
+ });
299
+ expect(migratedLog).not.toBeNull();
300
+ expect(migratedLog.thirdPartyLogId).toBe('third-party-legacy');
301
+ });
302
+
211
303
  test('should find call logs by session ID', async () => {
212
304
  // Arrange
213
305
  await CallLogModel.create({
@@ -0,0 +1,224 @@
1
+ const express = require('express');
2
+ const request = require('supertest');
3
+
4
+ jest.mock('../../handlers/calldown', () => ({
5
+ schedule: jest.fn(),
6
+ list: jest.fn(),
7
+ remove: jest.fn(),
8
+ update: jest.fn(),
9
+ }));
10
+ jest.mock('../../lib/analytics', () => ({
11
+ init: jest.fn(),
12
+ track: jest.fn(),
13
+ }));
14
+ jest.mock('../../lib/jwt', () => ({
15
+ decodeJwt: jest.fn(),
16
+ generateJwt: jest.fn().mockReturnValue('refreshed-crm-jwt'),
17
+ }));
18
+
19
+ const calldown = require('../../handlers/calldown');
20
+ const analytics = require('../../lib/analytics');
21
+ const jwt = require('../../lib/jwt');
22
+ const { createCoreRouter } = require('../../index');
23
+
24
+ describe('Calldown Routes', () => {
25
+ let app;
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ jwt.decodeJwt.mockReturnValue({
30
+ id: 'crm-user-id',
31
+ platform: 'testCRM',
32
+ });
33
+ app = express();
34
+ app.use(express.json());
35
+ app.use('/', createCoreRouter());
36
+ });
37
+
38
+ test('POST /calldown schedules a callback and delegates body/token values', async () => {
39
+ calldown.schedule.mockResolvedValue({
40
+ id: 'call-down-id',
41
+ });
42
+
43
+ const response = await request(app)
44
+ .post('/calldown')
45
+ .query({
46
+ jwtToken: 'crm-jwt',
47
+ rcAccessToken: 'rc-token',
48
+ })
49
+ .send({
50
+ contactId: 'contact-1',
51
+ scheduledAt: '2026-07-02T13:00:00.000Z',
52
+ });
53
+
54
+ expect(response.status).toBe(200);
55
+ expect(response.body).toEqual({
56
+ successful: true,
57
+ id: 'call-down-id',
58
+ });
59
+ expect(calldown.schedule).toHaveBeenCalledWith({
60
+ jwtToken: 'crm-jwt',
61
+ rcAccessToken: 'rc-token',
62
+ body: {
63
+ contactId: 'contact-1',
64
+ scheduledAt: '2026-07-02T13:00:00.000Z',
65
+ },
66
+ });
67
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
68
+ eventName: 'Schedule call down',
69
+ interfaceName: 'scheduleCallDown',
70
+ success: true,
71
+ }));
72
+ });
73
+
74
+ test('POST /calldown rejects missing CRM auth token', async () => {
75
+ const response = await request(app)
76
+ .post('/calldown')
77
+ .send({ contactId: 'contact-1' });
78
+
79
+ expect(response.status).toBe(400);
80
+ expect(response.text).toBe('Please go to Settings and authorize CRM platform');
81
+ expect(calldown.schedule).not.toHaveBeenCalled();
82
+ });
83
+
84
+ test('GET /calldown returns items from handler with status filter', async () => {
85
+ calldown.list.mockResolvedValue({
86
+ items: [
87
+ {
88
+ id: 'call-down-id',
89
+ status: 'called',
90
+ },
91
+ ],
92
+ });
93
+
94
+ const response = await request(app)
95
+ .get('/calldown')
96
+ .query({
97
+ jwtToken: 'crm-jwt',
98
+ status: 'called',
99
+ });
100
+
101
+ expect(response.status).toBe(200);
102
+ expect(response.body).toEqual({
103
+ successful: true,
104
+ items: [
105
+ {
106
+ id: 'call-down-id',
107
+ status: 'called',
108
+ },
109
+ ],
110
+ });
111
+ expect(calldown.list).toHaveBeenCalledWith({
112
+ jwtToken: 'crm-jwt',
113
+ status: 'called',
114
+ });
115
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
116
+ eventName: 'Get call down list',
117
+ interfaceName: 'getCallDownList',
118
+ success: true,
119
+ }));
120
+ });
121
+
122
+ test('GET /calldown maps handler errors to a 400 error body', async () => {
123
+ calldown.list.mockRejectedValue(new Error('Unauthorized'));
124
+
125
+ const response = await request(app)
126
+ .get('/calldown')
127
+ .query({ jwtToken: 'crm-jwt' });
128
+
129
+ expect(response.status).toBe(400);
130
+ expect(response.body).toEqual({
131
+ error: 'Unauthorized',
132
+ });
133
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
134
+ eventName: 'Get call down list',
135
+ success: false,
136
+ }));
137
+ });
138
+
139
+ test('DELETE /calldown/:id delegates deletion by route parameter', async () => {
140
+ calldown.remove.mockResolvedValue({
141
+ successful: true,
142
+ });
143
+
144
+ const response = await request(app)
145
+ .delete('/calldown/call-down-id')
146
+ .query({ jwtToken: 'crm-jwt' });
147
+
148
+ expect(response.status).toBe(200);
149
+ expect(response.body).toEqual({
150
+ successful: true,
151
+ });
152
+ expect(calldown.remove).toHaveBeenCalledWith({
153
+ jwtToken: 'crm-jwt',
154
+ id: 'call-down-id',
155
+ });
156
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
157
+ eventName: 'Delete call down item',
158
+ interfaceName: 'deleteCallDownItem',
159
+ success: true,
160
+ }));
161
+ });
162
+
163
+ test('DELETE /calldown/:id rejects missing CRM auth token', async () => {
164
+ const response = await request(app)
165
+ .delete('/calldown/call-down-id');
166
+
167
+ expect(response.status).toBe(400);
168
+ expect(response.text).toBe('Please go to Settings and authorize CRM platform');
169
+ expect(calldown.remove).not.toHaveBeenCalled();
170
+ });
171
+
172
+ test('PATCH /calldown/:id delegates allowed update body', async () => {
173
+ calldown.update.mockResolvedValue({
174
+ successful: true,
175
+ });
176
+
177
+ const response = await request(app)
178
+ .patch('/calldown/call-down-id')
179
+ .query({ jwtToken: 'crm-jwt' })
180
+ .send({
181
+ status: 'called',
182
+ lastCallAt: '2026-07-02T14:00:00.000Z',
183
+ });
184
+
185
+ expect(response.status).toBe(200);
186
+ expect(response.body).toEqual({
187
+ successful: true,
188
+ });
189
+ expect(calldown.update).toHaveBeenCalledWith({
190
+ jwtToken: 'crm-jwt',
191
+ id: 'call-down-id',
192
+ updateData: {
193
+ status: 'called',
194
+ lastCallAt: '2026-07-02T14:00:00.000Z',
195
+ },
196
+ });
197
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
198
+ eventName: 'Mark call down called',
199
+ interfaceName: 'markCallDownCalled',
200
+ success: true,
201
+ }));
202
+ });
203
+
204
+ test('PATCH /calldown/:id maps handler errors to a 400 error body', async () => {
205
+ calldown.update.mockRejectedValue(new Error('No valid fields to update'));
206
+
207
+ const response = await request(app)
208
+ .patch('/calldown/call-down-id')
209
+ .query({ jwtToken: 'crm-jwt' })
210
+ .send({
211
+ unexpectedField: 'ignored',
212
+ });
213
+
214
+ expect(response.status).toBe(400);
215
+ expect(response.body).toEqual({
216
+ error: 'No valid fields to update',
217
+ });
218
+ expect(analytics.track).toHaveBeenCalledWith(expect.objectContaining({
219
+ eventName: 'Mark call down called',
220
+ success: false,
221
+ }));
222
+ });
223
+ });
224
+