@friggframework/admin-scripts 2.0.0--canary.545.b4ca16d.0 → 2.0.0--canary.517.8eaf5df.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/README.md +277 -0
- package/index.js +23 -6
- package/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
- package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
- package/src/adapters/aws-scheduler-adapter.js +55 -28
- package/src/adapters/local-scheduler-adapter.js +2 -2
- package/src/adapters/scheduler-adapter-factory.js +3 -1
- package/src/adapters/scheduler-adapter.js +9 -3
- package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
- package/src/application/__tests__/admin-script-base.test.js +23 -6
- package/src/application/__tests__/script-factory.test.js +30 -6
- package/src/application/__tests__/script-runner.test.js +113 -24
- package/src/application/__tests__/validate-script-input.test.js +54 -15
- package/src/application/admin-frigg-commands.js +21 -9
- package/src/application/admin-script-base.js +3 -2
- package/src/application/script-factory.js +3 -1
- package/src/application/script-runner.js +90 -48
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
- package/src/application/use-cases/delete-schedule-use-case.js +6 -4
- package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
- package/src/application/use-cases/index.js +3 -1
- package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
- package/src/application/validate-script-input.js +10 -3
- package/src/builtins/__tests__/integration-health-check.test.js +232 -127
- package/src/builtins/__tests__/oauth-token-refresh.test.js +128 -75
- package/src/builtins/index.js +1 -4
- package/src/builtins/integration-health-check.js +63 -30
- package/src/builtins/oauth-token-refresh.js +64 -29
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
- package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
- package/src/infrastructure/admin-auth-middleware.js +3 -1
- package/src/infrastructure/admin-script-router.js +129 -14
- package/src/infrastructure/bootstrap.js +87 -0
- package/src/infrastructure/script-executor-handler.js +119 -52
- 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
|
@@ -19,19 +19,21 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
19
19
|
integrationIds: {
|
|
20
20
|
type: 'array',
|
|
21
21
|
items: { type: 'string' },
|
|
22
|
-
description:
|
|
22
|
+
description:
|
|
23
|
+
'Specific integration IDs to refresh (optional, defaults to all)',
|
|
23
24
|
},
|
|
24
25
|
expiryThresholdHours: {
|
|
25
26
|
type: 'number',
|
|
26
27
|
default: 24,
|
|
27
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'Refresh tokens expiring within this many hours',
|
|
28
30
|
},
|
|
29
31
|
dryRun: {
|
|
30
32
|
type: 'boolean',
|
|
31
33
|
default: false,
|
|
32
|
-
description: 'Preview without making changes'
|
|
33
|
-
}
|
|
34
|
-
}
|
|
34
|
+
description: 'Preview without making changes',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
35
37
|
},
|
|
36
38
|
|
|
37
39
|
outputSchema: {
|
|
@@ -40,13 +42,12 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
40
42
|
refreshed: { type: 'number' },
|
|
41
43
|
failed: { type: 'number' },
|
|
42
44
|
skipped: { type: 'number' },
|
|
43
|
-
details: { type: 'array' }
|
|
44
|
-
}
|
|
45
|
+
details: { type: 'array' },
|
|
46
|
+
},
|
|
45
47
|
},
|
|
46
48
|
|
|
47
49
|
config: {
|
|
48
50
|
timeout: 600000, // 10 minutes
|
|
49
|
-
maxRetries: 1,
|
|
50
51
|
requireIntegrationInstance: true, // Needs to call external APIs
|
|
51
52
|
},
|
|
52
53
|
|
|
@@ -60,27 +61,31 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
60
61
|
const {
|
|
61
62
|
integrationIds = null,
|
|
62
63
|
expiryThresholdHours = 24,
|
|
63
|
-
dryRun = false
|
|
64
|
+
dryRun = false,
|
|
64
65
|
} = params;
|
|
65
66
|
|
|
66
67
|
const results = {
|
|
67
68
|
refreshed: 0,
|
|
68
69
|
failed: 0,
|
|
69
70
|
skipped: 0,
|
|
70
|
-
details: []
|
|
71
|
+
details: [],
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
this.context.log('info', 'Starting OAuth token refresh', {
|
|
74
75
|
expiryThresholdHours,
|
|
75
76
|
dryRun,
|
|
76
|
-
specificIds: integrationIds?.length || 'all'
|
|
77
|
+
specificIds: integrationIds?.length || 'all',
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
// Get integrations to check
|
|
80
81
|
let integrations;
|
|
81
82
|
if (integrationIds && integrationIds.length > 0) {
|
|
82
83
|
integrations = await Promise.all(
|
|
83
|
-
integrationIds.map(
|
|
84
|
+
integrationIds.map((id) =>
|
|
85
|
+
this.context.integrationRepository
|
|
86
|
+
.findIntegrationById(id)
|
|
87
|
+
.catch(() => null)
|
|
88
|
+
)
|
|
84
89
|
);
|
|
85
90
|
integrations = integrations.filter(Boolean);
|
|
86
91
|
} else {
|
|
@@ -88,13 +93,16 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
88
93
|
integrations = await this.getAllIntegrations();
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
this.context.log(
|
|
96
|
+
this.context.log(
|
|
97
|
+
'info',
|
|
98
|
+
`Found ${integrations.length} integrations to check`
|
|
99
|
+
);
|
|
92
100
|
|
|
93
101
|
for (const integration of integrations) {
|
|
94
102
|
try {
|
|
95
103
|
const detail = await this.processIntegration(integration, {
|
|
96
104
|
expiryThresholdHours,
|
|
97
|
-
dryRun
|
|
105
|
+
dryRun,
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
results.details.push(detail);
|
|
@@ -107,14 +115,18 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
107
115
|
results.failed++;
|
|
108
116
|
}
|
|
109
117
|
} catch (error) {
|
|
110
|
-
this.context.log(
|
|
111
|
-
error
|
|
112
|
-
|
|
118
|
+
this.context.log(
|
|
119
|
+
'error',
|
|
120
|
+
`Error processing integration ${integration.id}`,
|
|
121
|
+
{
|
|
122
|
+
error: error.message,
|
|
123
|
+
}
|
|
124
|
+
);
|
|
113
125
|
results.failed++;
|
|
114
126
|
results.details.push({
|
|
115
127
|
integrationId: integration.id,
|
|
116
128
|
action: 'failed',
|
|
117
|
-
reason: error.message
|
|
129
|
+
reason: error.message,
|
|
118
130
|
});
|
|
119
131
|
}
|
|
120
132
|
}
|
|
@@ -122,7 +134,7 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
122
134
|
this.context.log('info', 'OAuth token refresh completed', {
|
|
123
135
|
refreshed: results.refreshed,
|
|
124
136
|
failed: results.failed,
|
|
125
|
-
skipped: results.skipped
|
|
137
|
+
skipped: results.skipped,
|
|
126
138
|
});
|
|
127
139
|
|
|
128
140
|
return results;
|
|
@@ -138,15 +150,25 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
138
150
|
const { expiryThresholdHours, dryRun } = options;
|
|
139
151
|
|
|
140
152
|
// Check prerequisites
|
|
141
|
-
const skipReason = this._checkRefreshPrerequisites(
|
|
153
|
+
const skipReason = this._checkRefreshPrerequisites(
|
|
154
|
+
integration,
|
|
155
|
+
expiryThresholdHours
|
|
156
|
+
);
|
|
142
157
|
if (skipReason) {
|
|
143
158
|
return this._createResult(integration.id, 'skipped', skipReason);
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
// Handle dry run
|
|
147
162
|
if (dryRun) {
|
|
148
|
-
this.context.log(
|
|
149
|
-
|
|
163
|
+
this.context.log(
|
|
164
|
+
'info',
|
|
165
|
+
`[DRY RUN] Would refresh token for ${integration.id}`
|
|
166
|
+
);
|
|
167
|
+
return this._createResult(
|
|
168
|
+
integration.id,
|
|
169
|
+
'skipped',
|
|
170
|
+
'Dry run - would have refreshed'
|
|
171
|
+
);
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
// Perform refresh
|
|
@@ -169,7 +191,9 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
169
191
|
}
|
|
170
192
|
|
|
171
193
|
const expiryTime = new Date(expiresAt);
|
|
172
|
-
const thresholdTime = new Date(
|
|
194
|
+
const thresholdTime = new Date(
|
|
195
|
+
Date.now() + expiryThresholdHours * 60 * 60 * 1000
|
|
196
|
+
);
|
|
173
197
|
|
|
174
198
|
if (expiryTime > thresholdTime) {
|
|
175
199
|
return 'Token not near expiry';
|
|
@@ -189,21 +213,32 @@ class OAuthTokenRefreshScript extends AdminScriptBase {
|
|
|
189
213
|
const instance = await this.context.instantiate(integration.id);
|
|
190
214
|
|
|
191
215
|
if (!instance.primary?.api?.refreshAccessToken) {
|
|
192
|
-
return this._createResult(
|
|
216
|
+
return this._createResult(
|
|
217
|
+
integration.id,
|
|
218
|
+
'skipped',
|
|
219
|
+
'API does not support token refresh'
|
|
220
|
+
);
|
|
193
221
|
}
|
|
194
222
|
|
|
195
223
|
await instance.primary.api.refreshAccessToken();
|
|
196
|
-
this.context.log(
|
|
224
|
+
this.context.log(
|
|
225
|
+
'info',
|
|
226
|
+
`Refreshed token for integration ${integration.id}`
|
|
227
|
+
);
|
|
197
228
|
|
|
198
229
|
return {
|
|
199
230
|
integrationId: integration.id,
|
|
200
231
|
action: 'refreshed',
|
|
201
|
-
previousExpiry: expiresAt
|
|
232
|
+
previousExpiry: expiresAt,
|
|
202
233
|
};
|
|
203
234
|
} catch (error) {
|
|
204
|
-
this.context.log(
|
|
205
|
-
error
|
|
206
|
-
|
|
235
|
+
this.context.log(
|
|
236
|
+
'error',
|
|
237
|
+
`Failed to refresh token for ${integration.id}`,
|
|
238
|
+
{
|
|
239
|
+
error: error.message,
|
|
240
|
+
}
|
|
241
|
+
);
|
|
207
242
|
return this._createResult(integration.id, 'failed', error.message);
|
|
208
243
|
}
|
|
209
244
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
1
2
|
const { validateAdminApiKey } = require('../admin-auth-middleware');
|
|
2
3
|
|
|
4
|
+
// Generated at runtime so no credential-like literal is committed
|
|
5
|
+
const TEST_ADMIN_KEY = crypto.randomBytes(16).toString('hex');
|
|
6
|
+
|
|
3
7
|
describe('validateAdminApiKey', () => {
|
|
4
8
|
let mockReq;
|
|
5
9
|
let mockRes;
|
|
@@ -8,7 +12,7 @@ describe('validateAdminApiKey', () => {
|
|
|
8
12
|
|
|
9
13
|
beforeEach(() => {
|
|
10
14
|
originalEnv = process.env.ADMIN_API_KEY;
|
|
11
|
-
process.env.ADMIN_API_KEY =
|
|
15
|
+
process.env.ADMIN_API_KEY = TEST_ADMIN_KEY;
|
|
12
16
|
|
|
13
17
|
mockReq = {
|
|
14
18
|
headers: {},
|
|
@@ -61,7 +65,9 @@ describe('validateAdminApiKey', () => {
|
|
|
61
65
|
|
|
62
66
|
describe('API key validation', () => {
|
|
63
67
|
it('should reject request with invalid API key', () => {
|
|
64
|
-
mockReq.headers[
|
|
68
|
+
mockReq.headers[
|
|
69
|
+
'x-frigg-admin-api-key'
|
|
70
|
+
] = `${TEST_ADMIN_KEY}-wrong`;
|
|
65
71
|
|
|
66
72
|
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
67
73
|
|
|
@@ -74,7 +80,7 @@ describe('validateAdminApiKey', () => {
|
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
it('should accept request with valid API key', () => {
|
|
77
|
-
mockReq.headers['x-frigg-admin-api-key'] =
|
|
83
|
+
mockReq.headers['x-frigg-admin-api-key'] = TEST_ADMIN_KEY;
|
|
78
84
|
|
|
79
85
|
validateAdminApiKey(mockReq, mockRes, mockNext);
|
|
80
86
|
|
|
@@ -15,12 +15,19 @@ jest.mock('../../application/script-runner');
|
|
|
15
15
|
jest.mock('@friggframework/core/application/commands/admin-script-commands');
|
|
16
16
|
jest.mock('@friggframework/core/queues');
|
|
17
17
|
jest.mock('../../adapters/scheduler-adapter-factory');
|
|
18
|
+
jest.mock('../bootstrap', () => ({
|
|
19
|
+
bootstrapAdminScripts: () => ({ integrationFactory: {} }),
|
|
20
|
+
}));
|
|
18
21
|
|
|
19
22
|
const { getScriptFactory } = require('../../application/script-factory');
|
|
20
23
|
const { createScriptRunner } = require('../../application/script-runner');
|
|
21
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
createAdminScriptCommands,
|
|
26
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
22
27
|
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
23
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
createSchedulerAdapter,
|
|
30
|
+
} = require('../../adapters/scheduler-adapter-factory');
|
|
24
31
|
|
|
25
32
|
describe('Admin Script Router', () => {
|
|
26
33
|
let mockFactory;
|
|
@@ -56,7 +63,7 @@ describe('Admin Script Router', () => {
|
|
|
56
63
|
mockCommands = {
|
|
57
64
|
createAdminProcess: jest.fn(),
|
|
58
65
|
findAdminProcessById: jest.fn(),
|
|
59
|
-
|
|
66
|
+
findAdminProcessesByName: jest.fn(),
|
|
60
67
|
};
|
|
61
68
|
|
|
62
69
|
mockSchedulerAdapter = {
|
|
@@ -118,7 +125,9 @@ describe('Admin Script Router', () => {
|
|
|
118
125
|
|
|
119
126
|
describe('GET /admin/scripts/:scriptName', () => {
|
|
120
127
|
it('should return script details', async () => {
|
|
121
|
-
const response = await request(app).get(
|
|
128
|
+
const response = await request(app).get(
|
|
129
|
+
'/admin/scripts/test-script'
|
|
130
|
+
);
|
|
122
131
|
|
|
123
132
|
expect(response.status).toBe(200);
|
|
124
133
|
expect(response.body.name).toBe('test-script');
|
|
@@ -169,7 +178,8 @@ describe('Admin Script Router', () => {
|
|
|
169
178
|
});
|
|
170
179
|
|
|
171
180
|
it('should queue script for async execution', async () => {
|
|
172
|
-
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
181
|
+
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
182
|
+
'https://sqs.us-east-1.amazonaws.com/123/test-queue';
|
|
173
183
|
mockCommands.createAdminProcess.mockResolvedValue({
|
|
174
184
|
id: 'exec-456',
|
|
175
185
|
});
|
|
@@ -195,7 +205,8 @@ describe('Admin Script Router', () => {
|
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
it('should default to async mode', async () => {
|
|
198
|
-
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
208
|
+
process.env.ADMIN_SCRIPT_QUEUE_URL =
|
|
209
|
+
'https://sqs.us-east-1.amazonaws.com/123/test-queue';
|
|
199
210
|
mockCommands.createAdminProcess.mockResolvedValue({
|
|
200
211
|
id: 'exec-789',
|
|
201
212
|
});
|
|
@@ -247,7 +258,9 @@ describe('Admin Script Router', () => {
|
|
|
247
258
|
status: 'COMPLETED',
|
|
248
259
|
});
|
|
249
260
|
|
|
250
|
-
const response = await request(app).get(
|
|
261
|
+
const response = await request(app).get(
|
|
262
|
+
'/admin/scripts/test-script/executions/exec-123'
|
|
263
|
+
);
|
|
251
264
|
|
|
252
265
|
expect(response.status).toBe(200);
|
|
253
266
|
expect(response.body.id).toBe('exec-123');
|
|
@@ -272,33 +285,39 @@ describe('Admin Script Router', () => {
|
|
|
272
285
|
|
|
273
286
|
describe('GET /admin/scripts/:scriptName/executions', () => {
|
|
274
287
|
it('should list executions for specific script', async () => {
|
|
275
|
-
mockCommands.
|
|
276
|
-
{ id: 'exec-1',
|
|
277
|
-
{ id: 'exec-2',
|
|
288
|
+
mockCommands.findAdminProcessesByName.mockResolvedValue([
|
|
289
|
+
{ id: 'exec-1', name: 'test-script', state: 'COMPLETED' },
|
|
290
|
+
{ id: 'exec-2', name: 'test-script', state: 'RUNNING' },
|
|
278
291
|
]);
|
|
279
292
|
|
|
280
|
-
const response = await request(app).get(
|
|
293
|
+
const response = await request(app).get(
|
|
294
|
+
'/admin/scripts/test-script/executions'
|
|
295
|
+
);
|
|
281
296
|
|
|
282
297
|
expect(response.status).toBe(200);
|
|
283
298
|
expect(response.body.executions).toHaveLength(2);
|
|
284
|
-
expect(mockCommands.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
|
|
300
|
+
'test-script',
|
|
301
|
+
{
|
|
302
|
+
limit: 50,
|
|
303
|
+
}
|
|
304
|
+
);
|
|
288
305
|
});
|
|
289
306
|
|
|
290
|
-
it('should accept query parameters', async () => {
|
|
291
|
-
mockCommands.
|
|
307
|
+
it('should accept query parameters (status maps to state, limit is bounded)', async () => {
|
|
308
|
+
mockCommands.findAdminProcessesByName.mockResolvedValue([]);
|
|
292
309
|
|
|
293
310
|
await request(app).get(
|
|
294
311
|
'/admin/scripts/test-script/executions?status=COMPLETED&limit=10'
|
|
295
312
|
);
|
|
296
313
|
|
|
297
|
-
expect(mockCommands.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
expect(mockCommands.findAdminProcessesByName).toHaveBeenCalledWith(
|
|
315
|
+
'test-script',
|
|
316
|
+
{
|
|
317
|
+
limit: 10,
|
|
318
|
+
state: 'COMPLETED',
|
|
319
|
+
}
|
|
320
|
+
);
|
|
302
321
|
});
|
|
303
322
|
});
|
|
304
323
|
|
|
@@ -311,15 +330,20 @@ describe('Admin Script Router', () => {
|
|
|
311
330
|
timezone: 'America/New_York',
|
|
312
331
|
lastTriggeredAt: new Date('2025-01-01T09:00:00Z'),
|
|
313
332
|
nextTriggerAt: new Date('2025-01-02T09:00:00Z'),
|
|
314
|
-
externalScheduleId:
|
|
333
|
+
externalScheduleId:
|
|
334
|
+
'arn:aws:events:us-east-1:123456789012:rule/test',
|
|
315
335
|
externalScheduleName: 'test-script-schedule',
|
|
316
336
|
createdAt: new Date('2025-01-01T00:00:00Z'),
|
|
317
337
|
updatedAt: new Date('2025-01-01T00:00:00Z'),
|
|
318
338
|
};
|
|
319
339
|
|
|
320
|
-
mockCommands.getScheduleByScriptName = jest
|
|
340
|
+
mockCommands.getScheduleByScriptName = jest
|
|
341
|
+
.fn()
|
|
342
|
+
.mockResolvedValue(dbSchedule);
|
|
321
343
|
|
|
322
|
-
const response = await request(app).get(
|
|
344
|
+
const response = await request(app).get(
|
|
345
|
+
'/admin/scripts/test-script/schedule'
|
|
346
|
+
);
|
|
323
347
|
|
|
324
348
|
expect(response.status).toBe(200);
|
|
325
349
|
expect(response.body.source).toBe('database');
|
|
@@ -329,7 +353,9 @@ describe('Admin Script Router', () => {
|
|
|
329
353
|
});
|
|
330
354
|
|
|
331
355
|
it('should return definition schedule when no database override', async () => {
|
|
332
|
-
mockCommands.getScheduleByScriptName = jest
|
|
356
|
+
mockCommands.getScheduleByScriptName = jest
|
|
357
|
+
.fn()
|
|
358
|
+
.mockResolvedValue(null);
|
|
333
359
|
|
|
334
360
|
// Update test script to include schedule
|
|
335
361
|
class ScheduledTestScript extends TestScript {
|
|
@@ -345,7 +371,9 @@ describe('Admin Script Router', () => {
|
|
|
345
371
|
|
|
346
372
|
mockFactory.get.mockReturnValue(ScheduledTestScript);
|
|
347
373
|
|
|
348
|
-
const response = await request(app).get(
|
|
374
|
+
const response = await request(app).get(
|
|
375
|
+
'/admin/scripts/test-script/schedule'
|
|
376
|
+
);
|
|
349
377
|
|
|
350
378
|
expect(response.status).toBe(200);
|
|
351
379
|
expect(response.body.source).toBe('definition');
|
|
@@ -355,9 +383,13 @@ describe('Admin Script Router', () => {
|
|
|
355
383
|
});
|
|
356
384
|
|
|
357
385
|
it('should return none when no schedule configured', async () => {
|
|
358
|
-
mockCommands.getScheduleByScriptName = jest
|
|
386
|
+
mockCommands.getScheduleByScriptName = jest
|
|
387
|
+
.fn()
|
|
388
|
+
.mockResolvedValue(null);
|
|
359
389
|
|
|
360
|
-
const response = await request(app).get(
|
|
390
|
+
const response = await request(app).get(
|
|
391
|
+
'/admin/scripts/test-script/schedule'
|
|
392
|
+
);
|
|
361
393
|
|
|
362
394
|
expect(response.status).toBe(200);
|
|
363
395
|
expect(response.body.source).toBe('none');
|
|
@@ -389,7 +421,9 @@ describe('Admin Script Router', () => {
|
|
|
389
421
|
updatedAt: new Date(),
|
|
390
422
|
};
|
|
391
423
|
|
|
392
|
-
mockCommands.upsertSchedule = jest
|
|
424
|
+
mockCommands.upsertSchedule = jest
|
|
425
|
+
.fn()
|
|
426
|
+
.mockResolvedValue(newSchedule);
|
|
393
427
|
|
|
394
428
|
const response = await request(app)
|
|
395
429
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -424,7 +458,9 @@ describe('Admin Script Router', () => {
|
|
|
424
458
|
updatedAt: new Date(),
|
|
425
459
|
};
|
|
426
460
|
|
|
427
|
-
mockCommands.upsertSchedule = jest
|
|
461
|
+
mockCommands.upsertSchedule = jest
|
|
462
|
+
.fn()
|
|
463
|
+
.mockResolvedValue(updatedSchedule);
|
|
428
464
|
|
|
429
465
|
const response = await request(app)
|
|
430
466
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -487,10 +523,15 @@ describe('Admin Script Router', () => {
|
|
|
487
523
|
updatedAt: new Date(),
|
|
488
524
|
};
|
|
489
525
|
|
|
490
|
-
mockCommands.upsertSchedule = jest
|
|
491
|
-
|
|
526
|
+
mockCommands.upsertSchedule = jest
|
|
527
|
+
.fn()
|
|
528
|
+
.mockResolvedValue(newSchedule);
|
|
529
|
+
mockCommands.updateScheduleExternalInfo = jest
|
|
530
|
+
.fn()
|
|
531
|
+
.mockResolvedValue(newSchedule);
|
|
492
532
|
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
493
|
-
scheduleArn:
|
|
533
|
+
scheduleArn:
|
|
534
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
494
535
|
scheduleName: 'frigg-script-test-script',
|
|
495
536
|
});
|
|
496
537
|
|
|
@@ -508,11 +549,16 @@ describe('Admin Script Router', () => {
|
|
|
508
549
|
cronExpression: '0 12 * * *',
|
|
509
550
|
timezone: 'America/Los_Angeles',
|
|
510
551
|
});
|
|
511
|
-
expect(
|
|
512
|
-
|
|
552
|
+
expect(
|
|
553
|
+
mockCommands.updateScheduleExternalInfo
|
|
554
|
+
).toHaveBeenCalledWith('test-script', {
|
|
555
|
+
externalScheduleId:
|
|
556
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
513
557
|
externalScheduleName: 'frigg-script-test-script',
|
|
514
558
|
});
|
|
515
|
-
expect(response.body.schedule.externalScheduleId).toBe(
|
|
559
|
+
expect(response.body.schedule.externalScheduleId).toBe(
|
|
560
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script'
|
|
561
|
+
);
|
|
516
562
|
});
|
|
517
563
|
|
|
518
564
|
it('should delete EventBridge schedule when disabling existing schedule', async () => {
|
|
@@ -521,14 +567,19 @@ describe('Admin Script Router', () => {
|
|
|
521
567
|
enabled: false,
|
|
522
568
|
cronExpression: null,
|
|
523
569
|
timezone: 'UTC',
|
|
524
|
-
externalScheduleId:
|
|
570
|
+
externalScheduleId:
|
|
571
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
525
572
|
externalScheduleName: 'frigg-script-test-script',
|
|
526
573
|
createdAt: new Date(),
|
|
527
574
|
updatedAt: new Date(),
|
|
528
575
|
};
|
|
529
576
|
|
|
530
|
-
mockCommands.upsertSchedule = jest
|
|
531
|
-
|
|
577
|
+
mockCommands.upsertSchedule = jest
|
|
578
|
+
.fn()
|
|
579
|
+
.mockResolvedValue(existingSchedule);
|
|
580
|
+
mockCommands.updateScheduleExternalInfo = jest
|
|
581
|
+
.fn()
|
|
582
|
+
.mockResolvedValue(existingSchedule);
|
|
532
583
|
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
533
584
|
|
|
534
585
|
const response = await request(app)
|
|
@@ -538,8 +589,12 @@ describe('Admin Script Router', () => {
|
|
|
538
589
|
});
|
|
539
590
|
|
|
540
591
|
expect(response.status).toBe(200);
|
|
541
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
542
|
-
|
|
592
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
593
|
+
'test-script'
|
|
594
|
+
);
|
|
595
|
+
expect(
|
|
596
|
+
mockCommands.updateScheduleExternalInfo
|
|
597
|
+
).toHaveBeenCalledWith('test-script', {
|
|
543
598
|
externalScheduleId: null,
|
|
544
599
|
externalScheduleName: null,
|
|
545
600
|
});
|
|
@@ -555,8 +610,12 @@ describe('Admin Script Router', () => {
|
|
|
555
610
|
updatedAt: new Date(),
|
|
556
611
|
};
|
|
557
612
|
|
|
558
|
-
mockCommands.upsertSchedule = jest
|
|
559
|
-
|
|
613
|
+
mockCommands.upsertSchedule = jest
|
|
614
|
+
.fn()
|
|
615
|
+
.mockResolvedValue(newSchedule);
|
|
616
|
+
mockSchedulerAdapter.createSchedule.mockRejectedValue(
|
|
617
|
+
new Error('AWS Scheduler API error')
|
|
618
|
+
);
|
|
560
619
|
|
|
561
620
|
const response = await request(app)
|
|
562
621
|
.put('/admin/scripts/test-script/schedule')
|
|
@@ -568,7 +627,9 @@ describe('Admin Script Router', () => {
|
|
|
568
627
|
// Request should succeed despite scheduler error
|
|
569
628
|
expect(response.status).toBe(200);
|
|
570
629
|
expect(response.body.success).toBe(true);
|
|
571
|
-
expect(response.body.schedulerWarning).toBe(
|
|
630
|
+
expect(response.body.schedulerWarning).toBe(
|
|
631
|
+
'AWS Scheduler API error'
|
|
632
|
+
);
|
|
572
633
|
});
|
|
573
634
|
});
|
|
574
635
|
|
|
@@ -592,7 +653,9 @@ describe('Admin Script Router', () => {
|
|
|
592
653
|
expect(response.body.success).toBe(true);
|
|
593
654
|
expect(response.body.deletedCount).toBe(1);
|
|
594
655
|
expect(response.body.message).toContain('removed');
|
|
595
|
-
expect(mockCommands.deleteSchedule).toHaveBeenCalledWith(
|
|
656
|
+
expect(mockCommands.deleteSchedule).toHaveBeenCalledWith(
|
|
657
|
+
'test-script'
|
|
658
|
+
);
|
|
596
659
|
});
|
|
597
660
|
|
|
598
661
|
it('should return definition schedule after deleting override', async () => {
|
|
@@ -636,7 +699,9 @@ describe('Admin Script Router', () => {
|
|
|
636
699
|
|
|
637
700
|
expect(response.status).toBe(200);
|
|
638
701
|
expect(response.body.deletedCount).toBe(0);
|
|
639
|
-
expect(response.body.message).toContain(
|
|
702
|
+
expect(response.body.message).toContain(
|
|
703
|
+
'No schedule override found'
|
|
704
|
+
);
|
|
640
705
|
});
|
|
641
706
|
|
|
642
707
|
it('should return 404 for non-existent script', async () => {
|
|
@@ -658,7 +723,8 @@ describe('Admin Script Router', () => {
|
|
|
658
723
|
scriptName: 'test-script',
|
|
659
724
|
enabled: true,
|
|
660
725
|
cronExpression: '0 12 * * *',
|
|
661
|
-
externalScheduleId:
|
|
726
|
+
externalScheduleId:
|
|
727
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
662
728
|
externalScheduleName: 'frigg-script-test-script',
|
|
663
729
|
},
|
|
664
730
|
});
|
|
@@ -669,7 +735,9 @@ describe('Admin Script Router', () => {
|
|
|
669
735
|
);
|
|
670
736
|
|
|
671
737
|
expect(response.status).toBe(200);
|
|
672
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
738
|
+
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith(
|
|
739
|
+
'test-script'
|
|
740
|
+
);
|
|
673
741
|
});
|
|
674
742
|
|
|
675
743
|
it('should not call scheduler when no external rule exists', async () => {
|
|
@@ -700,10 +768,13 @@ describe('Admin Script Router', () => {
|
|
|
700
768
|
scriptName: 'test-script',
|
|
701
769
|
enabled: true,
|
|
702
770
|
cronExpression: '0 12 * * *',
|
|
703
|
-
externalScheduleId:
|
|
771
|
+
externalScheduleId:
|
|
772
|
+
'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
704
773
|
},
|
|
705
774
|
});
|
|
706
|
-
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
775
|
+
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
776
|
+
new Error('Scheduler delete failed')
|
|
777
|
+
);
|
|
707
778
|
|
|
708
779
|
const response = await request(app).delete(
|
|
709
780
|
'/admin/scripts/test-script/schedule'
|
|
@@ -712,7 +783,9 @@ describe('Admin Script Router', () => {
|
|
|
712
783
|
// Request should succeed despite scheduler error
|
|
713
784
|
expect(response.status).toBe(200);
|
|
714
785
|
expect(response.body.success).toBe(true);
|
|
715
|
-
expect(response.body.schedulerWarning).toBe(
|
|
786
|
+
expect(response.body.schedulerWarning).toBe(
|
|
787
|
+
'Scheduler delete failed'
|
|
788
|
+
);
|
|
716
789
|
});
|
|
717
790
|
});
|
|
718
791
|
});
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Expects: x-frigg-admin-api-key header
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
validateAdminApiKey,
|
|
11
|
+
} = require('@friggframework/core/handlers/middleware/admin-auth');
|
|
10
12
|
|
|
11
13
|
module.exports = { validateAdminApiKey };
|