@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,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
|
|
|
@@ -104,7 +99,7 @@ describe('Admin Script Router', () => {
|
|
|
104
99
|
version: '1.0.0',
|
|
105
100
|
description: 'Test script',
|
|
106
101
|
category: 'test',
|
|
107
|
-
|
|
102
|
+
requireIntegrationInstance: false,
|
|
108
103
|
schedule: null,
|
|
109
104
|
});
|
|
110
105
|
});
|
|
@@ -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({
|
|
@@ -294,8 +293,8 @@ describe('Admin Script Router', () => {
|
|
|
294
293
|
timezone: 'America/New_York',
|
|
295
294
|
lastTriggeredAt: new Date('2025-01-01T09:00:00Z'),
|
|
296
295
|
nextTriggerAt: new Date('2025-01-02T09:00:00Z'),
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
externalScheduleId: 'arn:aws:events:us-east-1:123456789012:rule/test',
|
|
297
|
+
externalScheduleName: 'test-script-schedule',
|
|
299
298
|
createdAt: new Date('2025-01-01T00:00:00Z'),
|
|
300
299
|
updatedAt: new Date('2025-01-01T00:00:00Z'),
|
|
301
300
|
};
|
|
@@ -471,7 +470,7 @@ describe('Admin Script Router', () => {
|
|
|
471
470
|
};
|
|
472
471
|
|
|
473
472
|
mockCommands.upsertSchedule = jest.fn().mockResolvedValue(newSchedule);
|
|
474
|
-
mockCommands.
|
|
473
|
+
mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(newSchedule);
|
|
475
474
|
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
476
475
|
scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
477
476
|
scheduleName: 'frigg-script-test-script',
|
|
@@ -491,11 +490,11 @@ describe('Admin Script Router', () => {
|
|
|
491
490
|
cronExpression: '0 12 * * *',
|
|
492
491
|
timezone: 'America/Los_Angeles',
|
|
493
492
|
});
|
|
494
|
-
expect(mockCommands.
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
|
|
494
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
495
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
497
496
|
});
|
|
498
|
-
expect(response.body.schedule.
|
|
497
|
+
expect(response.body.schedule.externalScheduleId).toBe('arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script');
|
|
499
498
|
});
|
|
500
499
|
|
|
501
500
|
it('should delete EventBridge schedule when disabling existing schedule', async () => {
|
|
@@ -504,14 +503,14 @@ describe('Admin Script Router', () => {
|
|
|
504
503
|
enabled: false,
|
|
505
504
|
cronExpression: null,
|
|
506
505
|
timezone: 'UTC',
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
507
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
509
508
|
createdAt: new Date(),
|
|
510
509
|
updatedAt: new Date(),
|
|
511
510
|
};
|
|
512
511
|
|
|
513
512
|
mockCommands.upsertSchedule = jest.fn().mockResolvedValue(existingSchedule);
|
|
514
|
-
mockCommands.
|
|
513
|
+
mockCommands.updateScheduleExternalInfo = jest.fn().mockResolvedValue(existingSchedule);
|
|
515
514
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
516
515
|
|
|
517
516
|
const response = await request(app)
|
|
@@ -522,9 +521,9 @@ describe('Admin Script Router', () => {
|
|
|
522
521
|
|
|
523
522
|
expect(response.status).toBe(200);
|
|
524
523
|
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
525
|
-
expect(mockCommands.
|
|
526
|
-
|
|
527
|
-
|
|
524
|
+
expect(mockCommands.updateScheduleExternalInfo).toHaveBeenCalledWith('test-script', {
|
|
525
|
+
externalScheduleId: null,
|
|
526
|
+
externalScheduleName: null,
|
|
528
527
|
});
|
|
529
528
|
});
|
|
530
529
|
|
|
@@ -633,7 +632,7 @@ describe('Admin Script Router', () => {
|
|
|
633
632
|
expect(response.body.code).toBe('SCRIPT_NOT_FOUND');
|
|
634
633
|
});
|
|
635
634
|
|
|
636
|
-
it('should delete EventBridge schedule when
|
|
635
|
+
it('should delete EventBridge schedule when external rule exists', async () => {
|
|
637
636
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
638
637
|
acknowledged: true,
|
|
639
638
|
deletedCount: 1,
|
|
@@ -641,8 +640,8 @@ describe('Admin Script Router', () => {
|
|
|
641
640
|
scriptName: 'test-script',
|
|
642
641
|
enabled: true,
|
|
643
642
|
cronExpression: '0 12 * * *',
|
|
644
|
-
|
|
645
|
-
|
|
643
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
644
|
+
externalScheduleName: 'frigg-script-test-script',
|
|
646
645
|
},
|
|
647
646
|
});
|
|
648
647
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
@@ -655,7 +654,7 @@ describe('Admin Script Router', () => {
|
|
|
655
654
|
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
656
655
|
});
|
|
657
656
|
|
|
658
|
-
it('should not call scheduler when no
|
|
657
|
+
it('should not call scheduler when no external rule exists', async () => {
|
|
659
658
|
mockCommands.deleteSchedule = jest.fn().mockResolvedValue({
|
|
660
659
|
acknowledged: true,
|
|
661
660
|
deletedCount: 1,
|
|
@@ -663,7 +662,7 @@ describe('Admin Script Router', () => {
|
|
|
663
662
|
scriptName: 'test-script',
|
|
664
663
|
enabled: true,
|
|
665
664
|
cronExpression: '0 12 * * *',
|
|
666
|
-
// No
|
|
665
|
+
// No externalScheduleId
|
|
667
666
|
},
|
|
668
667
|
});
|
|
669
668
|
|
|
@@ -683,10 +682,10 @@ describe('Admin Script Router', () => {
|
|
|
683
682
|
scriptName: 'test-script',
|
|
684
683
|
enabled: true,
|
|
685
684
|
cronExpression: '0 12 * * *',
|
|
686
|
-
|
|
685
|
+
externalScheduleId: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
687
686
|
},
|
|
688
687
|
});
|
|
689
|
-
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(new Error('
|
|
688
|
+
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(new Error('Scheduler delete failed'));
|
|
690
689
|
|
|
691
690
|
const response = await request(app).delete(
|
|
692
691
|
'/admin/scripts/test-script/schedule'
|
|
@@ -695,7 +694,7 @@ describe('Admin Script Router', () => {
|
|
|
695
694
|
// Request should succeed despite scheduler error
|
|
696
695
|
expect(response.status).toBe(200);
|
|
697
696
|
expect(response.body.success).toBe(true);
|
|
698
|
-
expect(response.body.schedulerWarning).toBe('
|
|
697
|
+
expect(response.body.schedulerWarning).toBe('Scheduler delete failed');
|
|
699
698
|
});
|
|
700
699
|
});
|
|
701
700
|
});
|
|
@@ -1,49 +1,11 @@
|
|
|
1
|
-
const { createAdminScriptCommands } = require('@friggframework/core/application/commands/admin-script-commands');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Admin API Key Authentication Middleware
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Re-exports shared admin auth middleware from @friggframework/core.
|
|
5
|
+
* Uses simple ENV-based API key validation.
|
|
6
|
+
* Expects: x-frigg-admin-api-key header
|
|
8
7
|
*/
|
|
9
|
-
async function adminAuthMiddleware(req, res, next) {
|
|
10
|
-
try {
|
|
11
|
-
const authHeader = req.headers.authorization;
|
|
12
|
-
|
|
13
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
14
|
-
return res.status(401).json({
|
|
15
|
-
error: 'Missing or invalid Authorization header',
|
|
16
|
-
code: 'MISSING_AUTH'
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const apiKey = authHeader.substring(7); // Remove 'Bearer '
|
|
21
|
-
const commands = createAdminScriptCommands();
|
|
22
|
-
const result = await commands.validateAdminApiKey(apiKey);
|
|
23
|
-
|
|
24
|
-
if (result.error) {
|
|
25
|
-
return res.status(result.error).json({
|
|
26
|
-
error: result.reason,
|
|
27
|
-
code: result.code
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Attach validated key info to request for audit trail
|
|
32
|
-
req.adminApiKey = result.apiKey;
|
|
33
|
-
req.adminAudit = {
|
|
34
|
-
apiKeyName: result.apiKey.name,
|
|
35
|
-
apiKeyLast4: result.apiKey.keyLast4,
|
|
36
|
-
ipAddress: req.ip || req.connection?.remoteAddress || 'unknown'
|
|
37
|
-
};
|
|
38
8
|
|
|
39
|
-
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error('Admin auth middleware error:', error);
|
|
42
|
-
res.status(500).json({
|
|
43
|
-
error: 'Authentication failed',
|
|
44
|
-
code: 'AUTH_ERROR'
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
9
|
+
const { validateAdminApiKey } = require('@friggframework/core/handlers/middleware/admin-auth');
|
|
48
10
|
|
|
49
|
-
module.exports = {
|
|
11
|
+
module.exports = { validateAdminApiKey };
|