@friggframework/admin-scripts 2.0.0--canary.517.7e78259.0 → 2.0.0--canary.517.2239974.0
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/package.json +6 -6
- package/src/application/__tests__/admin-frigg-commands.test.js +81 -295
- package/src/application/__tests__/admin-script-base.test.js +0 -91
- package/src/application/__tests__/script-runner.test.js +0 -133
- package/src/application/__tests__/validate-script-input.test.js +196 -0
- package/src/application/admin-frigg-commands.js +15 -78
- package/src/application/admin-script-base.js +0 -20
- package/src/application/script-runner.js +1 -111
- package/src/application/validate-script-input.js +116 -0
- package/src/builtins/__tests__/integration-health-check.test.js +27 -25
- package/src/builtins/__tests__/oauth-token-refresh.test.js +17 -15
- package/src/builtins/integration-health-check.js +3 -3
- package/src/builtins/oauth-token-refresh.js +2 -2
- package/src/infrastructure/__tests__/admin-script-router.test.js +2 -2
- package/src/infrastructure/admin-script-router.js +28 -14
- package/src/infrastructure/script-executor-handler.js +18 -35
|
@@ -29,10 +29,9 @@ class ScriptRunner {
|
|
|
29
29
|
* @param {string} options.executionId - Reuse existing AdminProcess record ID (NOT the Lambda execution ID).
|
|
30
30
|
* This is the database ID from the AdminProcess collection/table that tracks script executions.
|
|
31
31
|
* Pass this when resuming a queued execution to continue using the same execution record.
|
|
32
|
-
* @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
|
|
33
32
|
*/
|
|
34
33
|
async execute(scriptName, params = {}, options = {}) {
|
|
35
|
-
const { trigger, audit = {}, executionId: existingExecutionId
|
|
34
|
+
const { trigger, audit = {}, executionId: existingExecutionId } = options;
|
|
36
35
|
|
|
37
36
|
if (!trigger) {
|
|
38
37
|
throw new Error('options.trigger is required (MANUAL | SCHEDULED | QUEUE)');
|
|
@@ -49,11 +48,6 @@ class ScriptRunner {
|
|
|
49
48
|
);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
// Dry-run mode: validate and return preview without executing
|
|
53
|
-
if (dryRun) {
|
|
54
|
-
return this.createDryRunPreview(scriptName, definition, params);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
let executionId = existingExecutionId;
|
|
58
52
|
|
|
59
53
|
// Create execution record if not provided
|
|
@@ -141,110 +135,6 @@ class ScriptRunner {
|
|
|
141
135
|
};
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Create dry-run preview without executing the script
|
|
147
|
-
* Validates inputs and shows what would be executed
|
|
148
|
-
*
|
|
149
|
-
* @param {string} scriptName - Script name
|
|
150
|
-
* @param {Object} definition - Script definition
|
|
151
|
-
* @param {Object} params - Input parameters
|
|
152
|
-
* @returns {Object} Dry-run preview
|
|
153
|
-
*/
|
|
154
|
-
createDryRunPreview(scriptName, definition, params) {
|
|
155
|
-
const validation = this.validateParams(definition, params);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
dryRun: true,
|
|
159
|
-
status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
|
|
160
|
-
scriptName,
|
|
161
|
-
preview: {
|
|
162
|
-
script: {
|
|
163
|
-
name: definition.name,
|
|
164
|
-
version: definition.version,
|
|
165
|
-
description: definition.description,
|
|
166
|
-
requireIntegrationInstance: definition.config?.requireIntegrationInstance || false,
|
|
167
|
-
},
|
|
168
|
-
input: params,
|
|
169
|
-
inputSchema: definition.inputSchema || null,
|
|
170
|
-
validation,
|
|
171
|
-
},
|
|
172
|
-
message: validation.valid
|
|
173
|
-
? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
|
|
174
|
-
: `Dry-run validation failed: ${validation.errors.join(', ')}`,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Validate parameters against script's input schema
|
|
180
|
-
*
|
|
181
|
-
* @param {Object} definition - Script definition
|
|
182
|
-
* @param {Object} params - Input parameters
|
|
183
|
-
* @returns {Object} Validation result { valid, errors }
|
|
184
|
-
*/
|
|
185
|
-
validateParams(definition, params) {
|
|
186
|
-
const errors = [];
|
|
187
|
-
const schema = definition.inputSchema;
|
|
188
|
-
|
|
189
|
-
if (!schema) {
|
|
190
|
-
return { valid: true, errors: [] };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Check required fields
|
|
194
|
-
if (schema.required && Array.isArray(schema.required)) {
|
|
195
|
-
for (const field of schema.required) {
|
|
196
|
-
if (params[field] === undefined || params[field] === null) {
|
|
197
|
-
errors.push(`Missing required parameter: ${field}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Basic type validation for properties
|
|
203
|
-
if (schema.properties) {
|
|
204
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
205
|
-
const value = params[key];
|
|
206
|
-
if (value !== undefined && value !== null) {
|
|
207
|
-
const typeError = this.validateType(key, value, prop);
|
|
208
|
-
if (typeError) {
|
|
209
|
-
errors.push(typeError);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { valid: errors.length === 0, errors };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Validate a single parameter type
|
|
220
|
-
*/
|
|
221
|
-
validateType(key, value, schema) {
|
|
222
|
-
const expectedType = schema.type;
|
|
223
|
-
if (!expectedType) return null;
|
|
224
|
-
|
|
225
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
226
|
-
|
|
227
|
-
if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
|
|
228
|
-
return `Parameter "${key}" must be an integer`;
|
|
229
|
-
}
|
|
230
|
-
if (expectedType === 'number' && typeof value !== 'number') {
|
|
231
|
-
return `Parameter "${key}" must be a number`;
|
|
232
|
-
}
|
|
233
|
-
if (expectedType === 'string' && typeof value !== 'string') {
|
|
234
|
-
return `Parameter "${key}" must be a string`;
|
|
235
|
-
}
|
|
236
|
-
if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
237
|
-
return `Parameter "${key}" must be a boolean`;
|
|
238
|
-
}
|
|
239
|
-
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
240
|
-
return `Parameter "${key}" must be an array`;
|
|
241
|
-
}
|
|
242
|
-
if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
243
|
-
return `Parameter "${key}" must be an object`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
138
|
}
|
|
249
139
|
|
|
250
140
|
function createScriptRunner(params = {}) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Script Input
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Standalone validation for script inputs.
|
|
5
|
+
* Used by the /validate endpoint to preview what would be executed
|
|
6
|
+
* without actually running the script.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate script input parameters against the script's definition and schema.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} scriptFactory - Script factory instance
|
|
13
|
+
* @param {string} scriptName - Name of the script to validate
|
|
14
|
+
* @param {Object} params - Input parameters to validate
|
|
15
|
+
* @returns {Object} Validation preview result
|
|
16
|
+
*/
|
|
17
|
+
function validateScriptInput(scriptFactory, scriptName, params = {}) {
|
|
18
|
+
const scriptClass = scriptFactory.get(scriptName);
|
|
19
|
+
const definition = scriptClass.Definition;
|
|
20
|
+
const validation = validateParams(definition, params);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
status: validation.valid ? 'VALID' : 'INVALID',
|
|
24
|
+
scriptName,
|
|
25
|
+
preview: {
|
|
26
|
+
script: {
|
|
27
|
+
name: definition.name,
|
|
28
|
+
version: definition.version,
|
|
29
|
+
description: definition.description,
|
|
30
|
+
requireIntegrationInstance: definition.config?.requireIntegrationInstance || false,
|
|
31
|
+
},
|
|
32
|
+
input: params,
|
|
33
|
+
inputSchema: definition.inputSchema || null,
|
|
34
|
+
validation,
|
|
35
|
+
},
|
|
36
|
+
message: validation.valid
|
|
37
|
+
? 'Validation passed. Script is ready to execute with provided parameters.'
|
|
38
|
+
: `Validation failed: ${validation.errors.join(', ')}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate parameters against a script's input schema.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} definition - Script definition
|
|
46
|
+
* @param {Object} params - Input parameters
|
|
47
|
+
* @returns {Object} { valid: boolean, errors: string[] }
|
|
48
|
+
*/
|
|
49
|
+
function validateParams(definition, params) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const schema = definition.inputSchema;
|
|
52
|
+
|
|
53
|
+
if (!schema) {
|
|
54
|
+
return { valid: true, errors: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check required fields
|
|
58
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
59
|
+
for (const field of schema.required) {
|
|
60
|
+
if (params[field] === undefined || params[field] === null) {
|
|
61
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Basic type validation for properties
|
|
67
|
+
if (schema.properties) {
|
|
68
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
69
|
+
const value = params[key];
|
|
70
|
+
if (value !== undefined && value !== null) {
|
|
71
|
+
const typeError = validateType(key, value, prop);
|
|
72
|
+
if (typeError) {
|
|
73
|
+
errors.push(typeError);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { valid: errors.length === 0, errors };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a single parameter type.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} key - Parameter name
|
|
86
|
+
* @param {*} value - Parameter value
|
|
87
|
+
* @param {Object} schema - JSON Schema property definition
|
|
88
|
+
* @returns {string|null} Error message or null if valid
|
|
89
|
+
*/
|
|
90
|
+
function validateType(key, value, schema) {
|
|
91
|
+
const expectedType = schema.type;
|
|
92
|
+
if (!expectedType) return null;
|
|
93
|
+
|
|
94
|
+
if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
|
|
95
|
+
return `Parameter "${key}" must be an integer`;
|
|
96
|
+
}
|
|
97
|
+
if (expectedType === 'number' && typeof value !== 'number') {
|
|
98
|
+
return `Parameter "${key}" must be a number`;
|
|
99
|
+
}
|
|
100
|
+
if (expectedType === 'string' && typeof value !== 'string') {
|
|
101
|
+
return `Parameter "${key}" must be a string`;
|
|
102
|
+
}
|
|
103
|
+
if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
104
|
+
return `Parameter "${key}" must be a boolean`;
|
|
105
|
+
}
|
|
106
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
107
|
+
return `Parameter "${key}" must be an array`;
|
|
108
|
+
}
|
|
109
|
+
if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
110
|
+
return `Parameter "${key}" must be an object`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { validateScriptInput, validateParams, validateType };
|
|
@@ -52,16 +52,18 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
52
52
|
beforeEach(() => {
|
|
53
53
|
mockContext = {
|
|
54
54
|
log: jest.fn(),
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
integrationRepository: {
|
|
56
|
+
findIntegrations: jest.fn(),
|
|
57
|
+
findIntegrationById: jest.fn(),
|
|
58
|
+
updateIntegrationStatus: jest.fn(),
|
|
59
|
+
},
|
|
57
60
|
instantiate: jest.fn(),
|
|
58
|
-
updateIntegrationStatus: jest.fn(),
|
|
59
61
|
};
|
|
60
62
|
script = new IntegrationHealthCheckScript({ context: mockContext });
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
it('should return empty results when no integrations found', async () => {
|
|
64
|
-
mockContext.
|
|
66
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([]);
|
|
65
67
|
|
|
66
68
|
const result = await script.execute({});
|
|
67
69
|
|
|
@@ -91,7 +93,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
91
93
|
}
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
mockContext.
|
|
96
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
95
97
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
96
98
|
|
|
97
99
|
const result = await script.execute({
|
|
@@ -118,7 +120,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
|
|
121
|
-
mockContext.
|
|
123
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
122
124
|
|
|
123
125
|
const result = await script.execute({
|
|
124
126
|
checkCredentials: true,
|
|
@@ -147,7 +149,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
151
|
|
|
150
|
-
mockContext.
|
|
152
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
151
153
|
|
|
152
154
|
const result = await script.execute({
|
|
153
155
|
checkCredentials: true,
|
|
@@ -182,7 +184,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
182
184
|
}
|
|
183
185
|
};
|
|
184
186
|
|
|
185
|
-
mockContext.
|
|
187
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
186
188
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
187
189
|
|
|
188
190
|
const result = await script.execute({
|
|
@@ -215,9 +217,9 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
215
217
|
}
|
|
216
218
|
};
|
|
217
219
|
|
|
218
|
-
mockContext.
|
|
220
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
219
221
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
220
|
-
mockContext.updateIntegrationStatus.mockResolvedValue(undefined);
|
|
222
|
+
mockContext.integrationRepository.updateIntegrationStatus.mockResolvedValue(undefined);
|
|
221
223
|
|
|
222
224
|
const result = await script.execute({
|
|
223
225
|
checkCredentials: true,
|
|
@@ -226,7 +228,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
226
228
|
});
|
|
227
229
|
|
|
228
230
|
expect(result.healthy).toBe(1);
|
|
229
|
-
expect(mockContext.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ACTIVE');
|
|
231
|
+
expect(mockContext.integrationRepository.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ACTIVE');
|
|
230
232
|
});
|
|
231
233
|
|
|
232
234
|
it('should update integration status to ERROR for unhealthy integrations', async () => {
|
|
@@ -238,8 +240,8 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
238
240
|
}
|
|
239
241
|
};
|
|
240
242
|
|
|
241
|
-
mockContext.
|
|
242
|
-
mockContext.updateIntegrationStatus.mockResolvedValue(undefined);
|
|
243
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
244
|
+
mockContext.integrationRepository.updateIntegrationStatus.mockResolvedValue(undefined);
|
|
243
245
|
|
|
244
246
|
const result = await script.execute({
|
|
245
247
|
checkCredentials: true,
|
|
@@ -248,7 +250,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
248
250
|
});
|
|
249
251
|
|
|
250
252
|
expect(result.unhealthy).toBe(1);
|
|
251
|
-
expect(mockContext.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ERROR');
|
|
253
|
+
expect(mockContext.integrationRepository.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ERROR');
|
|
252
254
|
});
|
|
253
255
|
|
|
254
256
|
it('should not update status when updateStatus is false', async () => {
|
|
@@ -271,7 +273,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
271
273
|
}
|
|
272
274
|
};
|
|
273
275
|
|
|
274
|
-
mockContext.
|
|
276
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
275
277
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
276
278
|
|
|
277
279
|
await script.execute({
|
|
@@ -280,7 +282,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
280
282
|
updateStatus: false
|
|
281
283
|
});
|
|
282
284
|
|
|
283
|
-
expect(mockContext.updateIntegrationStatus).not.toHaveBeenCalled();
|
|
285
|
+
expect(mockContext.integrationRepository.updateIntegrationStatus).not.toHaveBeenCalled();
|
|
284
286
|
});
|
|
285
287
|
|
|
286
288
|
it('should handle status update failures gracefully', async () => {
|
|
@@ -303,9 +305,9 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
303
305
|
}
|
|
304
306
|
};
|
|
305
307
|
|
|
306
|
-
mockContext.
|
|
308
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
307
309
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
308
|
-
mockContext.updateIntegrationStatus.mockRejectedValue(new Error('Update failed'));
|
|
310
|
+
mockContext.integrationRepository.updateIntegrationStatus.mockRejectedValue(new Error('Update failed'));
|
|
309
311
|
|
|
310
312
|
const result = await script.execute({
|
|
311
313
|
checkCredentials: true,
|
|
@@ -331,7 +333,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
331
333
|
config: { type: 'salesforce', credentials: { access_token: 'token2' } }
|
|
332
334
|
};
|
|
333
335
|
|
|
334
|
-
mockContext.findIntegrationById.mockImplementation((id) => {
|
|
336
|
+
mockContext.integrationRepository.findIntegrationById.mockImplementation((id) => {
|
|
335
337
|
if (id === 'int-1') return Promise.resolve(integration1);
|
|
336
338
|
if (id === 'int-2') return Promise.resolve(integration2);
|
|
337
339
|
return Promise.reject(new Error('Not found'));
|
|
@@ -343,9 +345,9 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
343
345
|
checkConnectivity: false
|
|
344
346
|
});
|
|
345
347
|
|
|
346
|
-
expect(mockContext.findIntegrationById).toHaveBeenCalledWith('int-1');
|
|
347
|
-
expect(mockContext.findIntegrationById).toHaveBeenCalledWith('int-2');
|
|
348
|
-
expect(mockContext.
|
|
348
|
+
expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-1');
|
|
349
|
+
expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-2');
|
|
350
|
+
expect(mockContext.integrationRepository.findIntegrations).not.toHaveBeenCalled();
|
|
349
351
|
expect(result.results).toHaveLength(2);
|
|
350
352
|
});
|
|
351
353
|
|
|
@@ -361,7 +363,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
361
363
|
}
|
|
362
364
|
};
|
|
363
365
|
|
|
364
|
-
mockContext.
|
|
366
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
365
367
|
mockContext.instantiate.mockRejectedValue(new Error('Instantiation failed'));
|
|
366
368
|
|
|
367
369
|
const result = await script.execute({
|
|
@@ -391,7 +393,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
391
393
|
}
|
|
392
394
|
};
|
|
393
395
|
|
|
394
|
-
mockContext.
|
|
396
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
395
397
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
396
398
|
|
|
397
399
|
const result = await script.execute({
|
|
@@ -415,7 +417,7 @@ describe('IntegrationHealthCheckScript', () => {
|
|
|
415
417
|
}
|
|
416
418
|
};
|
|
417
419
|
|
|
418
|
-
mockContext.
|
|
420
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
419
421
|
|
|
420
422
|
const result = await script.execute({
|
|
421
423
|
checkCredentials: true,
|
|
@@ -46,15 +46,17 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
46
46
|
beforeEach(() => {
|
|
47
47
|
mockContext = {
|
|
48
48
|
log: jest.fn(),
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
integrationRepository: {
|
|
50
|
+
findIntegrations: jest.fn(),
|
|
51
|
+
findIntegrationById: jest.fn(),
|
|
52
|
+
},
|
|
51
53
|
instantiate: jest.fn(),
|
|
52
54
|
};
|
|
53
55
|
script = new OAuthTokenRefreshScript({ context: mockContext });
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
it('should return empty results when no integrations found', async () => {
|
|
57
|
-
mockContext.
|
|
59
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([]);
|
|
58
60
|
|
|
59
61
|
const result = await script.execute({});
|
|
60
62
|
|
|
@@ -70,7 +72,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
70
72
|
id: 'int-1',
|
|
71
73
|
config: {} // No credentials
|
|
72
74
|
};
|
|
73
|
-
mockContext.
|
|
75
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
74
76
|
|
|
75
77
|
const result = await script.execute({});
|
|
76
78
|
|
|
@@ -93,7 +95,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
};
|
|
96
|
-
mockContext.
|
|
98
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
97
99
|
|
|
98
100
|
const result = await script.execute({});
|
|
99
101
|
|
|
@@ -116,7 +118,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
};
|
|
119
|
-
mockContext.
|
|
121
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
120
122
|
|
|
121
123
|
const result = await script.execute({
|
|
122
124
|
expiryThresholdHours: 24
|
|
@@ -150,7 +152,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
150
152
|
}
|
|
151
153
|
};
|
|
152
154
|
|
|
153
|
-
mockContext.
|
|
155
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
154
156
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
155
157
|
|
|
156
158
|
const result = await script.execute({
|
|
@@ -178,7 +180,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
178
180
|
}
|
|
179
181
|
};
|
|
180
182
|
|
|
181
|
-
mockContext.
|
|
183
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
182
184
|
|
|
183
185
|
const result = await script.execute({
|
|
184
186
|
expiryThresholdHours: 24,
|
|
@@ -215,7 +217,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
215
217
|
}
|
|
216
218
|
};
|
|
217
219
|
|
|
218
|
-
mockContext.
|
|
220
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
219
221
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
220
222
|
|
|
221
223
|
const result = await script.execute({
|
|
@@ -251,7 +253,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
251
253
|
}
|
|
252
254
|
};
|
|
253
255
|
|
|
254
|
-
mockContext.
|
|
256
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
255
257
|
mockContext.instantiate.mockResolvedValue(mockInstance);
|
|
256
258
|
|
|
257
259
|
const result = await script.execute({
|
|
@@ -276,7 +278,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
276
278
|
config: { credentials: { access_token: 'token2' } }
|
|
277
279
|
};
|
|
278
280
|
|
|
279
|
-
mockContext.findIntegrationById.mockImplementation((id) => {
|
|
281
|
+
mockContext.integrationRepository.findIntegrationById.mockImplementation((id) => {
|
|
280
282
|
if (id === 'int-1') return Promise.resolve(integration1);
|
|
281
283
|
if (id === 'int-2') return Promise.resolve(integration2);
|
|
282
284
|
return Promise.reject(new Error('Not found'));
|
|
@@ -286,9 +288,9 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
286
288
|
integrationIds: ['int-1', 'int-2']
|
|
287
289
|
});
|
|
288
290
|
|
|
289
|
-
expect(mockContext.findIntegrationById).toHaveBeenCalledWith('int-1');
|
|
290
|
-
expect(mockContext.findIntegrationById).toHaveBeenCalledWith('int-2');
|
|
291
|
-
expect(mockContext.
|
|
291
|
+
expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-1');
|
|
292
|
+
expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-2');
|
|
293
|
+
expect(mockContext.integrationRepository.findIntegrations).not.toHaveBeenCalled();
|
|
292
294
|
expect(result.details).toHaveLength(2);
|
|
293
295
|
});
|
|
294
296
|
|
|
@@ -303,7 +305,7 @@ describe('OAuthTokenRefreshScript', () => {
|
|
|
303
305
|
}
|
|
304
306
|
};
|
|
305
307
|
|
|
306
|
-
mockContext.
|
|
308
|
+
mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
|
|
307
309
|
mockContext.instantiate.mockRejectedValue(new Error('Instantiation failed'));
|
|
308
310
|
|
|
309
311
|
const result = await script.execute({
|
|
@@ -94,7 +94,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
|
|
|
94
94
|
let integrations;
|
|
95
95
|
if (integrationIds && integrationIds.length > 0) {
|
|
96
96
|
integrations = await Promise.all(
|
|
97
|
-
integrationIds.map(id => this.context.findIntegrationById(id).catch(() => null))
|
|
97
|
+
integrationIds.map(id => this.context.integrationRepository.findIntegrationById(id).catch(() => null))
|
|
98
98
|
);
|
|
99
99
|
integrations = integrations.filter(Boolean);
|
|
100
100
|
} else {
|
|
@@ -123,7 +123,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
|
|
|
123
123
|
if (updateStatus && result.status !== 'unknown') {
|
|
124
124
|
try {
|
|
125
125
|
const newStatus = result.status === 'healthy' ? 'ACTIVE' : 'ERROR';
|
|
126
|
-
await this.context.updateIntegrationStatus(integration.id, newStatus);
|
|
126
|
+
await this.context.integrationRepository.updateIntegrationStatus(integration.id, newStatus);
|
|
127
127
|
this.context.log('info', `Updated status for ${integration.id} to ${newStatus}`);
|
|
128
128
|
} catch (error) {
|
|
129
129
|
this.context.log('warn', `Failed to update status for ${integration.id}`, {
|
|
@@ -143,7 +143,7 @@ class IntegrationHealthCheckScript extends AdminScriptBase {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
async getAllIntegrations() {
|
|
146
|
-
return this.context.
|
|
146
|
+
return this.context.integrationRepository.findIntegrations({});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
async checkIntegration(integration, options) {
|
|
@@ -80,7 +80,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
80
80
|
let integrations;
|
|
81
81
|
if (integrationIds && integrationIds.length > 0) {
|
|
82
82
|
integrations = await Promise.all(
|
|
83
|
-
integrationIds.map(id => this.context.findIntegrationById(id).catch(() => null))
|
|
83
|
+
integrationIds.map(id => this.context.integrationRepository.findIntegrationById(id).catch(() => null))
|
|
84
84
|
);
|
|
85
85
|
integrations = integrations.filter(Boolean);
|
|
86
86
|
} else {
|
|
@@ -131,7 +131,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
131
131
|
async getAllIntegrations() {
|
|
132
132
|
// This is a simplified implementation
|
|
133
133
|
// In production, would need pagination for large datasets
|
|
134
|
-
return this.context.
|
|
134
|
+
return this.context.integrationRepository.findIntegrations({});
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
async processIntegration(integration, options) {
|
|
@@ -181,7 +181,7 @@ describe('Admin Script Router', () => {
|
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
expect(response.status).toBe(202);
|
|
184
|
-
expect(response.body.status).toBe('
|
|
184
|
+
expect(response.body.status).toBe('QUEUED');
|
|
185
185
|
expect(response.body.executionId).toBe('exec-456');
|
|
186
186
|
expect(QueuerUtil.send).toHaveBeenCalledWith(
|
|
187
187
|
expect.objectContaining({
|
|
@@ -204,7 +204,7 @@ describe('Admin Script Router', () => {
|
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
expect(response.status).toBe(202);
|
|
207
|
-
expect(response.body.status).toBe('
|
|
207
|
+
expect(response.body.status).toBe('QUEUED');
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
it('should return 404 for non-existent script', async () => {
|