@friggframework/admin-scripts 2.0.0--canary.517.300ded3.0 → 2.0.0--canary.522.cbd3d5a.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 +8 -6
- package/src/application/__tests__/admin-frigg-commands.test.js +18 -18
- package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
- package/src/application/__tests__/script-runner.test.js +14 -144
- package/src/application/admin-frigg-commands.js +7 -7
- package/src/application/admin-script-base.js +4 -2
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/script-runner.js +127 -121
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +95 -32
- package/src/infrastructure/__tests__/admin-script-router.test.js +25 -24
- package/src/infrastructure/admin-auth-middleware.js +43 -5
- package/src/infrastructure/admin-script-router.js +16 -14
- package/src/infrastructure/script-executor-handler.js +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
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');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Script Runner
|
|
@@ -28,7 +30,7 @@ class ScriptRunner {
|
|
|
28
30
|
* @param {string} options.mode - 'sync' | 'async'
|
|
29
31
|
* @param {Object} options.audit - Audit info { apiKeyName, apiKeyLast4, ipAddress }
|
|
30
32
|
* @param {string} options.executionId - Reuse existing execution ID
|
|
31
|
-
* @param {boolean} options.dryRun -
|
|
33
|
+
* @param {boolean} options.dryRun - Execute in dry-run mode (no writes, log operations)
|
|
32
34
|
*/
|
|
33
35
|
async execute(scriptName, params = {}, options = {}) {
|
|
34
36
|
const { trigger = 'MANUAL', audit = {}, executionId: existingExecutionId, dryRun = false } = options;
|
|
@@ -44,16 +46,11 @@ class ScriptRunner {
|
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
// Dry-run mode: validate and return preview without executing
|
|
48
|
-
if (dryRun) {
|
|
49
|
-
return this.createDryRunPreview(scriptName, definition, params);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
let executionId = existingExecutionId;
|
|
53
50
|
|
|
54
51
|
// Create execution record if not provided
|
|
55
52
|
if (!executionId) {
|
|
56
|
-
const execution = await this.commands.
|
|
53
|
+
const execution = await this.commands.createScriptExecution({
|
|
57
54
|
scriptName,
|
|
58
55
|
scriptVersion: definition.version,
|
|
59
56
|
trigger,
|
|
@@ -67,13 +64,25 @@ class ScriptRunner {
|
|
|
67
64
|
const startTime = new Date();
|
|
68
65
|
|
|
69
66
|
try {
|
|
70
|
-
|
|
67
|
+
// Update status to RUNNING (skip in dry-run)
|
|
68
|
+
if (!dryRun) {
|
|
69
|
+
await this.commands.updateScriptExecutionStatus(executionId, 'RUNNING');
|
|
70
|
+
}
|
|
71
71
|
|
|
72
72
|
// Create frigg commands for the script
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
let frigg;
|
|
74
|
+
let operationLog = [];
|
|
75
|
+
|
|
76
|
+
if (dryRun) {
|
|
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
|
+
}
|
|
77
86
|
|
|
78
87
|
// Create script instance
|
|
79
88
|
const script = this.scriptFactory.createInstance(scriptName, {
|
|
@@ -88,15 +97,34 @@ class ScriptRunner {
|
|
|
88
97
|
const endTime = new Date();
|
|
89
98
|
const durationMs = endTime - startTime;
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
// Complete execution (skip in dry-run)
|
|
101
|
+
if (!dryRun) {
|
|
102
|
+
await this.commands.completeScriptExecution(executionId, {
|
|
103
|
+
status: 'COMPLETED',
|
|
104
|
+
output,
|
|
105
|
+
metrics: {
|
|
106
|
+
startTime: startTime.toISOString(),
|
|
107
|
+
endTime: endTime.toISOString(),
|
|
108
|
+
durationMs,
|
|
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
|
+
}
|
|
100
128
|
|
|
101
129
|
return {
|
|
102
130
|
executionId,
|
|
@@ -106,26 +134,31 @@ class ScriptRunner {
|
|
|
106
134
|
metrics: { durationMs },
|
|
107
135
|
};
|
|
108
136
|
} catch (error) {
|
|
137
|
+
// Calculate metrics even on failure
|
|
109
138
|
const endTime = new Date();
|
|
110
139
|
const durationMs = endTime - startTime;
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
// Record failure (skip in dry-run)
|
|
142
|
+
if (!dryRun) {
|
|
143
|
+
await this.commands.completeScriptExecution(executionId, {
|
|
144
|
+
status: 'FAILED',
|
|
145
|
+
error: {
|
|
146
|
+
name: error.name,
|
|
147
|
+
message: error.message,
|
|
148
|
+
stack: error.stack,
|
|
149
|
+
},
|
|
150
|
+
metrics: {
|
|
151
|
+
startTime: startTime.toISOString(),
|
|
152
|
+
endTime: endTime.toISOString(),
|
|
153
|
+
durationMs,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
125
157
|
|
|
126
158
|
return {
|
|
127
159
|
executionId,
|
|
128
|
-
|
|
160
|
+
dryRun,
|
|
161
|
+
status: dryRun ? 'DRY_RUN_FAILED' : 'FAILED',
|
|
129
162
|
scriptName,
|
|
130
163
|
error: {
|
|
131
164
|
name: error.name,
|
|
@@ -137,107 +170,80 @@ class ScriptRunner {
|
|
|
137
170
|
}
|
|
138
171
|
|
|
139
172
|
/**
|
|
140
|
-
* Create dry-run
|
|
141
|
-
*
|
|
173
|
+
* Create dry-run version of AdminFriggCommands
|
|
174
|
+
* Intercepts all write operations and logs them
|
|
142
175
|
*
|
|
143
|
-
* @param {
|
|
144
|
-
* @
|
|
145
|
-
* @param {Object} params - Input parameters
|
|
146
|
-
* @returns {Object} Dry-run preview
|
|
176
|
+
* @param {Array} operationLog - Array to collect logged operations
|
|
177
|
+
* @returns {Object} Wrapped AdminFriggCommands
|
|
147
178
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
}
|
|
179
|
+
createDryRunFriggCommands(operationLog) {
|
|
180
|
+
// Create real commands (for read operations)
|
|
181
|
+
const realCommands = createAdminFriggCommands({
|
|
182
|
+
executionId: null, // Don't persist logs in dry-run
|
|
183
|
+
integrationFactory: this.integrationFactory,
|
|
184
|
+
});
|
|
171
185
|
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
186
|
+
// Wrap commands to intercept writes
|
|
187
|
+
const wrappedCommands = wrapAdminFriggCommandsForDryRun(realCommands, operationLog);
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
189
|
+
// Create dry-run HTTP client
|
|
190
|
+
const dryRunHttpClient = createDryRunHttpClient(operationLog);
|
|
186
191
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
errors.push(`Missing required parameter: ${field}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
192
|
+
// Override instantiate to inject dry-run HTTP client
|
|
193
|
+
const originalInstantiate = wrappedCommands.instantiate.bind(wrappedCommands);
|
|
194
|
+
wrappedCommands.instantiate = async (integrationId) => {
|
|
195
|
+
const instance = await originalInstantiate(integrationId);
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
}
|
|
197
|
+
// Inject dry-run HTTP client into the integration instance
|
|
198
|
+
injectDryRunHttpClient(instance, dryRunHttpClient);
|
|
208
199
|
|
|
209
|
-
|
|
200
|
+
return instance;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return wrappedCommands;
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
/**
|
|
213
|
-
*
|
|
207
|
+
* Summarize operations from dry-run log
|
|
208
|
+
*
|
|
209
|
+
* @param {Array} log - Operation log
|
|
210
|
+
* @returns {Object} Summary statistics
|
|
214
211
|
*/
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
212
|
+
summarizeOperations(log) {
|
|
213
|
+
const summary = {
|
|
214
|
+
totalOperations: log.length,
|
|
215
|
+
databaseWrites: 0,
|
|
216
|
+
httpRequests: 0,
|
|
217
|
+
byOperation: {},
|
|
218
|
+
byModel: {},
|
|
219
|
+
byService: {},
|
|
220
|
+
};
|
|
218
221
|
|
|
219
|
-
const
|
|
222
|
+
for (const op of log) {
|
|
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
|
+
}
|
|
220
237
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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`;
|
|
238
|
+
// HTTP requests
|
|
239
|
+
if (op.operation === 'HTTP_REQUEST') {
|
|
240
|
+
summary.httpRequests++;
|
|
241
|
+
const service = op.service || 'unknown';
|
|
242
|
+
summary.byService[service] = (summary.byService[service] || 0) + 1;
|
|
243
|
+
}
|
|
238
244
|
}
|
|
239
245
|
|
|
240
|
-
return
|
|
246
|
+
return summary;
|
|
241
247
|
}
|
|
242
248
|
}
|
|
243
249
|
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { adminAuthMiddleware } = require('../admin-auth-middleware');
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Mock the admin script commands
|
|
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', () => {
|
|
4
11
|
let mockReq;
|
|
5
12
|
let mockRes;
|
|
6
13
|
let mockNext;
|
|
7
|
-
let
|
|
14
|
+
let mockCommands;
|
|
8
15
|
|
|
9
16
|
beforeEach(() => {
|
|
10
|
-
originalEnv = process.env.ADMIN_API_KEY;
|
|
11
|
-
process.env.ADMIN_API_KEY = 'test-admin-key-123';
|
|
12
|
-
|
|
13
17
|
mockReq = {
|
|
14
18
|
headers: {},
|
|
19
|
+
ip: '127.0.0.1',
|
|
15
20
|
};
|
|
16
21
|
|
|
17
22
|
mockRes = {
|
|
@@ -20,66 +25,124 @@ describe('validateAdminApiKey', () => {
|
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
mockNext = jest.fn();
|
|
28
|
+
|
|
29
|
+
mockCommands = {
|
|
30
|
+
validateAdminApiKey: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
createAdminScriptCommands.mockReturnValue(mockCommands);
|
|
23
34
|
});
|
|
24
35
|
|
|
25
36
|
afterEach(() => {
|
|
26
|
-
if (originalEnv) {
|
|
27
|
-
process.env.ADMIN_API_KEY = originalEnv;
|
|
28
|
-
} else {
|
|
29
|
-
delete process.env.ADMIN_API_KEY;
|
|
30
|
-
}
|
|
31
37
|
jest.clearAllMocks();
|
|
32
38
|
});
|
|
33
39
|
|
|
34
|
-
describe('
|
|
35
|
-
it('should reject
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
40
|
+
describe('Authorization header validation', () => {
|
|
41
|
+
it('should reject request without Authorization header', async () => {
|
|
42
|
+
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
39
43
|
|
|
40
44
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
41
45
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
42
|
-
error: '
|
|
43
|
-
|
|
46
|
+
error: 'Missing or invalid Authorization header',
|
|
47
|
+
code: 'MISSING_AUTH',
|
|
44
48
|
});
|
|
45
49
|
expect(mockNext).not.toHaveBeenCalled();
|
|
46
50
|
});
|
|
47
|
-
});
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
it('should reject request with invalid Authorization format', async () => {
|
|
53
|
+
mockReq.headers.authorization = 'InvalidFormat key123';
|
|
54
|
+
|
|
55
|
+
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
52
56
|
|
|
53
57
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
54
58
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
55
|
-
error: '
|
|
56
|
-
|
|
59
|
+
error: 'Missing or invalid Authorization header',
|
|
60
|
+
code: 'MISSING_AUTH',
|
|
57
61
|
});
|
|
58
62
|
expect(mockNext).not.toHaveBeenCalled();
|
|
59
63
|
});
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
describe('API key validation', () => {
|
|
63
|
-
it('should reject request with invalid API key', () => {
|
|
64
|
-
mockReq.headers
|
|
67
|
+
it('should reject request with invalid API key', async () => {
|
|
68
|
+
mockReq.headers.authorization = 'Bearer invalid-key';
|
|
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);
|
|
76
|
+
|
|
77
|
+
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('invalid-key');
|
|
78
|
+
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
79
|
+
expect(mockRes.json).toHaveBeenCalledWith({
|
|
80
|
+
error: 'Invalid API key',
|
|
81
|
+
code: 'INVALID_API_KEY',
|
|
82
|
+
});
|
|
83
|
+
expect(mockNext).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
it('should reject request with expired API key', async () => {
|
|
87
|
+
mockReq.headers.authorization = 'Bearer expired-key';
|
|
88
|
+
mockCommands.validateAdminApiKey.mockResolvedValue({
|
|
89
|
+
error: 401,
|
|
90
|
+
reason: 'API key has expired',
|
|
91
|
+
code: 'EXPIRED_API_KEY',
|
|
92
|
+
});
|
|
67
93
|
|
|
94
|
+
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
95
|
+
|
|
96
|
+
expect(mockCommands.validateAdminApiKey).toHaveBeenCalledWith('expired-key');
|
|
68
97
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
69
98
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
70
|
-
error: '
|
|
71
|
-
|
|
99
|
+
error: 'API key has expired',
|
|
100
|
+
code: 'EXPIRED_API_KEY',
|
|
72
101
|
});
|
|
73
102
|
expect(mockNext).not.toHaveBeenCalled();
|
|
74
103
|
});
|
|
75
104
|
|
|
76
|
-
it('should accept request with valid API key', () => {
|
|
77
|
-
|
|
105
|
+
it('should accept request with valid API key', async () => {
|
|
106
|
+
const validKey = 'valid-api-key-123';
|
|
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
|
+
});
|
|
78
116
|
|
|
79
|
-
|
|
117
|
+
await adminAuthMiddleware(mockReq, mockRes, mockNext);
|
|
80
118
|
|
|
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');
|
|
81
126
|
expect(mockNext).toHaveBeenCalled();
|
|
82
127
|
expect(mockRes.status).not.toHaveBeenCalled();
|
|
83
128
|
});
|
|
84
129
|
});
|
|
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
|
+
});
|
|
85
148
|
});
|
|
@@ -4,8 +4,13 @@ const { AdminScriptBase } = require('../../application/admin-script-base');
|
|
|
4
4
|
|
|
5
5
|
// Mock dependencies
|
|
6
6
|
jest.mock('../admin-auth-middleware', () => ({
|
|
7
|
-
|
|
8
|
-
// Mock auth -
|
|
7
|
+
adminAuthMiddleware: (req, res, next) => {
|
|
8
|
+
// Mock auth - attach admin audit info
|
|
9
|
+
req.adminAudit = {
|
|
10
|
+
apiKeyName: 'test-key',
|
|
11
|
+
apiKeyLast4: '1234',
|
|
12
|
+
ipAddress: '127.0.0.1',
|
|
13
|
+
};
|
|
9
14
|
next();
|
|
10
15
|
},
|
|
11
16
|
}));
|
|
@@ -54,8 +59,8 @@ describe('Admin Script Router', () => {
|
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
mockCommands = {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
createScriptExecution: jest.fn(),
|
|
63
|
+
findScriptExecutionById: jest.fn(),
|
|
59
64
|
findRecentExecutions: jest.fn(),
|
|
60
65
|
};
|
|
61
66
|
|
|
@@ -138,7 +143,7 @@ describe('Admin Script Router', () => {
|
|
|
138
143
|
});
|
|
139
144
|
});
|
|
140
145
|
|
|
141
|
-
describe('POST /admin/scripts/:scriptName', () => {
|
|
146
|
+
describe('POST /admin/scripts/:scriptName/execute', () => {
|
|
142
147
|
it('should execute script synchronously', async () => {
|
|
143
148
|
mockRunner.execute.mockResolvedValue({
|
|
144
149
|
executionId: 'exec-123',
|
|
@@ -149,7 +154,7 @@ describe('Admin Script Router', () => {
|
|
|
149
154
|
});
|
|
150
155
|
|
|
151
156
|
const response = await request(app)
|
|
152
|
-
.post('/admin/scripts/test-script')
|
|
157
|
+
.post('/admin/scripts/test-script/execute')
|
|
153
158
|
.send({
|
|
154
159
|
params: { foo: 'bar' },
|
|
155
160
|
mode: 'sync',
|
|
@@ -169,12 +174,12 @@ describe('Admin Script Router', () => {
|
|
|
169
174
|
});
|
|
170
175
|
|
|
171
176
|
it('should queue script for async execution', async () => {
|
|
172
|
-
mockCommands.
|
|
177
|
+
mockCommands.createScriptExecution.mockResolvedValue({
|
|
173
178
|
id: 'exec-456',
|
|
174
179
|
});
|
|
175
180
|
|
|
176
181
|
const response = await request(app)
|
|
177
|
-
.post('/admin/scripts/test-script')
|
|
182
|
+
.post('/admin/scripts/test-script/execute')
|
|
178
183
|
.send({
|
|
179
184
|
params: { foo: 'bar' },
|
|
180
185
|
mode: 'async',
|
|
@@ -193,12 +198,12 @@ describe('Admin Script Router', () => {
|
|
|
193
198
|
});
|
|
194
199
|
|
|
195
200
|
it('should default to async mode', async () => {
|
|
196
|
-
mockCommands.
|
|
201
|
+
mockCommands.createScriptExecution.mockResolvedValue({
|
|
197
202
|
id: 'exec-789',
|
|
198
203
|
});
|
|
199
204
|
|
|
200
205
|
const response = await request(app)
|
|
201
|
-
.post('/admin/scripts/test-script')
|
|
206
|
+
.post('/admin/scripts/test-script/execute')
|
|
202
207
|
.send({
|
|
203
208
|
params: { foo: 'bar' },
|
|
204
209
|
});
|
|
@@ -211,7 +216,7 @@ describe('Admin Script Router', () => {
|
|
|
211
216
|
mockFactory.has.mockReturnValue(false);
|
|
212
217
|
|
|
213
218
|
const response = await request(app)
|
|
214
|
-
.post('/admin/scripts/non-existent')
|
|
219
|
+
.post('/admin/scripts/non-existent/execute')
|
|
215
220
|
.send({
|
|
216
221
|
params: {},
|
|
217
222
|
});
|
|
@@ -221,15 +226,15 @@ describe('Admin Script Router', () => {
|
|
|
221
226
|
});
|
|
222
227
|
});
|
|
223
228
|
|
|
224
|
-
describe('GET /admin/
|
|
229
|
+
describe('GET /admin/executions/:executionId', () => {
|
|
225
230
|
it('should return execution details', async () => {
|
|
226
|
-
mockCommands.
|
|
231
|
+
mockCommands.findScriptExecutionById.mockResolvedValue({
|
|
227
232
|
id: 'exec-123',
|
|
228
233
|
scriptName: 'test-script',
|
|
229
234
|
status: 'COMPLETED',
|
|
230
235
|
});
|
|
231
236
|
|
|
232
|
-
const response = await request(app).get('/admin/
|
|
237
|
+
const response = await request(app).get('/admin/executions/exec-123');
|
|
233
238
|
|
|
234
239
|
expect(response.status).toBe(200);
|
|
235
240
|
expect(response.body.id).toBe('exec-123');
|
|
@@ -237,14 +242,14 @@ describe('Admin Script Router', () => {
|
|
|
237
242
|
});
|
|
238
243
|
|
|
239
244
|
it('should return 404 for non-existent execution', async () => {
|
|
240
|
-
mockCommands.
|
|
245
|
+
mockCommands.findScriptExecutionById.mockResolvedValue({
|
|
241
246
|
error: 404,
|
|
242
247
|
reason: 'Execution not found',
|
|
243
248
|
code: 'EXECUTION_NOT_FOUND',
|
|
244
249
|
});
|
|
245
250
|
|
|
246
251
|
const response = await request(app).get(
|
|
247
|
-
'/admin/
|
|
252
|
+
'/admin/executions/non-existent'
|
|
248
253
|
);
|
|
249
254
|
|
|
250
255
|
expect(response.status).toBe(404);
|
|
@@ -252,28 +257,24 @@ describe('Admin Script Router', () => {
|
|
|
252
257
|
});
|
|
253
258
|
});
|
|
254
259
|
|
|
255
|
-
describe('GET /admin/
|
|
256
|
-
it('should list executions
|
|
260
|
+
describe('GET /admin/executions', () => {
|
|
261
|
+
it('should list recent executions', async () => {
|
|
257
262
|
mockCommands.findRecentExecutions.mockResolvedValue([
|
|
258
263
|
{ id: 'exec-1', scriptName: 'test-script', status: 'COMPLETED' },
|
|
259
264
|
{ id: 'exec-2', scriptName: 'test-script', status: 'RUNNING' },
|
|
260
265
|
]);
|
|
261
266
|
|
|
262
|
-
const response = await request(app).get('/admin/
|
|
267
|
+
const response = await request(app).get('/admin/executions');
|
|
263
268
|
|
|
264
269
|
expect(response.status).toBe(200);
|
|
265
270
|
expect(response.body.executions).toHaveLength(2);
|
|
266
|
-
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|
|
267
|
-
scriptName: 'test-script',
|
|
268
|
-
limit: 50,
|
|
269
|
-
});
|
|
270
271
|
});
|
|
271
272
|
|
|
272
273
|
it('should accept query parameters', async () => {
|
|
273
274
|
mockCommands.findRecentExecutions.mockResolvedValue([]);
|
|
274
275
|
|
|
275
276
|
await request(app).get(
|
|
276
|
-
'/admin/
|
|
277
|
+
'/admin/executions?scriptName=test-script&status=COMPLETED&limit=10'
|
|
277
278
|
);
|
|
278
279
|
|
|
279
280
|
expect(mockCommands.findRecentExecutions).toHaveBeenCalledWith({
|