@app-connect/core 1.7.34 → 1.7.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connector/mock.js +9 -4
- package/connector/proxy/engine.js +3 -2
- package/connector/proxy/index.js +2 -2
- package/docs/architecture.md +1 -0
- package/docs/handlers.md +1 -1
- package/docs/models.md +2 -0
- package/docs/routes.md +1 -1
- package/handlers/disposition.js +3 -2
- package/handlers/log.js +18 -6
- package/handlers/plugin.js +13 -6
- package/index.js +22 -11
- package/lib/callLogLookup.js +82 -9
- package/lib/migrateCallLogsSchema.js +128 -7
- package/models/callLogModel.js +6 -0
- package/package.json +1 -1
- package/releaseNotes.json +44 -0
- package/test/connector/developerPortal.test.js +166 -0
- package/test/connector/mock.test.js +131 -0
- package/test/connector/proxy/engine.test.js +85 -0
- package/test/connector/proxy/index.test.js +246 -0
- package/test/connector/proxy/sample.json +1 -0
- package/test/handlers/admin.test.js +344 -0
- package/test/handlers/appointment.test.js +260 -0
- package/test/handlers/auth.test.js +6 -2
- package/test/handlers/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +327 -3
- package/test/handlers/managedOAuth.test.js +262 -0
- package/test/handlers/plugin.test.js +305 -0
- package/test/handlers/user.test.js +381 -0
- package/test/index.test.js +166 -1
- package/test/lib/analytics.test.js +146 -0
- package/test/lib/authSession.test.js +173 -0
- package/test/lib/encode.test.js +59 -0
- package/test/lib/errorHandler.test.js +246 -0
- package/test/lib/generalErrorMessage.test.js +82 -0
- package/test/lib/s3ErrorLogReport.test.js +187 -0
- package/test/mcp/mcpHandlerMore.test.js +384 -0
- package/test/mcp/tools/appointmentTools.test.js +362 -0
- package/test/models/callDownListModel.test.js +125 -0
- package/test/models/dynamo/lockSchema.test.js +37 -0
- package/test/models/dynamo/noteCacheSchema.test.js +45 -0
- package/test/models/llmSessionModel.test.js +91 -0
- package/test/models/models.test.js +92 -0
- package/test/routes/calldownRoutes.test.js +224 -0
- package/test/routes/coreRouterBroadRoutes.test.js +855 -0
- package/test/routes/dispositionRoutes.test.js +192 -0
- package/test/routes/managedAuthRoutes.test.js +151 -0
- package/test/routes/pluginRoutes.test.js +262 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
jest.mock('../../../lib/jwt');
|
|
2
|
+
jest.mock('../../../connector/registry');
|
|
3
|
+
jest.mock('../../../handlers/appointment');
|
|
4
|
+
|
|
5
|
+
const jwt = require('../../../lib/jwt');
|
|
6
|
+
const connectorRegistry = require('../../../connector/registry');
|
|
7
|
+
const appointmentCore = require('../../../handlers/appointment');
|
|
8
|
+
const listAppointments = require('../../../mcp/tools/listAppointments');
|
|
9
|
+
const createAppointment = require('../../../mcp/tools/createAppointment');
|
|
10
|
+
const updateAppointment = require('../../../mcp/tools/updateAppointment');
|
|
11
|
+
const confirmAppointment = require('../../../mcp/tools/confirmAppointment');
|
|
12
|
+
const cancelAppointment = require('../../../mcp/tools/cancelAppointment');
|
|
13
|
+
|
|
14
|
+
describe('MCP appointment tools', () => {
|
|
15
|
+
const decodedToken = {
|
|
16
|
+
id: 'user-123',
|
|
17
|
+
platform: 'testCRM'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
jwt.decodeJwt.mockReturnValue(decodedToken);
|
|
23
|
+
connectorRegistry.getConnector.mockReturnValue({
|
|
24
|
+
listAppointments: jest.fn(),
|
|
25
|
+
createAppointment: jest.fn(),
|
|
26
|
+
updateAppointment: jest.fn(),
|
|
27
|
+
confirmAppointment: jest.fn(),
|
|
28
|
+
cancelAppointment: jest.fn()
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('defines appointment tool schemas and annotations', () => {
|
|
33
|
+
expect(listAppointments.definition.name).toBe('listAppointments');
|
|
34
|
+
expect(listAppointments.definition.annotations.readOnlyHint).toBe(true);
|
|
35
|
+
expect(createAppointment.definition.inputSchema.required).toEqual(['title', 'startTimeUtc', 'durationMinutes']);
|
|
36
|
+
expect(updateAppointment.definition.inputSchema.required).toEqual(['appointmentId']);
|
|
37
|
+
expect(confirmAppointment.definition.inputSchema.required).toEqual(['appointmentId']);
|
|
38
|
+
expect(cancelAppointment.definition.annotations.destructiveHint).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('listAppointments resolves filters, custom ranges, success, and handler failure', async () => {
|
|
42
|
+
appointmentCore.listAppointments.mockResolvedValueOnce({
|
|
43
|
+
successful: true,
|
|
44
|
+
appointments: [{ id: 'appt-1' }],
|
|
45
|
+
returnMessage: { message: 'Listed' }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const upcomingResult = await listAppointments.execute({
|
|
49
|
+
jwtToken: 'jwt-token',
|
|
50
|
+
filter: 'upcoming',
|
|
51
|
+
mineOnly: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(upcomingResult.success).toBe(true);
|
|
55
|
+
expect(upcomingResult.data.filter).toBe('upcoming');
|
|
56
|
+
expect(upcomingResult.data.totalCount).toBe(1);
|
|
57
|
+
expect(appointmentCore.listAppointments).toHaveBeenCalledWith({
|
|
58
|
+
platform: 'testCRM',
|
|
59
|
+
userId: 'user-123',
|
|
60
|
+
range: expect.objectContaining({
|
|
61
|
+
startDate: expect.any(String),
|
|
62
|
+
endDate: expect.any(String)
|
|
63
|
+
}),
|
|
64
|
+
mineOnly: true,
|
|
65
|
+
forceSync: false
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
appointmentCore.listAppointments.mockResolvedValueOnce({
|
|
69
|
+
successful: true,
|
|
70
|
+
appointments: []
|
|
71
|
+
});
|
|
72
|
+
const customResult = await listAppointments.execute({
|
|
73
|
+
jwtToken: 'jwt-token',
|
|
74
|
+
filter: 'custom',
|
|
75
|
+
startDate: '2026-07-01',
|
|
76
|
+
endDate: '2026-07-31'
|
|
77
|
+
});
|
|
78
|
+
expect(customResult.data.range).toEqual({
|
|
79
|
+
startDate: '2026-07-01',
|
|
80
|
+
endDate: '2026-07-31'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
appointmentCore.listAppointments.mockResolvedValueOnce({
|
|
84
|
+
successful: false,
|
|
85
|
+
returnMessage: { message: 'List failed' }
|
|
86
|
+
});
|
|
87
|
+
const failedResult = await listAppointments.execute({
|
|
88
|
+
jwtToken: 'jwt-token',
|
|
89
|
+
filter: 'past'
|
|
90
|
+
});
|
|
91
|
+
expect(failedResult).toMatchObject({
|
|
92
|
+
success: false,
|
|
93
|
+
error: 'List failed'
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('listAppointments validates auth and connector capability errors', async () => {
|
|
98
|
+
await expect(listAppointments.execute({})).resolves.toMatchObject({
|
|
99
|
+
success: false,
|
|
100
|
+
error: 'Please go to Settings and authorize CRM platform'
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
jwt.decodeJwt.mockReturnValueOnce(null);
|
|
104
|
+
await expect(listAppointments.execute({ jwtToken: 'bad' })).resolves.toMatchObject({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Invalid JWT token'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
jwt.decodeJwt.mockReturnValueOnce({ platform: 'testCRM' });
|
|
110
|
+
await expect(listAppointments.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
111
|
+
success: false,
|
|
112
|
+
error: 'Invalid JWT token: userId not found'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
connectorRegistry.getConnector.mockReturnValueOnce(null);
|
|
116
|
+
await expect(listAppointments.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
117
|
+
success: false,
|
|
118
|
+
error: 'Platform connector not found for: testCRM'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
122
|
+
await expect(listAppointments.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
123
|
+
success: false,
|
|
124
|
+
error: 'listAppointments is not implemented for platform: testCRM'
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('createAppointment validates payload and returns success or handler failure', async () => {
|
|
129
|
+
await expect(createAppointment.execute({})).resolves.toMatchObject({
|
|
130
|
+
success: false,
|
|
131
|
+
error: 'Please go to Settings and authorize CRM platform'
|
|
132
|
+
});
|
|
133
|
+
await expect(createAppointment.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
134
|
+
success: false,
|
|
135
|
+
error: 'title is required'
|
|
136
|
+
});
|
|
137
|
+
await expect(createAppointment.execute({ jwtToken: 'jwt-token', title: 'Meet' })).resolves.toMatchObject({
|
|
138
|
+
success: false,
|
|
139
|
+
error: expect.stringContaining('startTimeUtc is required')
|
|
140
|
+
});
|
|
141
|
+
await expect(createAppointment.execute({
|
|
142
|
+
jwtToken: 'jwt-token',
|
|
143
|
+
title: 'Meet',
|
|
144
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z'
|
|
145
|
+
})).resolves.toMatchObject({
|
|
146
|
+
success: false,
|
|
147
|
+
error: 'durationMinutes is required'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
appointmentCore.createAppointment.mockResolvedValueOnce({
|
|
151
|
+
successful: true,
|
|
152
|
+
appointmentId: 'appt-1',
|
|
153
|
+
appointment: { id: 'appt-1' },
|
|
154
|
+
returnMessage: { message: 'Created' }
|
|
155
|
+
});
|
|
156
|
+
const result = await createAppointment.execute({
|
|
157
|
+
jwtToken: 'jwt-token',
|
|
158
|
+
title: 'Meet',
|
|
159
|
+
summary: 'Summary',
|
|
160
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
161
|
+
durationMinutes: '45',
|
|
162
|
+
contacts: ['contact-1']
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result).toEqual({
|
|
166
|
+
success: true,
|
|
167
|
+
data: {
|
|
168
|
+
appointmentId: 'appt-1',
|
|
169
|
+
appointment: { id: 'appt-1' },
|
|
170
|
+
message: 'Created'
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
expect(appointmentCore.createAppointment).toHaveBeenCalledWith({
|
|
174
|
+
platform: 'testCRM',
|
|
175
|
+
userId: 'user-123',
|
|
176
|
+
payload: {
|
|
177
|
+
title: 'Meet',
|
|
178
|
+
summary: 'Summary',
|
|
179
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
180
|
+
durationMinutes: 45,
|
|
181
|
+
contacts: ['contact-1']
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
appointmentCore.createAppointment.mockResolvedValueOnce({
|
|
186
|
+
successful: false,
|
|
187
|
+
returnMessage: { message: 'Create failed' }
|
|
188
|
+
});
|
|
189
|
+
await expect(createAppointment.execute({
|
|
190
|
+
jwtToken: 'jwt-token',
|
|
191
|
+
title: 'Meet',
|
|
192
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
193
|
+
durationMinutes: 30
|
|
194
|
+
})).resolves.toMatchObject({
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Create failed'
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('createAppointment validates connector availability and implementation', async () => {
|
|
201
|
+
connectorRegistry.getConnector.mockReturnValueOnce(null);
|
|
202
|
+
await expect(createAppointment.execute({
|
|
203
|
+
jwtToken: 'jwt-token',
|
|
204
|
+
title: 'Meet',
|
|
205
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
206
|
+
durationMinutes: 30
|
|
207
|
+
})).resolves.toMatchObject({
|
|
208
|
+
success: false,
|
|
209
|
+
error: 'Platform connector not found for: testCRM'
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
213
|
+
await expect(createAppointment.execute({
|
|
214
|
+
jwtToken: 'jwt-token',
|
|
215
|
+
title: 'Meet',
|
|
216
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
217
|
+
durationMinutes: 30
|
|
218
|
+
})).resolves.toMatchObject({
|
|
219
|
+
success: false,
|
|
220
|
+
error: 'createAppointment is not implemented for platform: testCRM'
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('updateAppointment builds sparse patch body and handles failures', async () => {
|
|
225
|
+
await expect(updateAppointment.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
226
|
+
success: false,
|
|
227
|
+
error: 'appointmentId is required'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
appointmentCore.updateAppointment.mockResolvedValueOnce({
|
|
231
|
+
successful: true,
|
|
232
|
+
appointment: { id: 'appt-1' },
|
|
233
|
+
returnMessage: { message: 'Updated' }
|
|
234
|
+
});
|
|
235
|
+
const result = await updateAppointment.execute({
|
|
236
|
+
jwtToken: 'jwt-token',
|
|
237
|
+
appointmentId: 'appt-1',
|
|
238
|
+
title: 'Updated',
|
|
239
|
+
summary: 'Summary',
|
|
240
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
241
|
+
durationMinutes: '60',
|
|
242
|
+
contacts: [{ id: 'contact-1' }]
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual({
|
|
246
|
+
success: true,
|
|
247
|
+
data: {
|
|
248
|
+
appointment: { id: 'appt-1' },
|
|
249
|
+
message: 'Updated'
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
expect(appointmentCore.updateAppointment).toHaveBeenCalledWith({
|
|
253
|
+
platform: 'testCRM',
|
|
254
|
+
userId: 'user-123',
|
|
255
|
+
appointmentId: 'appt-1',
|
|
256
|
+
patchBody: {
|
|
257
|
+
title: 'Updated',
|
|
258
|
+
summary: 'Summary',
|
|
259
|
+
startTimeUtc: '2026-07-20T19:00:00.000Z',
|
|
260
|
+
durationMinutes: 60,
|
|
261
|
+
contacts: [{ id: 'contact-1' }]
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
appointmentCore.updateAppointment.mockResolvedValueOnce({
|
|
266
|
+
successful: false,
|
|
267
|
+
returnMessage: { message: 'Update failed' }
|
|
268
|
+
});
|
|
269
|
+
await expect(updateAppointment.execute({
|
|
270
|
+
jwtToken: 'jwt-token',
|
|
271
|
+
appointmentId: 'appt-1'
|
|
272
|
+
})).resolves.toMatchObject({
|
|
273
|
+
success: false,
|
|
274
|
+
error: 'Update failed'
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('updateAppointment validates connector availability and implementation', async () => {
|
|
279
|
+
connectorRegistry.getConnector.mockReturnValueOnce(null);
|
|
280
|
+
await expect(updateAppointment.execute({
|
|
281
|
+
jwtToken: 'jwt-token',
|
|
282
|
+
appointmentId: 'appt-1'
|
|
283
|
+
})).resolves.toMatchObject({
|
|
284
|
+
success: false,
|
|
285
|
+
error: 'Platform connector not found for: testCRM'
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
289
|
+
await expect(updateAppointment.execute({
|
|
290
|
+
jwtToken: 'jwt-token',
|
|
291
|
+
appointmentId: 'appt-1'
|
|
292
|
+
})).resolves.toMatchObject({
|
|
293
|
+
success: false,
|
|
294
|
+
error: 'updateAppointment is not implemented for platform: testCRM'
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test.each([
|
|
299
|
+
['confirmAppointment', confirmAppointment, 'confirmAppointment', appointmentCore.confirmAppointment, 'Appointment confirmed successfully', 'Failed to confirm appointment'],
|
|
300
|
+
['cancelAppointment', cancelAppointment, 'cancelAppointment', appointmentCore.cancelAppointment, 'Appointment cancelled successfully', 'Failed to cancel appointment']
|
|
301
|
+
])('%s validates, succeeds, fails, and checks capability', async (name, tool, capabilityName, handlerFn, defaultMessage, defaultError) => {
|
|
302
|
+
await expect(tool.execute({})).resolves.toMatchObject({
|
|
303
|
+
success: false,
|
|
304
|
+
error: 'Please go to Settings and authorize CRM platform'
|
|
305
|
+
});
|
|
306
|
+
await expect(tool.execute({ jwtToken: 'jwt-token' })).resolves.toMatchObject({
|
|
307
|
+
success: false,
|
|
308
|
+
error: 'appointmentId is required'
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
handlerFn.mockResolvedValueOnce({
|
|
312
|
+
successful: true,
|
|
313
|
+
appointment: { id: 'appt-1' },
|
|
314
|
+
returnMessage: {}
|
|
315
|
+
});
|
|
316
|
+
await expect(tool.execute({
|
|
317
|
+
jwtToken: 'jwt-token',
|
|
318
|
+
appointmentId: 'appt-1'
|
|
319
|
+
})).resolves.toEqual({
|
|
320
|
+
success: true,
|
|
321
|
+
data: {
|
|
322
|
+
appointment: { id: 'appt-1' },
|
|
323
|
+
message: defaultMessage
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
expect(handlerFn).toHaveBeenCalledWith({
|
|
327
|
+
platform: 'testCRM',
|
|
328
|
+
userId: 'user-123',
|
|
329
|
+
appointmentId: 'appt-1'
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
handlerFn.mockResolvedValueOnce({
|
|
333
|
+
successful: false,
|
|
334
|
+
returnMessage: {}
|
|
335
|
+
});
|
|
336
|
+
await expect(tool.execute({
|
|
337
|
+
jwtToken: 'jwt-token',
|
|
338
|
+
appointmentId: 'appt-1'
|
|
339
|
+
})).resolves.toMatchObject({
|
|
340
|
+
success: false,
|
|
341
|
+
error: defaultError
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
connectorRegistry.getConnector.mockReturnValueOnce(null);
|
|
345
|
+
await expect(tool.execute({
|
|
346
|
+
jwtToken: 'jwt-token',
|
|
347
|
+
appointmentId: 'appt-1'
|
|
348
|
+
})).resolves.toMatchObject({
|
|
349
|
+
success: false,
|
|
350
|
+
error: 'Platform connector not found for: testCRM'
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
354
|
+
await expect(tool.execute({
|
|
355
|
+
jwtToken: 'jwt-token',
|
|
356
|
+
appointmentId: 'appt-1'
|
|
357
|
+
})).resolves.toMatchObject({
|
|
358
|
+
success: false,
|
|
359
|
+
error: `${capabilityName} is not implemented for platform: testCRM`
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { CallDownListModel } = require('../../models/callDownListModel');
|
|
2
|
+
|
|
3
|
+
describe('CallDownListModel', () => {
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
await CallDownListModel.sync({ force: true });
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
await CallDownListModel.destroy({ where: {} });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('defines the expected fields and indexes', () => {
|
|
13
|
+
const attributes = CallDownListModel.rawAttributes;
|
|
14
|
+
|
|
15
|
+
expect(attributes.id.primaryKey).toBe(true);
|
|
16
|
+
expect(attributes.userId.type.key).toBe('STRING');
|
|
17
|
+
expect(attributes.contactId.type.key).toBe('STRING');
|
|
18
|
+
expect(attributes.contactType.type.key).toBe('STRING');
|
|
19
|
+
expect(attributes.status.type.key).toBe('STRING');
|
|
20
|
+
expect(attributes.scheduledAt.type.key).toBe('DATE');
|
|
21
|
+
expect(attributes.lastCallAt.type.key).toBe('DATE');
|
|
22
|
+
expect(CallDownListModel.options.timestamps).toBe(true);
|
|
23
|
+
expect(CallDownListModel.options.indexes).toEqual(expect.arrayContaining([
|
|
24
|
+
expect.objectContaining({ fields: ['userId'] }),
|
|
25
|
+
expect.objectContaining({ fields: ['status'] }),
|
|
26
|
+
expect.objectContaining({ fields: ['scheduledAt'] }),
|
|
27
|
+
expect.objectContaining({ fields: ['userId', 'status'] }),
|
|
28
|
+
]));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('creates and reads a scheduled callback record', async () => {
|
|
32
|
+
const scheduledAt = new Date('2026-07-02T10:00:00.000Z');
|
|
33
|
+
|
|
34
|
+
await CallDownListModel.create({
|
|
35
|
+
id: 'call-down-1',
|
|
36
|
+
userId: 'user-1',
|
|
37
|
+
contactId: 'contact-1',
|
|
38
|
+
contactType: 'Lead',
|
|
39
|
+
status: 'Pending',
|
|
40
|
+
scheduledAt,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const record = await CallDownListModel.findByPk('call-down-1');
|
|
44
|
+
|
|
45
|
+
expect(record).toMatchObject({
|
|
46
|
+
id: 'call-down-1',
|
|
47
|
+
userId: 'user-1',
|
|
48
|
+
contactId: 'contact-1',
|
|
49
|
+
contactType: 'Lead',
|
|
50
|
+
status: 'Pending',
|
|
51
|
+
});
|
|
52
|
+
expect(record.scheduledAt.toISOString()).toBe('2026-07-02T10:00:00.000Z');
|
|
53
|
+
expect(record.lastCallAt).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('allows nullable fields under the current schema', async () => {
|
|
57
|
+
await CallDownListModel.create({
|
|
58
|
+
id: 'minimal-call-down',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const record = await CallDownListModel.findByPk('minimal-call-down');
|
|
62
|
+
|
|
63
|
+
expect(record.userId).toBeNull();
|
|
64
|
+
expect(record.contactId).toBeNull();
|
|
65
|
+
expect(record.status).toBeNull();
|
|
66
|
+
expect(record.scheduledAt).toBeNull();
|
|
67
|
+
expect(record.lastCallAt).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('updates callback status and last call time', async () => {
|
|
71
|
+
await CallDownListModel.create({
|
|
72
|
+
id: 'call-down-update',
|
|
73
|
+
userId: 'user-1',
|
|
74
|
+
contactId: 'contact-1',
|
|
75
|
+
contactType: 'Contact',
|
|
76
|
+
status: 'Pending',
|
|
77
|
+
scheduledAt: new Date('2026-07-02T10:00:00.000Z'),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const lastCallAt = new Date('2026-07-02T10:30:00.000Z');
|
|
81
|
+
await CallDownListModel.update({
|
|
82
|
+
status: 'Called',
|
|
83
|
+
lastCallAt,
|
|
84
|
+
}, {
|
|
85
|
+
where: { id: 'call-down-update' },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const record = await CallDownListModel.findByPk('call-down-update');
|
|
89
|
+
expect(record.status).toBe('Called');
|
|
90
|
+
expect(record.lastCallAt.toISOString()).toBe('2026-07-02T10:30:00.000Z');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('queries by user and status together', async () => {
|
|
94
|
+
await CallDownListModel.bulkCreate([
|
|
95
|
+
{
|
|
96
|
+
id: 'call-down-user-1-pending',
|
|
97
|
+
userId: 'user-1',
|
|
98
|
+
status: 'Pending',
|
|
99
|
+
scheduledAt: new Date('2026-07-02T10:00:00.000Z'),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'call-down-user-1-called',
|
|
103
|
+
userId: 'user-1',
|
|
104
|
+
status: 'Called',
|
|
105
|
+
scheduledAt: new Date('2026-07-02T11:00:00.000Z'),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'call-down-user-2-pending',
|
|
109
|
+
userId: 'user-2',
|
|
110
|
+
status: 'Pending',
|
|
111
|
+
scheduledAt: new Date('2026-07-02T12:00:00.000Z'),
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const records = await CallDownListModel.findAll({
|
|
116
|
+
where: {
|
|
117
|
+
userId: 'user-1',
|
|
118
|
+
status: 'Pending',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(records).toHaveLength(1);
|
|
123
|
+
expect(records[0].id).toBe('call-down-user-1-pending');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
jest.mock('dynamoose', () => ({
|
|
2
|
+
Schema: jest.fn((definition, options) => ({
|
|
3
|
+
definition,
|
|
4
|
+
options,
|
|
5
|
+
})),
|
|
6
|
+
model: jest.fn((name, schema, options) => ({
|
|
7
|
+
modelName: name,
|
|
8
|
+
schema,
|
|
9
|
+
options,
|
|
10
|
+
})),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const { Lock } = require('../../../models/dynamo/lockSchema');
|
|
14
|
+
|
|
15
|
+
describe('lockSchema', () => {
|
|
16
|
+
test('defines userId as the hash key', () => {
|
|
17
|
+
expect(Lock.schema.definition.userId).toEqual({
|
|
18
|
+
type: String,
|
|
19
|
+
hashKey: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('defines ttl as a number field', () => {
|
|
24
|
+
expect(Lock.schema.definition.ttl).toEqual({
|
|
25
|
+
type: Number,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('creates the expected Dynamoose model and table options', () => {
|
|
30
|
+
expect(Lock.modelName).toBe('-token-refresh-lock');
|
|
31
|
+
expect(Lock.options).toEqual({
|
|
32
|
+
prefix: process.env.DYNAMODB_TABLE_PREFIX,
|
|
33
|
+
expires: 60,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
jest.mock('dynamoose', () => ({
|
|
2
|
+
Schema: jest.fn((definition, options) => ({
|
|
3
|
+
definition,
|
|
4
|
+
options,
|
|
5
|
+
})),
|
|
6
|
+
model: jest.fn((name, schema, options) => ({
|
|
7
|
+
modelName: name,
|
|
8
|
+
schema,
|
|
9
|
+
options,
|
|
10
|
+
})),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const { NoteCache } = require('../../../models/dynamo/noteCacheSchema');
|
|
14
|
+
|
|
15
|
+
describe('noteCacheSchema', () => {
|
|
16
|
+
test('defines sessionId as the hash key', () => {
|
|
17
|
+
expect(NoteCache.schema.definition.sessionId).toEqual({
|
|
18
|
+
type: String,
|
|
19
|
+
hashKey: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('requires the cached note value', () => {
|
|
24
|
+
expect(NoteCache.schema.definition.note).toEqual({
|
|
25
|
+
type: String,
|
|
26
|
+
required: true,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('requires ttl as a number field', () => {
|
|
31
|
+
expect(NoteCache.schema.definition.ttl).toEqual({
|
|
32
|
+
type: Number,
|
|
33
|
+
required: true,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('creates the expected Dynamoose model and table options', () => {
|
|
38
|
+
expect(NoteCache.modelName).toBe('-note-cache');
|
|
39
|
+
expect(NoteCache.options).toEqual({
|
|
40
|
+
prefix: process.env.DYNAMODB_TABLE_PREFIX,
|
|
41
|
+
expires: 60,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const { LlmSessionModel } = require('../../models/llmSessionModel');
|
|
2
|
+
|
|
3
|
+
describe('LlmSessionModel', () => {
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
await LlmSessionModel.sync({ force: true });
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
await LlmSessionModel.destroy({ where: {} });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('defines the expected session fields', () => {
|
|
13
|
+
const attributes = LlmSessionModel.rawAttributes;
|
|
14
|
+
|
|
15
|
+
expect(attributes.id.primaryKey).toBe(true);
|
|
16
|
+
expect(attributes.jwtToken.type.key).toBe('STRING');
|
|
17
|
+
expect(attributes.expiry.type.key).toBe('DATE');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('creates and reads a persisted LLM session', async () => {
|
|
21
|
+
await LlmSessionModel.create({
|
|
22
|
+
id: 'llm-session-1',
|
|
23
|
+
jwtToken: 'jwt-token-1',
|
|
24
|
+
expiry: new Date('2026-07-02T11:00:00.000Z'),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const record = await LlmSessionModel.findByPk('llm-session-1');
|
|
28
|
+
|
|
29
|
+
expect(record.id).toBe('llm-session-1');
|
|
30
|
+
expect(record.jwtToken).toBe('jwt-token-1');
|
|
31
|
+
expect(record.expiry.toISOString()).toBe('2026-07-02T11:00:00.000Z');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('allows nullable token and expiry under the current schema', async () => {
|
|
35
|
+
await LlmSessionModel.create({
|
|
36
|
+
id: 'minimal-llm-session',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const record = await LlmSessionModel.findByPk('minimal-llm-session');
|
|
40
|
+
|
|
41
|
+
expect(record.jwtToken).toBeNull();
|
|
42
|
+
expect(record.expiry).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('updates session token and expiry', async () => {
|
|
46
|
+
await LlmSessionModel.create({
|
|
47
|
+
id: 'llm-session-update',
|
|
48
|
+
jwtToken: 'old-token',
|
|
49
|
+
expiry: new Date('2026-07-02T11:00:00.000Z'),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await LlmSessionModel.update({
|
|
53
|
+
jwtToken: 'new-token',
|
|
54
|
+
expiry: new Date('2026-07-02T12:00:00.000Z'),
|
|
55
|
+
}, {
|
|
56
|
+
where: { id: 'llm-session-update' },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const record = await LlmSessionModel.findByPk('llm-session-update');
|
|
60
|
+
expect(record.jwtToken).toBe('new-token');
|
|
61
|
+
expect(record.expiry.toISOString()).toBe('2026-07-02T12:00:00.000Z');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('deletes expired session records by expiry query', async () => {
|
|
65
|
+
await LlmSessionModel.bulkCreate([
|
|
66
|
+
{
|
|
67
|
+
id: 'expired-llm-session',
|
|
68
|
+
jwtToken: 'expired-token',
|
|
69
|
+
expiry: new Date('2026-07-02T10:59:59.000Z'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'active-llm-session',
|
|
73
|
+
jwtToken: 'active-token',
|
|
74
|
+
expiry: new Date('2026-07-02T11:00:01.000Z'),
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const { Op } = require('sequelize');
|
|
79
|
+
await LlmSessionModel.destroy({
|
|
80
|
+
where: {
|
|
81
|
+
expiry: {
|
|
82
|
+
[Op.lt]: new Date('2026-07-02T11:00:00.000Z'),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await expect(LlmSessionModel.findByPk('expired-llm-session')).resolves.toBeNull();
|
|
88
|
+
await expect(LlmSessionModel.findByPk('active-llm-session')).resolves.not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|