@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.35ee143.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/index.js +2 -2
- package/package.json +6 -9
- package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
- package/src/application/__tests__/admin-script-base.test.js +2 -2
- package/src/application/__tests__/script-runner.test.js +146 -16
- package/src/application/admin-frigg-commands.js +8 -8
- package/src/application/admin-script-base.js +3 -5
- package/src/application/script-runner.js +125 -129
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
- package/src/application/use-cases/delete-schedule-use-case.js +108 -0
- package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
- package/src/application/use-cases/index.js +18 -0
- package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
- package/src/builtins/__tests__/integration-health-check.test.js +1 -1
- package/src/builtins/__tests__/oauth-token-refresh.test.js +1 -1
- package/src/builtins/integration-health-check.js +1 -1
- package/src/builtins/oauth-token-refresh.js +1 -1
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +38 -32
- package/src/infrastructure/script-executor-handler.js +2 -2
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- package/src/application/schedule-management-use-case.js +0 -230
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const { getScriptFactory } = require('./script-factory');
|
|
2
2
|
const { createAdminFriggCommands } = require('./admin-frigg-commands');
|
|
3
3
|
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
4
|
-
const { wrapAdminFriggCommandsForDryRun } = require('./dry-run-repository-wrapper');
|
|
5
|
-
const { createDryRunHttpClient, injectDryRunHttpClient } = require('./dry-run-http-interceptor');
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Script Runner
|
|
@@ -29,8 +27,10 @@ class ScriptRunner {
|
|
|
29
27
|
* @param {string} options.trigger - 'MANUAL' | 'SCHEDULED' | 'QUEUE'
|
|
30
28
|
* @param {string} options.mode - 'sync' | 'async'
|
|
31
29
|
* @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
|
|
32
|
-
* @param {string} options.executionId - Reuse existing execution ID
|
|
33
|
-
*
|
|
30
|
+
* @param {string} options.executionId - Reuse existing AdminProcess record ID (NOT the Lambda execution ID).
|
|
31
|
+
* This is the database ID from the AdminProcess collection/table that tracks script executions.
|
|
32
|
+
* Pass this when resuming a queued execution to continue using the same execution record.
|
|
33
|
+
* @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
|
|
34
34
|
*/
|
|
35
35
|
async execute(scriptName, params = {}, options = {}) {
|
|
36
36
|
const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
|
|
@@ -40,17 +40,22 @@ class ScriptRunner {
|
|
|
40
40
|
const definition = scriptClass.Definition;
|
|
41
41
|
|
|
42
42
|
// Validate integrationFactory requirement
|
|
43
|
-
if (definition.config?.
|
|
43
|
+
if (definition.config?.requireIntegrationInstance && !this.integrationFactory) {
|
|
44
44
|
throw new Error(
|
|
45
45
|
`Script "${scriptName}" requires integrationFactory but none was provided`
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Dry-run mode: validate and return preview without executing
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
return this.createDryRunPreview(scriptName, definition, params);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
let executionId = existingExecutionId;
|
|
50
55
|
|
|
51
56
|
// Create execution record if not provided
|
|
52
57
|
if (!executionId) {
|
|
53
|
-
const execution = await this.commands.
|
|
58
|
+
const execution = await this.commands.createAdminProcess({
|
|
54
59
|
scriptName,
|
|
55
60
|
scriptVersion: definition.version,
|
|
56
61
|
trigger,
|
|
@@ -64,25 +69,13 @@ class ScriptRunner {
|
|
|
64
69
|
const startTime = new Date();
|
|
65
70
|
|
|
66
71
|
try {
|
|
67
|
-
|
|
68
|
-
if (!dryRun) {
|
|
69
|
-
await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
|
|
70
|
-
}
|
|
72
|
+
await this.commands.updateAdminProcessState(executionId, 'RUNNING');
|
|
71
73
|
|
|
72
74
|
// Create frigg commands for the script
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Dry-run mode: wrap commands to intercept writes
|
|
78
|
-
frigg = this.createDryRunFriggCommands(operationLog);
|
|
79
|
-
} else {
|
|
80
|
-
// Normal mode: create real commands
|
|
81
|
-
frigg = createAdminFriggCommands({
|
|
82
|
-
executionId,
|
|
83
|
-
integrationFactory: this.integrationFactory,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
75
|
+
const frigg = createAdminFriggCommands({
|
|
76
|
+
executionId,
|
|
77
|
+
integrationFactory: this.integrationFactory,
|
|
78
|
+
});
|
|
86
79
|
|
|
87
80
|
// Create script instance
|
|
88
81
|
const script = this.scriptFactory.createInstance(scriptName, {
|
|
@@ -97,34 +90,15 @@ class ScriptRunner {
|
|
|
97
90
|
const endTime = new Date();
|
|
98
91
|
const durationMs = endTime - startTime;
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Return dry-run preview if in dry-run mode
|
|
114
|
-
if (dryRun) {
|
|
115
|
-
return {
|
|
116
|
-
executionId,
|
|
117
|
-
dryRun: true,
|
|
118
|
-
status: 'DRY_RUN_COMPLETED',
|
|
119
|
-
scriptName,
|
|
120
|
-
preview: {
|
|
121
|
-
operations: operationLog,
|
|
122
|
-
summary: this.summarizeOperations(operationLog),
|
|
123
|
-
scriptOutput: output,
|
|
124
|
-
},
|
|
125
|
-
metrics: { durationMs },
|
|
126
|
-
};
|
|
127
|
-
}
|
|
93
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
94
|
+
state: 'COMPLETED',
|
|
95
|
+
output,
|
|
96
|
+
metrics: {
|
|
97
|
+
startTime: startTime.toISOString(),
|
|
98
|
+
endTime: endTime.toISOString(),
|
|
99
|
+
durationMs,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
128
102
|
|
|
129
103
|
return {
|
|
130
104
|
executionId,
|
|
@@ -134,31 +108,26 @@ class ScriptRunner {
|
|
|
134
108
|
metrics: { durationMs },
|
|
135
109
|
};
|
|
136
110
|
} catch (error) {
|
|
137
|
-
// Calculate metrics even on failure
|
|
138
111
|
const endTime = new Date();
|
|
139
112
|
const durationMs = endTime - startTime;
|
|
140
113
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
}
|
|
114
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
115
|
+
state: 'FAILED',
|
|
116
|
+
error: {
|
|
117
|
+
name: error.name,
|
|
118
|
+
message: error.message,
|
|
119
|
+
stack: error.stack,
|
|
120
|
+
},
|
|
121
|
+
metrics: {
|
|
122
|
+
startTime: startTime.toISOString(),
|
|
123
|
+
endTime: endTime.toISOString(),
|
|
124
|
+
durationMs,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
157
127
|
|
|
158
128
|
return {
|
|
159
129
|
executionId,
|
|
160
|
-
|
|
161
|
-
status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
|
|
130
|
+
status: 'FAILED',
|
|
162
131
|
scriptName,
|
|
163
132
|
error: {
|
|
164
133
|
name: error.name,
|
|
@@ -170,80 +139,107 @@ class ScriptRunner {
|
|
|
170
139
|
}
|
|
171
140
|
|
|
172
141
|
/**
|
|
173
|
-
* Create dry-run
|
|
174
|
-
*
|
|
142
|
+
* Create dry-run preview without executing the script
|
|
143
|
+
* Validates inputs and shows what would be executed
|
|
175
144
|
*
|
|
176
|
-
* @param {
|
|
177
|
-
* @
|
|
145
|
+
* @param {string} scriptName - Script name
|
|
146
|
+
* @param {Object} definition - Script definition
|
|
147
|
+
* @param {Object} params - Input parameters
|
|
148
|
+
* @returns {Object} Dry-run preview
|
|
178
149
|
*/
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
150
|
+
createDryRunPreview(scriptName, definition, params) {
|
|
151
|
+
const validation = this.validateParams(definition, params);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
dryRun: true,
|
|
155
|
+
status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
|
|
156
|
+
scriptName,
|
|
157
|
+
preview: {
|
|
158
|
+
script: {
|
|
159
|
+
name: definition.name,
|
|
160
|
+
version: definition.version,
|
|
161
|
+
description: definition.description,
|
|
162
|
+
requireIntegrationInstance: definition.config?.requireIntegrationInstance || false,
|
|
163
|
+
},
|
|
164
|
+
input: params,
|
|
165
|
+
inputSchema: definition.inputSchema || null,
|
|
166
|
+
validation,
|
|
167
|
+
},
|
|
168
|
+
message: validation.valid
|
|
169
|
+
? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
|
|
170
|
+
: `Dry-run validation failed: ${validation.errors.join(', ')}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
188
173
|
|
|
189
|
-
|
|
190
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Validate parameters against script's input schema
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} definition - Script definition
|
|
178
|
+
* @param {Object} params - Input parameters
|
|
179
|
+
* @returns {Object} Validation result { valid, errors }
|
|
180
|
+
*/
|
|
181
|
+
validateParams(definition, params) {
|
|
182
|
+
const errors = [];
|
|
183
|
+
const schema = definition.inputSchema;
|
|
191
184
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const instance = await originalInstantiate(integrationId);
|
|
185
|
+
if (!schema) {
|
|
186
|
+
return { valid: true, errors: [] };
|
|
187
|
+
}
|
|
196
188
|
|
|
197
|
-
|
|
198
|
-
|
|
189
|
+
// Check required fields
|
|
190
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
191
|
+
for (const field of schema.required) {
|
|
192
|
+
if (params[field] === undefined || params[field] === null) {
|
|
193
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
199
197
|
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// Basic type validation for properties
|
|
199
|
+
if (schema.properties) {
|
|
200
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
201
|
+
const value = params[key];
|
|
202
|
+
if (value !== undefined && value !== null) {
|
|
203
|
+
const typeError = this.validateType(key, value, prop);
|
|
204
|
+
if (typeError) {
|
|
205
|
+
errors.push(typeError);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
202
210
|
|
|
203
|
-
return
|
|
211
|
+
return { valid: errors.length === 0, errors };
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* @param {Array} log - Operation log
|
|
210
|
-
* @returns {Object} Summary statistics
|
|
215
|
+
* Validate a single parameter type
|
|
211
216
|
*/
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
databaseWrites: 0,
|
|
216
|
-
httpRequests: 0,
|
|
217
|
-
byOperation: {},
|
|
218
|
-
byModel: {},
|
|
219
|
-
byService: {},
|
|
220
|
-
};
|
|
217
|
+
validateType(key, value, schema) {
|
|
218
|
+
const expectedType = schema.type;
|
|
219
|
+
if (!expectedType) return null;
|
|
221
220
|
|
|
222
|
-
|
|
223
|
-
// Count by operation type
|
|
224
|
-
const operation = op.operation || op.method || 'UNKNOWN';
|
|
225
|
-
summary.byOperation[operation] = (summary.byOperation[operation] || 0) + 1;
|
|
226
|
-
|
|
227
|
-
// Database operations
|
|
228
|
-
if (op.model) {
|
|
229
|
-
summary.databaseWrites++;
|
|
230
|
-
summary.byModel[op.model] = summary.byModel[op.model] || [];
|
|
231
|
-
summary.byModel[op.model].push({
|
|
232
|
-
operation: op.operation,
|
|
233
|
-
method: op.method,
|
|
234
|
-
timestamp: op.timestamp,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
221
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
237
222
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
223
|
+
if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
|
|
224
|
+
return `Parameter "${key}" must be an integer`;
|
|
225
|
+
}
|
|
226
|
+
if (expectedType === 'number' && typeof value !== 'number') {
|
|
227
|
+
return `Parameter "${key}" must be a number`;
|
|
228
|
+
}
|
|
229
|
+
if (expectedType === 'string' && typeof value !== 'string') {
|
|
230
|
+
return `Parameter "${key}" must be a string`;
|
|
231
|
+
}
|
|
232
|
+
if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
233
|
+
return `Parameter "${key}" must be a boolean`;
|
|
234
|
+
}
|
|
235
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
236
|
+
return `Parameter "${key}" must be an array`;
|
|
237
|
+
}
|
|
238
|
+
if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
239
|
+
return `Parameter "${key}" must be an object`;
|
|
244
240
|
}
|
|
245
241
|
|
|
246
|
-
return
|
|
242
|
+
return null;
|
|
247
243
|
}
|
|
248
244
|
}
|
|
249
245
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const { DeleteScheduleUseCase } = require('../delete-schedule-use-case');
|
|
2
|
+
|
|
3
|
+
describe('DeleteScheduleUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockCommands;
|
|
6
|
+
let mockSchedulerAdapter;
|
|
7
|
+
let mockScriptFactory;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockCommands = {
|
|
11
|
+
deleteSchedule: jest.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
mockSchedulerAdapter = {
|
|
15
|
+
deleteSchedule: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
mockScriptFactory = {
|
|
19
|
+
has: jest.fn(),
|
|
20
|
+
get: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
useCase = new DeleteScheduleUseCase({
|
|
24
|
+
commands: mockCommands,
|
|
25
|
+
schedulerAdapter: mockSchedulerAdapter,
|
|
26
|
+
scriptFactory: mockScriptFactory,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('execute', () => {
|
|
31
|
+
it('should delete schedule and cleanup external scheduler', async () => {
|
|
32
|
+
const deletedSchedule = {
|
|
33
|
+
scriptName: 'test-script',
|
|
34
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
38
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
39
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
40
|
+
deletedCount: 1,
|
|
41
|
+
deleted: deletedSchedule,
|
|
42
|
+
});
|
|
43
|
+
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
44
|
+
|
|
45
|
+
const result = await useCase.execute('test-script');
|
|
46
|
+
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
expect(result.deletedCount).toBe(1);
|
|
49
|
+
expect(result.message).toBe('Schedule override removed');
|
|
50
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not call scheduler when no external rule exists', async () => {
|
|
54
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
55
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
56
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
57
|
+
deletedCount: 1,
|
|
58
|
+
deleted: { scriptName: 'test-script' }, // No externalScheduleId
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await useCase.execute('test-script');
|
|
62
|
+
|
|
63
|
+
expect(result.success).toBe(true);
|
|
64
|
+
expect(mockSchedulerAdapter.deleteSchedule).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle scheduler delete errors gracefully with warning', async () => {
|
|
68
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
69
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
70
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
71
|
+
deletedCount: 1,
|
|
72
|
+
deleted: {
|
|
73
|
+
scriptName: 'test-script',
|
|
74
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
78
|
+
new Error('Scheduler delete failed')
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = await useCase.execute('test-script');
|
|
82
|
+
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
expect(result.schedulerWarning).toBe('Scheduler delete failed');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should return definition schedule as effective after deletion', async () => {
|
|
88
|
+
const definitionSchedule = {
|
|
89
|
+
enabled: true,
|
|
90
|
+
cronExpression: '0 6 * * *',
|
|
91
|
+
timezone: 'America/Los_Angeles',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
95
|
+
mockScriptFactory.get.mockReturnValue({
|
|
96
|
+
Definition: { schedule: definitionSchedule },
|
|
97
|
+
});
|
|
98
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
99
|
+
deletedCount: 1,
|
|
100
|
+
deleted: { scriptName: 'test-script' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await useCase.execute('test-script');
|
|
104
|
+
|
|
105
|
+
expect(result.effectiveSchedule.source).toBe('definition');
|
|
106
|
+
expect(result.effectiveSchedule.enabled).toBe(true);
|
|
107
|
+
expect(result.effectiveSchedule.cronExpression).toBe('0 6 * * *');
|
|
108
|
+
expect(result.effectiveSchedule.timezone).toBe('America/Los_Angeles');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should default timezone to UTC when not in definition', async () => {
|
|
112
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
113
|
+
mockScriptFactory.get.mockReturnValue({
|
|
114
|
+
Definition: { schedule: { enabled: true, cronExpression: '0 6 * * *' } },
|
|
115
|
+
});
|
|
116
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
117
|
+
deletedCount: 1,
|
|
118
|
+
deleted: { scriptName: 'test-script' },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await useCase.execute('test-script');
|
|
122
|
+
|
|
123
|
+
expect(result.effectiveSchedule.timezone).toBe('UTC');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return none as effective when no definition schedule', async () => {
|
|
127
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
128
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
129
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
130
|
+
deletedCount: 1,
|
|
131
|
+
deleted: { scriptName: 'test-script' },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await useCase.execute('test-script');
|
|
135
|
+
|
|
136
|
+
expect(result.effectiveSchedule.source).toBe('none');
|
|
137
|
+
expect(result.effectiveSchedule.enabled).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return correct message when no schedule found', async () => {
|
|
141
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
142
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
143
|
+
mockCommands.deleteSchedule.mockResolvedValue({
|
|
144
|
+
deletedCount: 0,
|
|
145
|
+
deleted: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await useCase.execute('test-script');
|
|
149
|
+
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
expect(result.deletedCount).toBe(0);
|
|
152
|
+
expect(result.message).toBe('No schedule override found');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
156
|
+
mockScriptFactory.has.mockReturnValue(false);
|
|
157
|
+
|
|
158
|
+
await expect(useCase.execute('non-existent'))
|
|
159
|
+
.rejects.toThrow('Script "non-existent" not found');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await useCase.execute('non-existent');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
expect(error.code).toBe('SCRIPT_NOT_FOUND');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { GetEffectiveScheduleUseCase } = require('../get-effective-schedule-use-case');
|
|
2
|
+
|
|
3
|
+
describe('GetEffectiveScheduleUseCase', () => {
|
|
4
|
+
let useCase;
|
|
5
|
+
let mockCommands;
|
|
6
|
+
let mockScriptFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockCommands = {
|
|
10
|
+
getScheduleByScriptName: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
mockScriptFactory = {
|
|
14
|
+
has: jest.fn(),
|
|
15
|
+
get: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
useCase = new GetEffectiveScheduleUseCase({
|
|
19
|
+
commands: mockCommands,
|
|
20
|
+
scriptFactory: mockScriptFactory,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('execute', () => {
|
|
25
|
+
it('should return database schedule when override exists', async () => {
|
|
26
|
+
const dbSchedule = {
|
|
27
|
+
scriptName: 'test-script',
|
|
28
|
+
enabled: true,
|
|
29
|
+
cronExpression: '0 9 * * *',
|
|
30
|
+
timezone: 'UTC',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
34
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
35
|
+
mockCommands.getScheduleByScriptName.mockResolvedValue(dbSchedule);
|
|
36
|
+
|
|
37
|
+
const result = await useCase.execute('test-script');
|
|
38
|
+
|
|
39
|
+
expect(result.source).toBe('database');
|
|
40
|
+
expect(result.schedule).toEqual(dbSchedule);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return definition schedule when no database override', async () => {
|
|
44
|
+
const definitionSchedule = {
|
|
45
|
+
enabled: true,
|
|
46
|
+
cronExpression: '0 12 * * *',
|
|
47
|
+
timezone: 'America/New_York',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
51
|
+
mockScriptFactory.get.mockReturnValue({
|
|
52
|
+
Definition: { schedule: definitionSchedule },
|
|
53
|
+
});
|
|
54
|
+
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
55
|
+
|
|
56
|
+
const result = await useCase.execute('test-script');
|
|
57
|
+
|
|
58
|
+
expect(result.source).toBe('definition');
|
|
59
|
+
expect(result.schedule.enabled).toBe(true);
|
|
60
|
+
expect(result.schedule.cronExpression).toBe('0 12 * * *');
|
|
61
|
+
expect(result.schedule.timezone).toBe('America/New_York');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should default timezone to UTC when not specified in definition', async () => {
|
|
65
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
66
|
+
mockScriptFactory.get.mockReturnValue({
|
|
67
|
+
Definition: { schedule: { enabled: true, cronExpression: '0 12 * * *' } },
|
|
68
|
+
});
|
|
69
|
+
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
70
|
+
|
|
71
|
+
const result = await useCase.execute('test-script');
|
|
72
|
+
|
|
73
|
+
expect(result.schedule.timezone).toBe('UTC');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return none when no schedule configured', async () => {
|
|
77
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
78
|
+
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
79
|
+
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
80
|
+
|
|
81
|
+
const result = await useCase.execute('test-script');
|
|
82
|
+
|
|
83
|
+
expect(result.source).toBe('none');
|
|
84
|
+
expect(result.schedule.enabled).toBe(false);
|
|
85
|
+
expect(result.schedule.scriptName).toBe('test-script');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return none when definition schedule is disabled', async () => {
|
|
89
|
+
mockScriptFactory.has.mockReturnValue(true);
|
|
90
|
+
mockScriptFactory.get.mockReturnValue({
|
|
91
|
+
Definition: { schedule: { enabled: false } },
|
|
92
|
+
});
|
|
93
|
+
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
94
|
+
|
|
95
|
+
const result = await useCase.execute('test-script');
|
|
96
|
+
|
|
97
|
+
expect(result.source).toBe('none');
|
|
98
|
+
expect(result.schedule.enabled).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw SCRIPT_NOT_FOUND error when script does not exist', async () => {
|
|
102
|
+
mockScriptFactory.has.mockReturnValue(false);
|
|
103
|
+
|
|
104
|
+
await expect(useCase.execute('non-existent'))
|
|
105
|
+
.rejects.toThrow('Script "non-existent" not found');
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await useCase.execute('non-existent');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
expect(error.code).toBe('SCRIPT_NOT_FOUND');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|