@app-connect/core 1.7.24 → 1.7.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.test +5 -5
- package/README.md +441 -441
- package/connector/developerPortal.js +31 -42
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -163
- package/connector/proxy/index.js +500 -500
- package/connector/registry.js +252 -252
- package/docs/README.md +50 -50
- package/docs/architecture.md +93 -93
- package/docs/connectors.md +116 -117
- package/docs/handlers.md +125 -125
- package/docs/libraries.md +101 -101
- package/docs/models.md +144 -144
- package/docs/routes.md +115 -115
- package/docs/tests.md +73 -73
- package/handlers/admin.js +523 -523
- package/handlers/appointment.js +193 -0
- package/handlers/auth.js +296 -296
- package/handlers/calldown.js +99 -99
- package/handlers/contact.js +280 -280
- package/handlers/disposition.js +82 -80
- package/handlers/log.js +984 -973
- package/handlers/managedAuth.js +446 -446
- package/handlers/plugin.js +208 -208
- package/handlers/user.js +142 -142
- package/index.js +3140 -2652
- package/jest.config.js +56 -56
- package/lib/analytics.js +54 -54
- package/lib/authSession.js +109 -109
- package/lib/cacheCleanup.js +21 -0
- package/lib/callLogComposer.js +898 -898
- package/lib/callLogLookup.js +34 -0
- package/lib/constants.js +8 -8
- package/lib/debugTracer.js +177 -177
- package/lib/encode.js +30 -30
- package/lib/errorHandler.js +218 -206
- package/lib/generalErrorMessage.js +41 -41
- package/lib/jwt.js +18 -18
- package/lib/logger.js +190 -190
- package/lib/migrateCallLogsSchema.js +116 -0
- package/lib/ringcentral.js +266 -266
- package/lib/s3ErrorLogReport.js +65 -65
- package/lib/sharedSMSComposer.js +471 -471
- package/lib/util.js +67 -67
- package/mcp/README.md +412 -395
- package/mcp/lib/validator.js +91 -91
- package/mcp/mcpHandler.js +425 -425
- package/mcp/tools/cancelAppointment.js +101 -0
- package/mcp/tools/checkAuthStatus.js +105 -105
- package/mcp/tools/confirmAppointment.js +101 -0
- package/mcp/tools/createAppointment.js +157 -0
- package/mcp/tools/createCallLog.js +327 -316
- package/mcp/tools/createContact.js +117 -117
- package/mcp/tools/createMessageLog.js +287 -287
- package/mcp/tools/doAuth.js +60 -60
- package/mcp/tools/findContactByName.js +93 -93
- package/mcp/tools/findContactByPhone.js +101 -101
- package/mcp/tools/getCallLog.js +111 -102
- package/mcp/tools/getGoogleFilePicker.js +99 -99
- package/mcp/tools/getHelp.js +43 -43
- package/mcp/tools/getPublicConnectors.js +94 -94
- package/mcp/tools/getSessionInfo.js +90 -90
- package/mcp/tools/index.js +51 -41
- package/mcp/tools/listAppointments.js +163 -0
- package/mcp/tools/logout.js +96 -96
- package/mcp/tools/rcGetCallLogs.js +65 -65
- package/mcp/tools/updateAppointment.js +154 -0
- package/mcp/tools/updateCallLog.js +130 -126
- package/mcp/ui/App/App.tsx +358 -358
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
- package/mcp/ui/App/components/ConnectorList.tsx +82 -82
- package/mcp/ui/App/components/DebugPanel.tsx +43 -43
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
- package/mcp/ui/App/lib/callTool.ts +130 -130
- package/mcp/ui/App/lib/debugLog.ts +41 -41
- package/mcp/ui/App/lib/developerPortal.ts +111 -111
- package/mcp/ui/App/main.css +5 -5
- package/mcp/ui/App/root.tsx +13 -13
- package/mcp/ui/index.html +13 -13
- package/mcp/ui/package-lock.json +6356 -6356
- package/mcp/ui/package.json +25 -25
- package/mcp/ui/tsconfig.json +26 -26
- package/mcp/ui/vite.config.ts +16 -16
- package/models/accountDataModel.js +33 -33
- package/models/adminConfigModel.js +35 -35
- package/models/cacheModel.js +30 -26
- package/models/callDownListModel.js +34 -34
- package/models/callLogModel.js +33 -27
- package/models/dynamo/connectorSchema.js +146 -146
- package/models/dynamo/lockSchema.js +24 -24
- package/models/dynamo/noteCacheSchema.js +29 -29
- package/models/llmSessionModel.js +17 -17
- package/models/messageLogModel.js +25 -25
- package/models/sequelize.js +16 -16
- package/models/userModel.js +45 -45
- package/package.json +72 -72
- package/releaseNotes.json +1093 -1073
- package/test/connector/proxy/engine.test.js +126 -93
- package/test/connector/proxy/index.test.js +279 -279
- package/test/connector/proxy/sample.json +161 -161
- package/test/connector/registry.test.js +415 -415
- package/test/handlers/admin.test.js +616 -616
- package/test/handlers/auth.test.js +1018 -1015
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +458 -458
- package/test/handlers/plugin.test.js +380 -380
- package/test/index.test.js +105 -105
- package/test/lib/cacheCleanup.test.js +42 -0
- package/test/lib/callLogComposer.test.js +1231 -1231
- package/test/lib/debugTracer.test.js +328 -328
- package/test/lib/jwt.test.js +176 -176
- package/test/lib/logger.test.js +206 -206
- package/test/lib/oauth.test.js +359 -359
- package/test/lib/ringcentral.test.js +467 -467
- package/test/lib/sharedSMSComposer.test.js +1084 -1084
- package/test/lib/util.test.js +329 -329
- package/test/mcp/tools/checkAuthStatus.test.js +83 -82
- package/test/mcp/tools/createCallLog.test.js +436 -436
- package/test/mcp/tools/createContact.test.js +58 -58
- package/test/mcp/tools/createMessageLog.test.js +595 -595
- package/test/mcp/tools/doAuth.test.js +113 -113
- package/test/mcp/tools/findContactByName.test.js +275 -275
- package/test/mcp/tools/findContactByPhone.test.js +296 -296
- package/test/mcp/tools/getCallLog.test.js +298 -298
- package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
- package/test/mcp/tools/getPublicConnectors.test.js +107 -107
- package/test/mcp/tools/getSessionInfo.test.js +127 -127
- package/test/mcp/tools/logout.test.js +233 -233
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
- package/test/mcp/tools/updateCallLog.test.js +360 -360
- package/test/models/accountDataModel.test.js +98 -98
- package/test/models/dynamo/connectorSchema.test.js +189 -189
- package/test/models/models.test.js +568 -539
- package/test/routes/managedAuthRoutes.test.js +104 -129
- package/test/setup.js +178 -178
|
@@ -1,380 +1,380 @@
|
|
|
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
|
-
jest.mock('axios');
|
|
13
|
-
|
|
14
|
-
const pluginHandler = require('../../handlers/plugin');
|
|
15
|
-
const { CacheModel } = require('../../models/cacheModel');
|
|
16
|
-
const { AccountDataModel } = require('../../models/accountDataModel');
|
|
17
|
-
const axios = require('axios');
|
|
18
|
-
const { sequelize } = require('../../models/sequelize');
|
|
19
|
-
|
|
20
|
-
describe('Plugin Handler', () => {
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
process.env.HASH_KEY = 'unit-test-hash-key';
|
|
23
|
-
await CacheModel.sync({ force: true });
|
|
24
|
-
await AccountDataModel.sync({ force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
await CacheModel.destroy({ where: {} });
|
|
29
|
-
await AccountDataModel.destroy({ where: {} });
|
|
30
|
-
jest.clearAllMocks();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterAll(async () => {
|
|
34
|
-
await sequelize.close();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('getPluginAsyncTasks', () => {
|
|
38
|
-
test('should retrieve async task status by IDs from CacheModel', async () => {
|
|
39
|
-
// Arrange
|
|
40
|
-
await CacheModel.create({
|
|
41
|
-
id: 'user-123-task-1',
|
|
42
|
-
status: 'processing',
|
|
43
|
-
userId: 'user-123',
|
|
44
|
-
cacheKey: 'pluginTask-googleDrive'
|
|
45
|
-
});
|
|
46
|
-
await CacheModel.create({
|
|
47
|
-
id: 'user-123-task-2',
|
|
48
|
-
status: 'completed',
|
|
49
|
-
userId: 'user-123',
|
|
50
|
-
cacheKey: 'pluginTask-piiRedaction'
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Act
|
|
54
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
55
|
-
asyncTaskIds: ['user-123-task-1', 'user-123-task-2']
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Assert
|
|
59
|
-
expect(result).toHaveLength(2);
|
|
60
|
-
expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
|
|
61
|
-
expect(result).toContainEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'completed' });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('should return empty array when no matching tasks found', async () => {
|
|
65
|
-
// Arrange - no tasks created
|
|
66
|
-
|
|
67
|
-
// Act
|
|
68
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
69
|
-
asyncTaskIds: ['non-existent-task-1', 'non-existent-task-2']
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Assert
|
|
73
|
-
expect(result).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('should filter and return only tasks with matching IDs', async () => {
|
|
77
|
-
// Arrange
|
|
78
|
-
await CacheModel.create({
|
|
79
|
-
id: 'user-123-task-1',
|
|
80
|
-
status: 'processing',
|
|
81
|
-
userId: 'user-123',
|
|
82
|
-
cacheKey: 'pluginTask-googleDrive'
|
|
83
|
-
});
|
|
84
|
-
await CacheModel.create({
|
|
85
|
-
id: 'user-456-task-2',
|
|
86
|
-
status: 'completed',
|
|
87
|
-
userId: 'user-456',
|
|
88
|
-
cacheKey: 'pluginTask-piiRedaction'
|
|
89
|
-
});
|
|
90
|
-
await CacheModel.create({
|
|
91
|
-
id: 'user-789-task-3',
|
|
92
|
-
status: 'failed',
|
|
93
|
-
userId: 'user-789',
|
|
94
|
-
cacheKey: 'pluginTask-other'
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Act - only request tasks for user-123 and user-789
|
|
98
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
99
|
-
asyncTaskIds: ['user-123-task-1', 'user-789-task-3']
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Assert
|
|
103
|
-
expect(result).toHaveLength(2);
|
|
104
|
-
expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
|
|
105
|
-
expect(result).toContainEqual({ cacheKey: 'pluginTask-other', status: 'failed' });
|
|
106
|
-
expect(result).not.toContainEqual(expect.objectContaining({ cacheKey: 'pluginTask-piiRedaction' }));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('should automatically remove completed tasks from cache after retrieval', async () => {
|
|
110
|
-
// Arrange
|
|
111
|
-
await CacheModel.create({
|
|
112
|
-
id: 'user-123-completed-task',
|
|
113
|
-
status: 'completed',
|
|
114
|
-
userId: 'user-123',
|
|
115
|
-
cacheKey: 'pluginTask-googleDrive'
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Act
|
|
119
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
120
|
-
asyncTaskIds: ['user-123-completed-task']
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Assert - result should contain the task
|
|
124
|
-
expect(result).toHaveLength(1);
|
|
125
|
-
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'completed' });
|
|
126
|
-
|
|
127
|
-
// Verify task was removed from cache
|
|
128
|
-
const remainingTask = await CacheModel.findByPk('user-123-completed-task');
|
|
129
|
-
expect(remainingTask).toBeNull();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('should automatically remove failed tasks from cache after retrieval', async () => {
|
|
133
|
-
// Arrange
|
|
134
|
-
await CacheModel.create({
|
|
135
|
-
id: 'user-123-failed-task',
|
|
136
|
-
status: 'failed',
|
|
137
|
-
userId: 'user-123',
|
|
138
|
-
cacheKey: 'pluginTask-piiRedaction'
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Act
|
|
142
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
143
|
-
asyncTaskIds: ['user-123-failed-task']
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Assert - result should contain the task
|
|
147
|
-
expect(result).toHaveLength(1);
|
|
148
|
-
expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'failed' });
|
|
149
|
-
|
|
150
|
-
// Verify task was removed from cache
|
|
151
|
-
const remainingTask = await CacheModel.findByPk('user-123-failed-task');
|
|
152
|
-
expect(remainingTask).toBeNull();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('should preserve pending tasks in cache after retrieval', async () => {
|
|
156
|
-
// Arrange
|
|
157
|
-
await CacheModel.create({
|
|
158
|
-
id: 'user-123-pending-task',
|
|
159
|
-
status: 'pending',
|
|
160
|
-
userId: 'user-123',
|
|
161
|
-
cacheKey: 'pluginTask-googleDrive'
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Act
|
|
165
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
166
|
-
asyncTaskIds: ['user-123-pending-task']
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Assert - result should contain the task
|
|
170
|
-
expect(result).toHaveLength(1);
|
|
171
|
-
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'pending' });
|
|
172
|
-
|
|
173
|
-
// Verify task was NOT removed from cache
|
|
174
|
-
const remainingTask = await CacheModel.findByPk('user-123-pending-task');
|
|
175
|
-
expect(remainingTask).not.toBeNull();
|
|
176
|
-
expect(remainingTask.status).toBe('pending');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('should preserve processing tasks in cache after retrieval', async () => {
|
|
180
|
-
// Arrange
|
|
181
|
-
await CacheModel.create({
|
|
182
|
-
id: 'user-123-processing-task',
|
|
183
|
-
status: 'processing',
|
|
184
|
-
userId: 'user-123',
|
|
185
|
-
cacheKey: 'pluginTask-piiRedaction'
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Act
|
|
189
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
190
|
-
asyncTaskIds: ['user-123-processing-task']
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Assert - result should contain the task
|
|
194
|
-
expect(result).toHaveLength(1);
|
|
195
|
-
expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'processing' });
|
|
196
|
-
|
|
197
|
-
// Verify task was NOT removed from cache
|
|
198
|
-
const remainingTask = await CacheModel.findByPk('user-123-processing-task');
|
|
199
|
-
expect(remainingTask).not.toBeNull();
|
|
200
|
-
expect(remainingTask.status).toBe('processing');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('should handle mixed task statuses - remove completed/failed but preserve pending/processing', async () => {
|
|
204
|
-
// Arrange
|
|
205
|
-
await CacheModel.create({
|
|
206
|
-
id: 'task-completed',
|
|
207
|
-
status: 'completed',
|
|
208
|
-
userId: 'user-123',
|
|
209
|
-
cacheKey: 'pluginTask-1'
|
|
210
|
-
});
|
|
211
|
-
await CacheModel.create({
|
|
212
|
-
id: 'task-failed',
|
|
213
|
-
status: 'failed',
|
|
214
|
-
userId: 'user-123',
|
|
215
|
-
cacheKey: 'pluginTask-2'
|
|
216
|
-
});
|
|
217
|
-
await CacheModel.create({
|
|
218
|
-
id: 'task-pending',
|
|
219
|
-
status: 'pending',
|
|
220
|
-
userId: 'user-123',
|
|
221
|
-
cacheKey: 'pluginTask-3'
|
|
222
|
-
});
|
|
223
|
-
await CacheModel.create({
|
|
224
|
-
id: 'task-processing',
|
|
225
|
-
status: 'processing',
|
|
226
|
-
userId: 'user-123',
|
|
227
|
-
cacheKey: 'pluginTask-4'
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Act
|
|
231
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
232
|
-
asyncTaskIds: ['task-completed', 'task-failed', 'task-pending', 'task-processing']
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Assert - all tasks should be in result
|
|
236
|
-
expect(result).toHaveLength(4);
|
|
237
|
-
|
|
238
|
-
// Verify completed and failed tasks were removed
|
|
239
|
-
expect(await CacheModel.findByPk('task-completed')).toBeNull();
|
|
240
|
-
expect(await CacheModel.findByPk('task-failed')).toBeNull();
|
|
241
|
-
|
|
242
|
-
// Verify pending and processing tasks were preserved
|
|
243
|
-
expect(await CacheModel.findByPk('task-pending')).not.toBeNull();
|
|
244
|
-
expect(await CacheModel.findByPk('task-processing')).not.toBeNull();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('should handle empty asyncTaskIds array', async () => {
|
|
248
|
-
// Arrange
|
|
249
|
-
await CacheModel.create({
|
|
250
|
-
id: 'some-task',
|
|
251
|
-
status: 'completed',
|
|
252
|
-
userId: 'user-123',
|
|
253
|
-
cacheKey: 'pluginTask-test'
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Act
|
|
257
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
258
|
-
asyncTaskIds: []
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Assert
|
|
262
|
-
expect(result).toEqual([]);
|
|
263
|
-
|
|
264
|
-
// Verify existing task was not touched
|
|
265
|
-
const existingTask = await CacheModel.findByPk('some-task');
|
|
266
|
-
expect(existingTask).not.toBeNull();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test('should preserve initialized status tasks in cache', async () => {
|
|
270
|
-
// Arrange
|
|
271
|
-
await CacheModel.create({
|
|
272
|
-
id: 'user-123-initialized-task',
|
|
273
|
-
status: 'initialized',
|
|
274
|
-
userId: 'user-123',
|
|
275
|
-
cacheKey: 'pluginTask-googleDrive'
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Act
|
|
279
|
-
const result = await pluginHandler.getPluginAsyncTasks({
|
|
280
|
-
asyncTaskIds: ['user-123-initialized-task']
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Assert
|
|
284
|
-
expect(result).toHaveLength(1);
|
|
285
|
-
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'initialized' });
|
|
286
|
-
|
|
287
|
-
// Verify task was NOT removed from cache
|
|
288
|
-
const remainingTask = await CacheModel.findByPk('user-123-initialized-task');
|
|
289
|
-
expect(remainingTask).not.toBeNull();
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe('registerPluginAccount', () => {
|
|
294
|
-
test('should register plugin account and persist plugin jwt token in account data', async () => {
|
|
295
|
-
const rcAccountId = '12345';
|
|
296
|
-
const pluginId = 'sync-all-caps';
|
|
297
|
-
|
|
298
|
-
axios.get.mockResolvedValue({
|
|
299
|
-
data: {
|
|
300
|
-
platforms: {
|
|
301
|
-
'plugin.sample': {
|
|
302
|
-
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
303
|
-
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
axios.post.mockResolvedValue({
|
|
309
|
-
data: {
|
|
310
|
-
jwtToken: 'plugin-jwt-token'
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const result = await pluginHandler.registerPluginAccount({
|
|
315
|
-
pluginId,
|
|
316
|
-
rcAccessToken: 'rc-access-token',
|
|
317
|
-
rcAccountId,
|
|
318
|
-
pluginAccess: 'public',
|
|
319
|
-
pluginName: 'plugin.sample'
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
expect(result.successful).toBe(true);
|
|
323
|
-
expect(axios.post).toHaveBeenCalledWith(
|
|
324
|
-
`https://plugins.example.com/plugin/${pluginId}/auth/register`,
|
|
325
|
-
{
|
|
326
|
-
rcAccessToken: 'rc-access-token',
|
|
327
|
-
rcAccountId
|
|
328
|
-
}
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
const accountData = await AccountDataModel.findOne({
|
|
332
|
-
where: {
|
|
333
|
-
rcAccountId,
|
|
334
|
-
platformName: pluginId,
|
|
335
|
-
dataKey: 'pluginData'
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
expect(accountData).not.toBeNull();
|
|
339
|
-
expect(accountData.data.jwtToken).toBe('plugin-jwt-token');
|
|
340
|
-
expect(accountData.data.endpointUrl).toBe(`https://plugins.example.com/plugin/${pluginId}`);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test('should throw when register API does not return jwt token', async () => {
|
|
344
|
-
const rcAccountId = '12345';
|
|
345
|
-
const pluginId = 'sync-all-caps';
|
|
346
|
-
|
|
347
|
-
axios.get.mockResolvedValue({
|
|
348
|
-
data: {
|
|
349
|
-
platforms: {
|
|
350
|
-
'plugin.sample': {
|
|
351
|
-
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
352
|
-
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
axios.post.mockResolvedValue({ data: {} });
|
|
358
|
-
|
|
359
|
-
await expect(pluginHandler.registerPluginAccount({
|
|
360
|
-
pluginId,
|
|
361
|
-
rcAccessToken: 'rc-access-token',
|
|
362
|
-
rcAccountId,
|
|
363
|
-
pluginAccess: 'public',
|
|
364
|
-
pluginName: 'plugin.sample'
|
|
365
|
-
})).rejects.toThrow('Plugin register API did not return jwtToken');
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe('token header helper', () => {
|
|
370
|
-
test('should parse refreshed jwt token from response headers', () => {
|
|
371
|
-
const token = pluginHandler.getRefreshedJwtTokenFromHeaders({
|
|
372
|
-
headers: {
|
|
373
|
-
'x-refreshed-jwt-token': 'new-plugin-token'
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
expect(token).toBe('new-plugin-token');
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
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
|
+
jest.mock('axios');
|
|
13
|
+
|
|
14
|
+
const pluginHandler = require('../../handlers/plugin');
|
|
15
|
+
const { CacheModel } = require('../../models/cacheModel');
|
|
16
|
+
const { AccountDataModel } = require('../../models/accountDataModel');
|
|
17
|
+
const axios = require('axios');
|
|
18
|
+
const { sequelize } = require('../../models/sequelize');
|
|
19
|
+
|
|
20
|
+
describe('Plugin Handler', () => {
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
process.env.HASH_KEY = 'unit-test-hash-key';
|
|
23
|
+
await CacheModel.sync({ force: true });
|
|
24
|
+
await AccountDataModel.sync({ force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await CacheModel.destroy({ where: {} });
|
|
29
|
+
await AccountDataModel.destroy({ where: {} });
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await sequelize.close();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getPluginAsyncTasks', () => {
|
|
38
|
+
test('should retrieve async task status by IDs from CacheModel', async () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
await CacheModel.create({
|
|
41
|
+
id: 'user-123-task-1',
|
|
42
|
+
status: 'processing',
|
|
43
|
+
userId: 'user-123',
|
|
44
|
+
cacheKey: 'pluginTask-googleDrive'
|
|
45
|
+
});
|
|
46
|
+
await CacheModel.create({
|
|
47
|
+
id: 'user-123-task-2',
|
|
48
|
+
status: 'completed',
|
|
49
|
+
userId: 'user-123',
|
|
50
|
+
cacheKey: 'pluginTask-piiRedaction'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
55
|
+
asyncTaskIds: ['user-123-task-1', 'user-123-task-2']
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(result).toHaveLength(2);
|
|
60
|
+
expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
|
|
61
|
+
expect(result).toContainEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'completed' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should return empty array when no matching tasks found', async () => {
|
|
65
|
+
// Arrange - no tasks created
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
69
|
+
asyncTaskIds: ['non-existent-task-1', 'non-existent-task-2']
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should filter and return only tasks with matching IDs', async () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
await CacheModel.create({
|
|
79
|
+
id: 'user-123-task-1',
|
|
80
|
+
status: 'processing',
|
|
81
|
+
userId: 'user-123',
|
|
82
|
+
cacheKey: 'pluginTask-googleDrive'
|
|
83
|
+
});
|
|
84
|
+
await CacheModel.create({
|
|
85
|
+
id: 'user-456-task-2',
|
|
86
|
+
status: 'completed',
|
|
87
|
+
userId: 'user-456',
|
|
88
|
+
cacheKey: 'pluginTask-piiRedaction'
|
|
89
|
+
});
|
|
90
|
+
await CacheModel.create({
|
|
91
|
+
id: 'user-789-task-3',
|
|
92
|
+
status: 'failed',
|
|
93
|
+
userId: 'user-789',
|
|
94
|
+
cacheKey: 'pluginTask-other'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Act - only request tasks for user-123 and user-789
|
|
98
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
99
|
+
asyncTaskIds: ['user-123-task-1', 'user-789-task-3']
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
expect(result).toHaveLength(2);
|
|
104
|
+
expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
|
|
105
|
+
expect(result).toContainEqual({ cacheKey: 'pluginTask-other', status: 'failed' });
|
|
106
|
+
expect(result).not.toContainEqual(expect.objectContaining({ cacheKey: 'pluginTask-piiRedaction' }));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should automatically remove completed tasks from cache after retrieval', async () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
await CacheModel.create({
|
|
112
|
+
id: 'user-123-completed-task',
|
|
113
|
+
status: 'completed',
|
|
114
|
+
userId: 'user-123',
|
|
115
|
+
cacheKey: 'pluginTask-googleDrive'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Act
|
|
119
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
120
|
+
asyncTaskIds: ['user-123-completed-task']
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Assert - result should contain the task
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'completed' });
|
|
126
|
+
|
|
127
|
+
// Verify task was removed from cache
|
|
128
|
+
const remainingTask = await CacheModel.findByPk('user-123-completed-task');
|
|
129
|
+
expect(remainingTask).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should automatically remove failed tasks from cache after retrieval', async () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
await CacheModel.create({
|
|
135
|
+
id: 'user-123-failed-task',
|
|
136
|
+
status: 'failed',
|
|
137
|
+
userId: 'user-123',
|
|
138
|
+
cacheKey: 'pluginTask-piiRedaction'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Act
|
|
142
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
143
|
+
asyncTaskIds: ['user-123-failed-task']
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Assert - result should contain the task
|
|
147
|
+
expect(result).toHaveLength(1);
|
|
148
|
+
expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'failed' });
|
|
149
|
+
|
|
150
|
+
// Verify task was removed from cache
|
|
151
|
+
const remainingTask = await CacheModel.findByPk('user-123-failed-task');
|
|
152
|
+
expect(remainingTask).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should preserve pending tasks in cache after retrieval', async () => {
|
|
156
|
+
// Arrange
|
|
157
|
+
await CacheModel.create({
|
|
158
|
+
id: 'user-123-pending-task',
|
|
159
|
+
status: 'pending',
|
|
160
|
+
userId: 'user-123',
|
|
161
|
+
cacheKey: 'pluginTask-googleDrive'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
166
|
+
asyncTaskIds: ['user-123-pending-task']
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Assert - result should contain the task
|
|
170
|
+
expect(result).toHaveLength(1);
|
|
171
|
+
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'pending' });
|
|
172
|
+
|
|
173
|
+
// Verify task was NOT removed from cache
|
|
174
|
+
const remainingTask = await CacheModel.findByPk('user-123-pending-task');
|
|
175
|
+
expect(remainingTask).not.toBeNull();
|
|
176
|
+
expect(remainingTask.status).toBe('pending');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should preserve processing tasks in cache after retrieval', async () => {
|
|
180
|
+
// Arrange
|
|
181
|
+
await CacheModel.create({
|
|
182
|
+
id: 'user-123-processing-task',
|
|
183
|
+
status: 'processing',
|
|
184
|
+
userId: 'user-123',
|
|
185
|
+
cacheKey: 'pluginTask-piiRedaction'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Act
|
|
189
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
190
|
+
asyncTaskIds: ['user-123-processing-task']
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Assert - result should contain the task
|
|
194
|
+
expect(result).toHaveLength(1);
|
|
195
|
+
expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'processing' });
|
|
196
|
+
|
|
197
|
+
// Verify task was NOT removed from cache
|
|
198
|
+
const remainingTask = await CacheModel.findByPk('user-123-processing-task');
|
|
199
|
+
expect(remainingTask).not.toBeNull();
|
|
200
|
+
expect(remainingTask.status).toBe('processing');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should handle mixed task statuses - remove completed/failed but preserve pending/processing', async () => {
|
|
204
|
+
// Arrange
|
|
205
|
+
await CacheModel.create({
|
|
206
|
+
id: 'task-completed',
|
|
207
|
+
status: 'completed',
|
|
208
|
+
userId: 'user-123',
|
|
209
|
+
cacheKey: 'pluginTask-1'
|
|
210
|
+
});
|
|
211
|
+
await CacheModel.create({
|
|
212
|
+
id: 'task-failed',
|
|
213
|
+
status: 'failed',
|
|
214
|
+
userId: 'user-123',
|
|
215
|
+
cacheKey: 'pluginTask-2'
|
|
216
|
+
});
|
|
217
|
+
await CacheModel.create({
|
|
218
|
+
id: 'task-pending',
|
|
219
|
+
status: 'pending',
|
|
220
|
+
userId: 'user-123',
|
|
221
|
+
cacheKey: 'pluginTask-3'
|
|
222
|
+
});
|
|
223
|
+
await CacheModel.create({
|
|
224
|
+
id: 'task-processing',
|
|
225
|
+
status: 'processing',
|
|
226
|
+
userId: 'user-123',
|
|
227
|
+
cacheKey: 'pluginTask-4'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Act
|
|
231
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
232
|
+
asyncTaskIds: ['task-completed', 'task-failed', 'task-pending', 'task-processing']
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Assert - all tasks should be in result
|
|
236
|
+
expect(result).toHaveLength(4);
|
|
237
|
+
|
|
238
|
+
// Verify completed and failed tasks were removed
|
|
239
|
+
expect(await CacheModel.findByPk('task-completed')).toBeNull();
|
|
240
|
+
expect(await CacheModel.findByPk('task-failed')).toBeNull();
|
|
241
|
+
|
|
242
|
+
// Verify pending and processing tasks were preserved
|
|
243
|
+
expect(await CacheModel.findByPk('task-pending')).not.toBeNull();
|
|
244
|
+
expect(await CacheModel.findByPk('task-processing')).not.toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('should handle empty asyncTaskIds array', async () => {
|
|
248
|
+
// Arrange
|
|
249
|
+
await CacheModel.create({
|
|
250
|
+
id: 'some-task',
|
|
251
|
+
status: 'completed',
|
|
252
|
+
userId: 'user-123',
|
|
253
|
+
cacheKey: 'pluginTask-test'
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Act
|
|
257
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
258
|
+
asyncTaskIds: []
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Assert
|
|
262
|
+
expect(result).toEqual([]);
|
|
263
|
+
|
|
264
|
+
// Verify existing task was not touched
|
|
265
|
+
const existingTask = await CacheModel.findByPk('some-task');
|
|
266
|
+
expect(existingTask).not.toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('should preserve initialized status tasks in cache', async () => {
|
|
270
|
+
// Arrange
|
|
271
|
+
await CacheModel.create({
|
|
272
|
+
id: 'user-123-initialized-task',
|
|
273
|
+
status: 'initialized',
|
|
274
|
+
userId: 'user-123',
|
|
275
|
+
cacheKey: 'pluginTask-googleDrive'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Act
|
|
279
|
+
const result = await pluginHandler.getPluginAsyncTasks({
|
|
280
|
+
asyncTaskIds: ['user-123-initialized-task']
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Assert
|
|
284
|
+
expect(result).toHaveLength(1);
|
|
285
|
+
expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'initialized' });
|
|
286
|
+
|
|
287
|
+
// Verify task was NOT removed from cache
|
|
288
|
+
const remainingTask = await CacheModel.findByPk('user-123-initialized-task');
|
|
289
|
+
expect(remainingTask).not.toBeNull();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('registerPluginAccount', () => {
|
|
294
|
+
test('should register plugin account and persist plugin jwt token in account data', async () => {
|
|
295
|
+
const rcAccountId = '12345';
|
|
296
|
+
const pluginId = 'sync-all-caps';
|
|
297
|
+
|
|
298
|
+
axios.get.mockResolvedValue({
|
|
299
|
+
data: {
|
|
300
|
+
platforms: {
|
|
301
|
+
'plugin.sample': {
|
|
302
|
+
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
303
|
+
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
axios.post.mockResolvedValue({
|
|
309
|
+
data: {
|
|
310
|
+
jwtToken: 'plugin-jwt-token'
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await pluginHandler.registerPluginAccount({
|
|
315
|
+
pluginId,
|
|
316
|
+
rcAccessToken: 'rc-access-token',
|
|
317
|
+
rcAccountId,
|
|
318
|
+
pluginAccess: 'public',
|
|
319
|
+
pluginName: 'plugin.sample'
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.successful).toBe(true);
|
|
323
|
+
expect(axios.post).toHaveBeenCalledWith(
|
|
324
|
+
`https://plugins.example.com/plugin/${pluginId}/auth/register`,
|
|
325
|
+
{
|
|
326
|
+
rcAccessToken: 'rc-access-token',
|
|
327
|
+
rcAccountId
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const accountData = await AccountDataModel.findOne({
|
|
332
|
+
where: {
|
|
333
|
+
rcAccountId,
|
|
334
|
+
platformName: pluginId,
|
|
335
|
+
dataKey: 'pluginData'
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
expect(accountData).not.toBeNull();
|
|
339
|
+
expect(accountData.data.jwtToken).toBe('plugin-jwt-token');
|
|
340
|
+
expect(accountData.data.endpointUrl).toBe(`https://plugins.example.com/plugin/${pluginId}`);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('should throw when register API does not return jwt token', async () => {
|
|
344
|
+
const rcAccountId = '12345';
|
|
345
|
+
const pluginId = 'sync-all-caps';
|
|
346
|
+
|
|
347
|
+
axios.get.mockResolvedValue({
|
|
348
|
+
data: {
|
|
349
|
+
platforms: {
|
|
350
|
+
'plugin.sample': {
|
|
351
|
+
endpointUrl: `https://plugins.example.com/plugin/${pluginId}`,
|
|
352
|
+
userRegisterEndpointUrl: `https://plugins.example.com/plugin/${pluginId}/auth/register`
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
axios.post.mockResolvedValue({ data: {} });
|
|
358
|
+
|
|
359
|
+
await expect(pluginHandler.registerPluginAccount({
|
|
360
|
+
pluginId,
|
|
361
|
+
rcAccessToken: 'rc-access-token',
|
|
362
|
+
rcAccountId,
|
|
363
|
+
pluginAccess: 'public',
|
|
364
|
+
pluginName: 'plugin.sample'
|
|
365
|
+
})).rejects.toThrow('Plugin register API did not return jwtToken');
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('token header helper', () => {
|
|
370
|
+
test('should parse refreshed jwt token from response headers', () => {
|
|
371
|
+
const token = pluginHandler.getRefreshedJwtTokenFromHeaders({
|
|
372
|
+
headers: {
|
|
373
|
+
'x-refreshed-jwt-token': 'new-plugin-token'
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
expect(token).toBe('new-plugin-token');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|