@friggframework/admin-scripts 2.0.0--canary.517.f04156f.0 → 2.0.0--canary.517.300ded3.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 -8
- package/src/application/__tests__/admin-frigg-commands.test.js +18 -18
- package/src/application/__tests__/script-runner.test.js +144 -14
- package/src/application/admin-frigg-commands.js +7 -7
- package/src/application/admin-script-base.js +2 -4
- package/src/application/script-runner.js +121 -127
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +24 -25
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +14 -16
- 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/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
|
@@ -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
|
|
@@ -30,7 +28,7 @@ class ScriptRunner {
|
|
|
30
28
|
* @param {string} options.mode - 'sync' | 'async'
|
|
31
29
|
* @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
|
|
32
30
|
* @param {string} options.executionId - Reuse existing execution ID
|
|
33
|
-
* @param {boolean} options.dryRun -
|
|
31
|
+
* @param {boolean} options.dryRun - Dry-run mode: validate and preview without executing
|
|
34
32
|
*/
|
|
35
33
|
async execute(scriptName, params = {}, options = {}) {
|
|
36
34
|
const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
|
|
@@ -46,11 +44,16 @@ class ScriptRunner {
|
|
|
46
44
|
);
|
|
47
45
|
}
|
|
48
46
|
|
|
47
|
+
// Dry-run mode: validate and return preview without executing
|
|
48
|
+
if (dryRun) {
|
|
49
|
+
return this.createDryRunPreview(scriptName, definition, params);
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
let executionId = existingExecutionId;
|
|
50
53
|
|
|
51
54
|
// Create execution record if not provided
|
|
52
55
|
if (!executionId) {
|
|
53
|
-
const execution = await this.commands.
|
|
56
|
+
const execution = await this.commands.createAdminProcess({
|
|
54
57
|
scriptName,
|
|
55
58
|
scriptVersion: definition.version,
|
|
56
59
|
trigger,
|
|
@@ -64,25 +67,13 @@ class ScriptRunner {
|
|
|
64
67
|
const startTime = new Date();
|
|
65
68
|
|
|
66
69
|
try {
|
|
67
|
-
|
|
68
|
-
if (!dryRun) {
|
|
69
|
-
await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
|
|
70
|
-
}
|
|
70
|
+
await this.commands.updateAdminProcessState(executionId, 'RUNNING');
|
|
71
71
|
|
|
72
72
|
// 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
|
-
}
|
|
73
|
+
const frigg = createAdminFriggCommands({
|
|
74
|
+
executionId,
|
|
75
|
+
integrationFactory: this.integrationFactory,
|
|
76
|
+
});
|
|
86
77
|
|
|
87
78
|
// Create script instance
|
|
88
79
|
const script = this.scriptFactory.createInstance(scriptName, {
|
|
@@ -97,34 +88,15 @@ class ScriptRunner {
|
|
|
97
88
|
const endTime = new Date();
|
|
98
89
|
const durationMs = endTime - startTime;
|
|
99
90
|
|
|
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
|
-
}
|
|
91
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
92
|
+
state: 'COMPLETED',
|
|
93
|
+
output,
|
|
94
|
+
metrics: {
|
|
95
|
+
startTime: startTime.toISOString(),
|
|
96
|
+
endTime: endTime.toISOString(),
|
|
97
|
+
durationMs,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
128
100
|
|
|
129
101
|
return {
|
|
130
102
|
executionId,
|
|
@@ -134,31 +106,26 @@ class ScriptRunner {
|
|
|
134
106
|
metrics: { durationMs },
|
|
135
107
|
};
|
|
136
108
|
} catch (error) {
|
|
137
|
-
// Calculate metrics even on failure
|
|
138
109
|
const endTime = new Date();
|
|
139
110
|
const durationMs = endTime - startTime;
|
|
140
111
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
}
|
|
112
|
+
await this.commands.completeAdminProcess(executionId, {
|
|
113
|
+
state: 'FAILED',
|
|
114
|
+
error: {
|
|
115
|
+
name: error.name,
|
|
116
|
+
message: error.message,
|
|
117
|
+
stack: error.stack,
|
|
118
|
+
},
|
|
119
|
+
metrics: {
|
|
120
|
+
startTime: startTime.toISOString(),
|
|
121
|
+
endTime: endTime.toISOString(),
|
|
122
|
+
durationMs,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
157
125
|
|
|
158
126
|
return {
|
|
159
127
|
executionId,
|
|
160
|
-
|
|
161
|
-
status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
|
|
128
|
+
status: 'FAILED',
|
|
162
129
|
scriptName,
|
|
163
130
|
error: {
|
|
164
131
|
name: error.name,
|
|
@@ -170,80 +137,107 @@ class ScriptRunner {
|
|
|
170
137
|
}
|
|
171
138
|
|
|
172
139
|
/**
|
|
173
|
-
* Create dry-run
|
|
174
|
-
*
|
|
140
|
+
* Create dry-run preview without executing the script
|
|
141
|
+
* Validates inputs and shows what would be executed
|
|
175
142
|
*
|
|
176
|
-
* @param {
|
|
177
|
-
* @
|
|
143
|
+
* @param {string} scriptName - Script name
|
|
144
|
+
* @param {Object} definition - Script definition
|
|
145
|
+
* @param {Object} params - Input parameters
|
|
146
|
+
* @returns {Object} Dry-run preview
|
|
178
147
|
*/
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
148
|
+
createDryRunPreview(scriptName, definition, params) {
|
|
149
|
+
const validation = this.validateParams(definition, params);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
dryRun: true,
|
|
153
|
+
status: validation.valid ? 'DRY_RUN_VALID' : 'DRY_RUN_INVALID',
|
|
154
|
+
scriptName,
|
|
155
|
+
preview: {
|
|
156
|
+
script: {
|
|
157
|
+
name: definition.name,
|
|
158
|
+
version: definition.version,
|
|
159
|
+
description: definition.description,
|
|
160
|
+
requiresIntegrationFactory: definition.config?.requiresIntegrationFactory || false,
|
|
161
|
+
},
|
|
162
|
+
input: params,
|
|
163
|
+
inputSchema: definition.inputSchema || null,
|
|
164
|
+
validation,
|
|
165
|
+
},
|
|
166
|
+
message: validation.valid
|
|
167
|
+
? 'Dry-run validation passed. Script is ready to execute with provided parameters.'
|
|
168
|
+
: `Dry-run validation failed: ${validation.errors.join(', ')}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
188
171
|
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Validate parameters against script's input schema
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} definition - Script definition
|
|
176
|
+
* @param {Object} params - Input parameters
|
|
177
|
+
* @returns {Object} Validation result { valid, errors }
|
|
178
|
+
*/
|
|
179
|
+
validateParams(definition, params) {
|
|
180
|
+
const errors = [];
|
|
181
|
+
const schema = definition.inputSchema;
|
|
191
182
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const instance = await originalInstantiate(integrationId);
|
|
183
|
+
if (!schema) {
|
|
184
|
+
return { valid: true, errors: [] };
|
|
185
|
+
}
|
|
196
186
|
|
|
197
|
-
|
|
198
|
-
|
|
187
|
+
// Check required fields
|
|
188
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
189
|
+
for (const field of schema.required) {
|
|
190
|
+
if (params[field] === undefined || params[field] === null) {
|
|
191
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
199
195
|
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
// Basic type validation for properties
|
|
197
|
+
if (schema.properties) {
|
|
198
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
199
|
+
const value = params[key];
|
|
200
|
+
if (value !== undefined && value !== null) {
|
|
201
|
+
const typeError = this.validateType(key, value, prop);
|
|
202
|
+
if (typeError) {
|
|
203
|
+
errors.push(typeError);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
202
208
|
|
|
203
|
-
return
|
|
209
|
+
return { valid: errors.length === 0, errors };
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* @param {Array} log - Operation log
|
|
210
|
-
* @returns {Object} Summary statistics
|
|
213
|
+
* Validate a single parameter type
|
|
211
214
|
*/
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
databaseWrites: 0,
|
|
216
|
-
httpRequests: 0,
|
|
217
|
-
byOperation: {},
|
|
218
|
-
byModel: {},
|
|
219
|
-
byService: {},
|
|
220
|
-
};
|
|
215
|
+
validateType(key, value, schema) {
|
|
216
|
+
const expectedType = schema.type;
|
|
217
|
+
if (!expectedType) return null;
|
|
221
218
|
|
|
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
|
-
}
|
|
219
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
237
220
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
221
|
+
if (expectedType === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) {
|
|
222
|
+
return `Parameter "${key}" must be an integer`;
|
|
223
|
+
}
|
|
224
|
+
if (expectedType === 'number' && typeof value !== 'number') {
|
|
225
|
+
return `Parameter "${key}" must be a number`;
|
|
226
|
+
}
|
|
227
|
+
if (expectedType === 'string' && typeof value !== 'string') {
|
|
228
|
+
return `Parameter "${key}" must be a string`;
|
|
229
|
+
}
|
|
230
|
+
if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
231
|
+
return `Parameter "${key}" must be a boolean`;
|
|
232
|
+
}
|
|
233
|
+
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
234
|
+
return `Parameter "${key}" must be an array`;
|
|
235
|
+
}
|
|
236
|
+
if (expectedType === 'object' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
237
|
+
return `Parameter "${key}" must be an object`;
|
|
244
238
|
}
|
|
245
239
|
|
|
246
|
-
return
|
|
240
|
+
return null;
|
|
247
241
|
}
|
|
248
242
|
}
|
|
249
243
|
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { validateAdminApiKey } = require('../admin-auth-middleware');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
jest.mock('@friggframework/core/application/commands/admin-script-commands', () => ({
|
|
5
|
-
createAdminScriptCommands: jest.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
9
|
-
|
|
10
|
-
describe('adminAuthMiddleware', () => {
|
|
3
|
+
describe('validateAdminApiKey', () => {
|
|
11
4
|
let mockReq;
|
|
12
5
|
let mockRes;
|
|
13
6
|
let mockNext;
|
|
14
|
-
let
|
|
7
|
+
let originalEnv;
|
|
15
8
|
|
|
16
9
|
beforeEach(() => {
|
|
10
|
+
originalEnv = process.env.ADMIN_API_KEY;
|
|
11
|
+
process.env.ADMIN_API_KEY = 'test-admin-key-123';
|
|
12
|
+
|
|
17
13
|
mockReq = {
|
|
18
14
|
headers: {},
|
|
19
|
-
ip: '127.0.0.1',
|
|
20
15
|
};
|
|
21
16
|
|
|
22
17
|
mockRes = {
|
|
@@ -25,124 +20,66 @@ describe('adminAuthMiddleware', () => {
|
|
|
25
20
|
};
|
|
26
21
|
|
|
27
22
|
mockNext = jest.fn();
|
|
28
|
-
|
|
29
|
-
mockCommands = {
|
|
30
|
-
validateAdminApiKey: jest.fn(),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
createAdminScriptCommands.mockReturnValue(mockCommands);
|
|
34
23
|
});
|
|
35
24
|
|
|
36
25
|
afterEach(() => {
|
|
26
|
+
if (originalEnv) {
|
|
27
|
+
process.env.ADMIN_API_KEY = originalEnv;
|
|
28
|
+
} else {
|
|
29
|
+
delete process.env.ADMIN_API_KEY;
|
|
30
|
+
}
|
|
37
31
|
jest.clearAllMocks();
|
|
38
32
|
});
|
|
39
33
|
|
|
40
|
-
describe('
|
|
41
|
-
it('should reject
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
45
|
-
expect(mockRes.json).toHaveBeenCalledWith({
|
|
46
|
-
error: 'Missing or invalid Authorization header',
|
|
47
|
-
code: 'MISSING_AUTH',
|
|
48
|
-
});
|
|
49
|
-
expect(mockNext).not.toHaveBeenCalled();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should reject request with invalid Authorization format', async () => {
|
|
53
|
-
mockReq.headers.authorization = 'InvalidFormat key123';
|
|
34
|
+
describe('Environment configuration', () => {
|
|
35
|
+
it('should reject when ADMIN_API_KEY not configured', () => {
|
|
36
|
+
delete process.env.ADMIN_API_KEY;
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
56
39
|
|
|
57
40
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
58
41
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
59
|
-
error: '
|
|
60
|
-
|
|
42
|
+
error: 'Unauthorized',
|
|
43
|
+
message: 'Admin API key not configured',
|
|
61
44
|
});
|
|
62
45
|
expect(mockNext).not.toHaveBeenCalled();
|
|
63
46
|
});
|
|
64
47
|
});
|
|
65
48
|
|
|
66
|
-
describe('
|
|
67
|
-
it('should reject request
|
|
68
|
-
mockReq
|
|
69
|
-
mockCommands.validateAdminApiKey.mockResolvedValue({
|
|
70
|
-
error: 401,
|
|
71
|
-
reason: 'Invalid API key',
|
|
72
|
-
code: 'INVALID_API_KEY',
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
49
|
+
describe('Header validation', () => {
|
|
50
|
+
it('should reject request without x-frigg-admin-api-key header', () => {
|
|
51
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
76
52
|
|
|
77
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
|
|
78
53
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
79
54
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
80
|
-
error: '
|
|
81
|
-
|
|
55
|
+
error: 'Unauthorized',
|
|
56
|
+
message: 'x-frigg-admin-api-key header required',
|
|
82
57
|
});
|
|
83
58
|
expect(mockNext).not.toHaveBeenCalled();
|
|
84
59
|
});
|
|
60
|
+
});
|
|
85
61
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
error: 401,
|
|
90
|
-
reason: 'API key has expired',
|
|
91
|
-
code: 'EXPIRED_API_KEY',
|
|
92
|
-
});
|
|
62
|
+
describe('API key validation', () => {
|
|
63
|
+
it('should reject request with invalid API key', () => {
|
|
64
|
+
mockReq.headers['x-frigg-admin-api-key'] = 'invalid-key';
|
|
93
65
|
|
|
94
|
-
|
|
66
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
95
67
|
|
|
96
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
|
|
97
68
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
98
69
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
99
|
-
error: '
|
|
100
|
-
|
|
70
|
+
error: 'Unauthorized',
|
|
71
|
+
message: 'Invalid admin API key',
|
|
101
72
|
});
|
|
102
73
|
expect(mockNext).not.toHaveBeenCalled();
|
|
103
74
|
});
|
|
104
75
|
|
|
105
|
-
it('should accept request with valid API key',
|
|
106
|
-
|
|
107
|
-
mockReq.headers.authorization = `Bearer ${validKey}`;
|
|
108
|
-
mockCommands.validateAdminApiKey.mockResolvedValue({
|
|
109
|
-
valid: true,
|
|
110
|
-
apiKey: {
|
|
111
|
-
id: 'key-id-1',
|
|
112
|
-
name: 'test-key',
|
|
113
|
-
keyLast4: 'e123',
|
|
114
|
-
},
|
|
115
|
-
});
|
|
76
|
+
it('should accept request with valid API key', () => {
|
|
77
|
+
mockReq.headers['x-frigg-admin-api-key'] = 'test-admin-key-123';
|
|
116
78
|
|
|
117
|
-
|
|
79
|
+
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
118
80
|
|
|
119
|
-
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith(validKey);
|
|
120
|
-
expect(mockReq.adminApiKey).toBeDefined();
|
|
121
|
-
expect(mockReq.adminApiKey.name).toBe('test-key');
|
|
122
|
-
expect(mockReq.adminAudit).toBeDefined();
|
|
123
|
-
expect(mockReq.adminAudit.apiKeyName).toBe('test-key');
|
|
124
|
-
expect(mockReq.adminAudit.apiKeyLast4).toBe('e123');
|
|
125
|
-
expect(mockReq.adminAudit.ipAddress).toBe('127.0.0.1');
|
|
126
81
|
expect(mockNext).toHaveBeenCalled();
|
|
127
82
|
expect(mockRes.status).not.toHaveBeenCalled();
|
|
128
83
|
});
|
|
129
84
|
});
|
|
130
|
-
|
|
131
|
-
describe('Error handling', () => {
|
|
132
|
-
it('should handle validation errors gracefully', async () => {
|
|
133
|
-
mockReq.headers.authorization = 'Bearer some-key';
|
|
134
|
-
mockCommands.validateAdminApiKey.mockRejectedValue(
|
|
135
|
-
new Error('Database error')
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
139
|
-
|
|
140
|
-
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
141
|
-
expect(mockRes.json).toHaveBeenCalledWith({
|
|
142
|
-
error: 'Authentication failed',
|
|
143
|
-
code: 'AUTH_ERROR',
|
|
144
|
-
});
|
|
145
|
-
expect(mockNext).not.toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
85
|
});
|
|
@@ -4,13 +4,8 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
|
|
|
4
4
|
|
|
5
5
|
// Mock dependencies
|
|
6
6
|
jest.mock('../admin-auth-middleware', () => ({
|
|
7
|
-
|
|
8
|
-
// Mock auth -
|
|
9
|
-
req.adminAudit = {
|
|
10
|
-
apiKeyName: 'test-key',
|
|
11
|
-
apiKeyLast4: '1234',
|
|
12
|
-
ipAddress: '127.0.0.1',
|
|
13
|
-
};
|
|
7
|
+
validateAdminApiKey: (req, res, next) => {
|
|
8
|
+
// Mock auth - no audit trail with simplified auth
|
|
14
9
|
next();
|
|
15
10
|
},
|
|
16
11
|
}));
|
|
@@ -59,8 +54,8 @@ describe('Admin Script Router', () => {
|
|
|
59
54
|
};
|
|
60
55
|
|
|
61
56
|
mockCommands = {
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
createAdminProcess: jest.fn(),
|
|
58
|
+
findAdminProcessById: jest.fn(),
|
|
64
59
|
findRecentExecutions: jest.fn(),
|
|
65
60
|
};
|
|
66
61
|
|
|
@@ -143,7 +138,7 @@ describe('Admin Script Router', () => {
|
|
|
143
138
|
});
|
|
144
139
|
});
|
|
145
140
|
|
|
146
|
-
describe('POST /admin/scripts/:scriptName
|
|
141
|
+
describe('POST /admin/scripts/:scriptName', () => {
|
|
147
142
|
it('should execute script synchronously', async () => {
|
|
148
143
|
mockRunner.execute.mockResolvedValue({
|
|
149
144
|
executionId: 'exec-123',
|
|
@@ -154,7 +149,7 @@ describe('Admin Script Router', () => {
|
|
|
154
149
|
});
|
|
155
150
|
|
|
156
151
|
const response = await request(app)
|
|
157
|
-
.post('/admin/scripts/test-script
|
|
152
|
+
.post('/admin/scripts/test-script')
|
|
158
153
|
.send({
|
|
159
154
|
params: { foo: 'bar' },
|
|
160
155
|
mode: 'sync',
|
|
@@ -174,12 +169,12 @@ describe('Admin Script Router', () => {
|
|
|
174
169
|
});
|
|
175
170
|
|
|
176
171
|
it('should queue script for async execution', async () => {
|
|
177
|
-
mockCommands.
|
|
172
|
+
mockCommands.createAdminProcess.mockResolvedValue({
|
|
178
173
|
id: 'exec-456',
|
|
179
174
|
});
|
|
180
175
|
|
|
181
176
|
const response = await request(app)
|
|
182
|
-
.post('/admin/scripts/test-script
|
|
177
|
+
.post('/admin/scripts/test-script')
|
|
183
178
|
.send({
|
|
184
179
|
params: { foo: 'bar' },
|
|
185
180
|
mode: 'async',
|
|
@@ -198,12 +193,12 @@ describe('Admin Script Router', () => {
|
|
|
198
193
|
});
|
|
199
194
|
|
|
200
195
|
it('should default to async mode', async () => {
|
|
201
|
-
mockCommands.
|
|
196
|
+
mockCommands.createAdminProcess.mockResolvedValue({
|
|
202
197
|
id: 'exec-789',
|
|
203
198
|
});
|
|
204
199
|
|
|
205
200
|
const response = await request(app)
|
|
206
|
-
.post('/admin/scripts/test-script
|
|
201
|
+
.post('/admin/scripts/test-script')
|
|
207
202
|
.send({
|
|
208
203
|
params: { foo: 'bar' },
|
|
209
204
|
});
|
|
@@ -216,7 +211,7 @@ describe('Admin Script Router', () => {
|
|
|
216
211
|
mockFactory.has.mockReturnValue(false);
|
|
217
212
|
|
|
218
213
|
const response = await request(app)
|
|
219
|
-
.post('/admin/scripts/non-existent
|
|
214
|
+
.post('/admin/scripts/non-existent')
|
|
220
215
|
.send({
|
|
221
216
|
params: {},
|
|
222
217
|
});
|
|
@@ -226,15 +221,15 @@ describe('Admin Script Router', () => {
|
|
|
226
221
|
});
|
|
227
222
|
});
|
|
228
223
|
|
|
229
|
-
describe('GET /admin/executions/:executionId', () => {
|
|
224
|
+
describe('GET /admin/scripts/:scriptName/executions/:executionId', () => {
|
|
230
225
|
it('should return execution details', async () => {
|
|
231
|
-
mockCommands.
|
|
226
|
+
mockCommands.findAdminProcessById.mockResolvedValue({
|
|
232
227
|
id: 'exec-123',
|
|
233
228
|
scriptName: 'test-script',
|
|
234
229
|
status: 'COMPLETED',
|
|
235
230
|
});
|
|
236
231
|
|
|
237
|
-
const response = await request(app).get('/admin/executions/exec-123');
|
|
232
|
+
const response = await request(app).get('/admin/scripts/test-script/executions/exec-123');
|
|
238
233
|
|
|
239
234
|
expect(response.status).toBe(200);
|
|
240
235
|
expect(response.body.id).toBe('exec-123');
|
|
@@ -242,14 +237,14 @@ describe('Admin Script Router', () => {
|
|
|
242
237
|
});
|
|
243
238
|
|
|
244
239
|
it('should return 404 for non-existent execution', async () => {
|
|
245
|
-
mockCommands.
|
|
240
|
+
mockCommands.findAdminProcessById.mockResolvedValue({
|
|
246
241
|
error: 404,
|
|
247
242
|
reason: 'Execution not found',
|
|
248
243
|
code: 'EXECUTION_NOT_FOUND',
|
|
249
244
|
});
|
|
250
245
|
|
|
251
246
|
const response = await request(app).get(
|
|
252
|
-
'/admin/executions/non-existent'
|
|
247
|
+
'/admin/scripts/test-script/executions/non-existent'
|
|
253
248
|
);
|
|
254
249
|
|
|
255
250
|
expect(response.status).toBe(404);
|
|
@@ -257,24 +252,28 @@ describe('Admin Script Router', () => {
|
|
|
257
252
|
});
|
|
258
253
|
});
|
|
259
254
|
|
|
260
|
-
describe('GET /admin/executions', () => {
|
|
261
|
-
it('should list
|
|
255
|
+
describe('GET /admin/scripts/:scriptName/executions', () => {
|
|
256
|
+
it('should list executions for specific script', async () => {
|
|
262
257
|
mockCommands.findRecentExecutions.mockResolvedValue([
|
|
263
258
|
{ id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
|
|
264
259
|
{ id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
|
|
265
260
|
]);
|
|
266
261
|
|
|
267
|
-
const response = await request(app).get('/admin/executions');
|
|
262
|
+
const response = await request(app).get('/admin/scripts/test-script/executions');
|
|
268
263
|
|
|
269
264
|
expect(response.status).toBe(200);
|
|
270
265
|
expect(response.body.executions).toHaveLength(2);
|
|
266
|
+
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|
|
267
|
+
scriptName: 'test-script',
|
|
268
|
+
limit: 50,
|
|
269
|
+
});
|
|
271
270
|
});
|
|
272
271
|
|
|
273
272
|
it('should accept query parameters', async () => {
|
|
274
273
|
mockCommands.findRecentExecutions.mockResolvedValue([]);
|
|
275
274
|
|
|
276
275
|
await request(app).get(
|
|
277
|
-
'/admin/
|
|
276
|
+
'/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
|
|
278
277
|
);
|
|
279
278
|
|
|
280
279
|
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|