@friggframework/admin-scripts 2.0.0--canary.517.41839c5.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/LICENSE.md +9 -0
- package/index.js +66 -0
- package/package.json +53 -0
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
- package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
- package/src/adapters/aws-scheduler-adapter.js +138 -0
- package/src/adapters/local-scheduler-adapter.js +103 -0
- package/src/adapters/scheduler-adapter-factory.js +69 -0
- package/src/adapters/scheduler-adapter.js +64 -0
- package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
- package/src/application/__tests__/admin-script-base.test.js +273 -0
- 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__/schedule-management-use-case.test.js +276 -0
- package/src/application/__tests__/script-factory.test.js +381 -0
- package/src/application/__tests__/script-runner.test.js +202 -0
- package/src/application/admin-frigg-commands.js +242 -0
- package/src/application/admin-script-base.js +138 -0
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/schedule-management-use-case.js +230 -0
- package/src/application/script-factory.js +161 -0
- package/src/application/script-runner.js +254 -0
- package/src/builtins/__tests__/integration-health-check.test.js +598 -0
- package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
- package/src/builtins/index.js +28 -0
- package/src/builtins/integration-health-check.js +279 -0
- package/src/builtins/oauth-token-refresh.js +221 -0
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
- package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
- package/src/infrastructure/admin-auth-middleware.js +49 -0
- package/src/infrastructure/admin-script-router.js +311 -0
- package/src/infrastructure/script-executor-handler.js +75 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const { LocalSchedulerAdapter } = require('../local-scheduler-adapter');
|
|
2
|
+
const { SchedulerAdapter } = require('../scheduler-adapter');
|
|
3
|
+
|
|
4
|
+
describe('LocalSchedulerAdapter', () => {
|
|
5
|
+
let adapter;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
adapter = new LocalSchedulerAdapter();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
adapter.clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Inheritance', () => {
|
|
16
|
+
it('should extend SchedulerAdapter', () => {
|
|
17
|
+
expect(adapter).toBeInstanceOf(SchedulerAdapter);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have correct adapter name', () => {
|
|
21
|
+
expect(adapter.getName()).toBe('local-cron');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createSchedule()', () => {
|
|
26
|
+
it('should create a schedule with required fields', async () => {
|
|
27
|
+
const config = {
|
|
28
|
+
scriptName: 'test-script',
|
|
29
|
+
cronExpression: '0 0 * * *',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = await adapter.createSchedule(config);
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
scheduleName: 'test-script',
|
|
36
|
+
scheduleArn: 'local:schedule:test-script',
|
|
37
|
+
});
|
|
38
|
+
expect(adapter.size).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should create a schedule with all optional fields', async () => {
|
|
42
|
+
const config = {
|
|
43
|
+
scriptName: 'test-script',
|
|
44
|
+
cronExpression: '0 0 * * *',
|
|
45
|
+
timezone: 'America/New_York',
|
|
46
|
+
input: { key: 'value' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = await adapter.createSchedule(config);
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
scheduleName: 'test-script',
|
|
53
|
+
scheduleArn: 'local:schedule:test-script',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
57
|
+
expect(schedule.ScheduleExpressionTimezone).toBe('America/New_York');
|
|
58
|
+
expect(JSON.parse(schedule.Target.Input).params).toEqual({ key: 'value' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should default timezone to UTC', async () => {
|
|
62
|
+
const config = {
|
|
63
|
+
scriptName: 'test-script',
|
|
64
|
+
cronExpression: '0 0 * * *',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await adapter.createSchedule(config);
|
|
68
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
69
|
+
|
|
70
|
+
expect(schedule.ScheduleExpressionTimezone).toBe('UTC');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should enable schedule by default', async () => {
|
|
74
|
+
const config = {
|
|
75
|
+
scriptName: 'test-script',
|
|
76
|
+
cronExpression: '0 0 * * *',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await adapter.createSchedule(config);
|
|
80
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
81
|
+
|
|
82
|
+
expect(schedule.State).toBe('ENABLED');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should update existing schedule if created again', async () => {
|
|
86
|
+
const config1 = {
|
|
87
|
+
scriptName: 'test-script',
|
|
88
|
+
cronExpression: '0 0 * * *',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const config2 = {
|
|
92
|
+
scriptName: 'test-script',
|
|
93
|
+
cronExpression: '0 12 * * *',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await adapter.createSchedule(config1);
|
|
97
|
+
expect(adapter.size).toBe(1);
|
|
98
|
+
|
|
99
|
+
await adapter.createSchedule(config2);
|
|
100
|
+
expect(adapter.size).toBe(1); // Still only 1 schedule
|
|
101
|
+
|
|
102
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
103
|
+
expect(schedule.ScheduleExpression).toBe('0 12 * * *');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('deleteSchedule()', () => {
|
|
108
|
+
it('should delete an existing schedule', async () => {
|
|
109
|
+
await adapter.createSchedule({
|
|
110
|
+
scriptName: 'test-script',
|
|
111
|
+
cronExpression: '0 0 * * *',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(adapter.size).toBe(1);
|
|
115
|
+
|
|
116
|
+
await adapter.deleteSchedule('test-script');
|
|
117
|
+
|
|
118
|
+
expect(adapter.size).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should not throw error when deleting non-existent schedule', async () => {
|
|
122
|
+
await expect(adapter.deleteSchedule('non-existent')).resolves.toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should clear intervals if they exist', async () => {
|
|
126
|
+
await adapter.createSchedule({
|
|
127
|
+
scriptName: 'test-script',
|
|
128
|
+
cronExpression: '0 0 * * *',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Simulate an interval
|
|
132
|
+
const intervalId = setInterval(() => {}, 1000);
|
|
133
|
+
adapter.intervals.set('test-script', intervalId);
|
|
134
|
+
|
|
135
|
+
await adapter.deleteSchedule('test-script');
|
|
136
|
+
|
|
137
|
+
expect(adapter.intervals.has('test-script')).toBe(false);
|
|
138
|
+
expect(adapter.size).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('setScheduleEnabled()', () => {
|
|
143
|
+
beforeEach(async () => {
|
|
144
|
+
await adapter.createSchedule({
|
|
145
|
+
scriptName: 'test-script',
|
|
146
|
+
cronExpression: '0 0 * * *',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should disable a schedule', async () => {
|
|
151
|
+
await adapter.setScheduleEnabled('test-script', false);
|
|
152
|
+
|
|
153
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
154
|
+
expect(schedule.State).toBe('DISABLED');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should enable a schedule', async () => {
|
|
158
|
+
await adapter.setScheduleEnabled('test-script', false);
|
|
159
|
+
await adapter.setScheduleEnabled('test-script', true);
|
|
160
|
+
|
|
161
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
162
|
+
expect(schedule.State).toBe('ENABLED');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should throw error if schedule not found', async () => {
|
|
166
|
+
await expect(
|
|
167
|
+
adapter.setScheduleEnabled('non-existent', true)
|
|
168
|
+
).rejects.toThrow('Schedule for script "non-existent" not found');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should update the updatedAt timestamp', async () => {
|
|
172
|
+
const schedule1 = await adapter.getSchedule('test-script');
|
|
173
|
+
const originalUpdatedAt = schedule1.LastModificationDate;
|
|
174
|
+
|
|
175
|
+
// Wait a bit to ensure timestamp changes
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
177
|
+
|
|
178
|
+
await adapter.setScheduleEnabled('test-script', false);
|
|
179
|
+
|
|
180
|
+
const schedule2 = await adapter.getSchedule('test-script');
|
|
181
|
+
expect(schedule2.LastModificationDate.getTime()).toBeGreaterThan(
|
|
182
|
+
originalUpdatedAt.getTime()
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('listSchedules()', () => {
|
|
188
|
+
it('should return empty array when no schedules exist', async () => {
|
|
189
|
+
const schedules = await adapter.listSchedules();
|
|
190
|
+
|
|
191
|
+
expect(schedules).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should return all schedules', async () => {
|
|
195
|
+
await adapter.createSchedule({
|
|
196
|
+
scriptName: 'script-1',
|
|
197
|
+
cronExpression: '0 0 * * *',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await adapter.createSchedule({
|
|
201
|
+
scriptName: 'script-2',
|
|
202
|
+
cronExpression: '0 12 * * *',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await adapter.createSchedule({
|
|
206
|
+
scriptName: 'script-3',
|
|
207
|
+
cronExpression: '0 18 * * *',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const schedules = await adapter.listSchedules();
|
|
211
|
+
|
|
212
|
+
expect(schedules).toHaveLength(3);
|
|
213
|
+
expect(schedules.map((s) => s.scriptName)).toContain('script-1');
|
|
214
|
+
expect(schedules.map((s) => s.scriptName)).toContain('script-2');
|
|
215
|
+
expect(schedules.map((s) => s.scriptName)).toContain('script-3');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should include all schedule properties', async () => {
|
|
219
|
+
await adapter.createSchedule({
|
|
220
|
+
scriptName: 'test-script',
|
|
221
|
+
cronExpression: '0 0 * * *',
|
|
222
|
+
timezone: 'America/New_York',
|
|
223
|
+
input: { key: 'value' },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const schedules = await adapter.listSchedules();
|
|
227
|
+
|
|
228
|
+
expect(schedules[0]).toMatchObject({
|
|
229
|
+
scriptName: 'test-script',
|
|
230
|
+
cronExpression: '0 0 * * *',
|
|
231
|
+
timezone: 'America/New_York',
|
|
232
|
+
input: { key: 'value' },
|
|
233
|
+
enabled: true,
|
|
234
|
+
});
|
|
235
|
+
expect(schedules[0]).toHaveProperty('createdAt');
|
|
236
|
+
expect(schedules[0]).toHaveProperty('updatedAt');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('getSchedule()', () => {
|
|
241
|
+
beforeEach(async () => {
|
|
242
|
+
await adapter.createSchedule({
|
|
243
|
+
scriptName: 'test-script',
|
|
244
|
+
cronExpression: '0 0 * * *',
|
|
245
|
+
timezone: 'America/New_York',
|
|
246
|
+
input: { key: 'value' },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return schedule details', async () => {
|
|
251
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
252
|
+
|
|
253
|
+
expect(schedule.Name).toBe('test-script');
|
|
254
|
+
expect(schedule.State).toBe('ENABLED');
|
|
255
|
+
expect(schedule.ScheduleExpression).toBe('0 0 * * *');
|
|
256
|
+
expect(schedule.ScheduleExpressionTimezone).toBe('America/New_York');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should include target configuration', async () => {
|
|
260
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
261
|
+
|
|
262
|
+
const targetInput = JSON.parse(schedule.Target.Input);
|
|
263
|
+
expect(targetInput).toEqual({
|
|
264
|
+
scriptName: 'test-script',
|
|
265
|
+
trigger: 'SCHEDULED',
|
|
266
|
+
params: { key: 'value' },
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should include creation and modification dates', async () => {
|
|
271
|
+
const schedule = await adapter.getSchedule('test-script');
|
|
272
|
+
|
|
273
|
+
expect(schedule.CreationDate).toBeInstanceOf(Date);
|
|
274
|
+
expect(schedule.LastModificationDate).toBeInstanceOf(Date);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should throw error if schedule not found', async () => {
|
|
278
|
+
await expect(adapter.getSchedule('non-existent')).rejects.toThrow(
|
|
279
|
+
'Schedule for script "non-existent" not found'
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('Utility methods', () => {
|
|
285
|
+
it('clear() should remove all schedules', async () => {
|
|
286
|
+
await adapter.createSchedule({
|
|
287
|
+
scriptName: 'script-1',
|
|
288
|
+
cronExpression: '0 0 * * *',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await adapter.createSchedule({
|
|
292
|
+
scriptName: 'script-2',
|
|
293
|
+
cronExpression: '0 12 * * *',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(adapter.size).toBe(2);
|
|
297
|
+
|
|
298
|
+
adapter.clear();
|
|
299
|
+
|
|
300
|
+
expect(adapter.size).toBe(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('size should return number of schedules', async () => {
|
|
304
|
+
expect(adapter.size).toBe(0);
|
|
305
|
+
|
|
306
|
+
await adapter.createSchedule({
|
|
307
|
+
scriptName: 'script-1',
|
|
308
|
+
cronExpression: '0 0 * * *',
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(adapter.size).toBe(1);
|
|
312
|
+
|
|
313
|
+
await adapter.createSchedule({
|
|
314
|
+
scriptName: 'script-2',
|
|
315
|
+
cronExpression: '0 12 * * *',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(adapter.size).toBe(2);
|
|
319
|
+
|
|
320
|
+
await adapter.deleteSchedule('script-1');
|
|
321
|
+
|
|
322
|
+
expect(adapter.size).toBe(1);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createSchedulerAdapter,
|
|
3
|
+
detectSchedulerAdapterType,
|
|
4
|
+
} = require('../scheduler-adapter-factory');
|
|
5
|
+
const { AWSSchedulerAdapter } = require('../aws-scheduler-adapter');
|
|
6
|
+
const { LocalSchedulerAdapter } = require('../local-scheduler-adapter');
|
|
7
|
+
|
|
8
|
+
// Mock AWS SDK to prevent actual AWS calls
|
|
9
|
+
jest.mock('@aws-sdk/client-scheduler', () => ({
|
|
10
|
+
SchedulerClient: jest.fn(() => ({
|
|
11
|
+
send: jest.fn(),
|
|
12
|
+
})),
|
|
13
|
+
CreateScheduleCommand: jest.fn(),
|
|
14
|
+
DeleteScheduleCommand: jest.fn(),
|
|
15
|
+
GetScheduleCommand: jest.fn(),
|
|
16
|
+
UpdateScheduleCommand: jest.fn(),
|
|
17
|
+
ListSchedulesCommand: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('Scheduler Adapter Factory', () => {
|
|
21
|
+
let originalEnv;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
originalEnv = { ...process.env };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
// Reset environment variables
|
|
29
|
+
delete process.env.SCHEDULER_ADAPTER;
|
|
30
|
+
delete process.env.STAGE;
|
|
31
|
+
delete process.env.NODE_ENV;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
process.env = originalEnv;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('createSchedulerAdapter()', () => {
|
|
39
|
+
it('should create local adapter by default', () => {
|
|
40
|
+
const adapter = createSchedulerAdapter();
|
|
41
|
+
|
|
42
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
43
|
+
expect(adapter.getName()).toBe('local-cron');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create local adapter when explicitly specified', () => {
|
|
47
|
+
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
48
|
+
|
|
49
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should create AWS adapter when type is "aws"', () => {
|
|
53
|
+
const adapter = createSchedulerAdapter({ type: 'aws' });
|
|
54
|
+
|
|
55
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
56
|
+
expect(adapter.getName()).toBe('aws-eventbridge-scheduler');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should create AWS adapter when type is "eventbridge"', () => {
|
|
60
|
+
const adapter = createSchedulerAdapter({ type: 'eventbridge' });
|
|
61
|
+
|
|
62
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should use SCHEDULER_ADAPTER env variable', () => {
|
|
66
|
+
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
67
|
+
|
|
68
|
+
const adapter = createSchedulerAdapter();
|
|
69
|
+
|
|
70
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should allow explicit type to override env variable', () => {
|
|
74
|
+
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
75
|
+
|
|
76
|
+
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
77
|
+
|
|
78
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle case-insensitive type values', () => {
|
|
82
|
+
const adapter1 = createSchedulerAdapter({ type: 'AWS' });
|
|
83
|
+
const adapter2 = createSchedulerAdapter({ type: 'LOCAL' });
|
|
84
|
+
const adapter3 = createSchedulerAdapter({ type: 'EventBridge' });
|
|
85
|
+
|
|
86
|
+
expect(adapter1).toBeInstanceOf(AWSSchedulerAdapter);
|
|
87
|
+
expect(adapter2).toBeInstanceOf(LocalSchedulerAdapter);
|
|
88
|
+
expect(adapter3).toBeInstanceOf(AWSSchedulerAdapter);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should pass AWS configuration to AWS adapter', () => {
|
|
92
|
+
const config = {
|
|
93
|
+
type: 'aws',
|
|
94
|
+
region: 'eu-west-1',
|
|
95
|
+
targetLambdaArn: 'arn:aws:lambda:eu-west-1:123456789012:function:test',
|
|
96
|
+
scheduleGroupName: 'custom-group',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const adapter = createSchedulerAdapter(config);
|
|
100
|
+
|
|
101
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
102
|
+
expect(adapter.region).toBe('eu-west-1');
|
|
103
|
+
expect(adapter.targetLambdaArn).toBe('arn:aws:lambda:eu-west-1:123456789012:function:test');
|
|
104
|
+
expect(adapter.scheduleGroupName).toBe('custom-group');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should ignore AWS config for local adapter', () => {
|
|
108
|
+
const config = {
|
|
109
|
+
type: 'local',
|
|
110
|
+
region: 'eu-west-1', // This should be ignored
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const adapter = createSchedulerAdapter(config);
|
|
114
|
+
|
|
115
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
116
|
+
expect(adapter.region).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle unknown adapter type by creating local adapter', () => {
|
|
120
|
+
const adapter = createSchedulerAdapter({ type: 'unknown-type' });
|
|
121
|
+
|
|
122
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('detectSchedulerAdapterType()', () => {
|
|
127
|
+
it('should return "local" by default', () => {
|
|
128
|
+
const type = detectSchedulerAdapterType();
|
|
129
|
+
|
|
130
|
+
expect(type).toBe('local');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return env SCHEDULER_ADAPTER when set', () => {
|
|
134
|
+
process.env.SCHEDULER_ADAPTER = 'aws';
|
|
135
|
+
|
|
136
|
+
const type = detectSchedulerAdapterType();
|
|
137
|
+
|
|
138
|
+
expect(type).toBe('aws');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return "aws" for production stage', () => {
|
|
142
|
+
process.env.STAGE = 'production';
|
|
143
|
+
|
|
144
|
+
const type = detectSchedulerAdapterType();
|
|
145
|
+
|
|
146
|
+
expect(type).toBe('aws');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return "aws" for prod stage', () => {
|
|
150
|
+
process.env.STAGE = 'prod';
|
|
151
|
+
|
|
152
|
+
const type = detectSchedulerAdapterType();
|
|
153
|
+
|
|
154
|
+
expect(type).toBe('aws');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return "aws" for staging stage', () => {
|
|
158
|
+
process.env.STAGE = 'staging';
|
|
159
|
+
|
|
160
|
+
const type = detectSchedulerAdapterType();
|
|
161
|
+
|
|
162
|
+
expect(type).toBe('aws');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return "aws" for stage stage', () => {
|
|
166
|
+
process.env.STAGE = 'stage';
|
|
167
|
+
|
|
168
|
+
const type = detectSchedulerAdapterType();
|
|
169
|
+
|
|
170
|
+
expect(type).toBe('aws');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle case-insensitive stage values', () => {
|
|
174
|
+
process.env.STAGE = 'PRODUCTION';
|
|
175
|
+
|
|
176
|
+
const type = detectSchedulerAdapterType();
|
|
177
|
+
|
|
178
|
+
expect(type).toBe('aws');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return "local" for dev stage', () => {
|
|
182
|
+
process.env.STAGE = 'dev';
|
|
183
|
+
|
|
184
|
+
const type = detectSchedulerAdapterType();
|
|
185
|
+
|
|
186
|
+
expect(type).toBe('local');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return "local" for development stage', () => {
|
|
190
|
+
process.env.STAGE = 'development';
|
|
191
|
+
|
|
192
|
+
const type = detectSchedulerAdapterType();
|
|
193
|
+
|
|
194
|
+
expect(type).toBe('local');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should return "local" for test stage', () => {
|
|
198
|
+
process.env.STAGE = 'test';
|
|
199
|
+
|
|
200
|
+
const type = detectSchedulerAdapterType();
|
|
201
|
+
|
|
202
|
+
expect(type).toBe('local');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return "local" for local stage', () => {
|
|
206
|
+
process.env.STAGE = 'local';
|
|
207
|
+
|
|
208
|
+
const type = detectSchedulerAdapterType();
|
|
209
|
+
|
|
210
|
+
expect(type).toBe('local');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should use NODE_ENV as fallback for STAGE', () => {
|
|
214
|
+
delete process.env.STAGE;
|
|
215
|
+
process.env.NODE_ENV = 'production';
|
|
216
|
+
|
|
217
|
+
const type = detectSchedulerAdapterType();
|
|
218
|
+
|
|
219
|
+
expect(type).toBe('aws');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should prioritize explicit SCHEDULER_ADAPTER over auto-detection', () => {
|
|
223
|
+
process.env.SCHEDULER_ADAPTER = 'local';
|
|
224
|
+
process.env.STAGE = 'production';
|
|
225
|
+
|
|
226
|
+
const type = detectSchedulerAdapterType();
|
|
227
|
+
|
|
228
|
+
expect(type).toBe('local');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('Integration with createSchedulerAdapter', () => {
|
|
233
|
+
it('should auto-detect and create AWS adapter in production', () => {
|
|
234
|
+
process.env.STAGE = 'production';
|
|
235
|
+
|
|
236
|
+
const adapter = createSchedulerAdapter();
|
|
237
|
+
|
|
238
|
+
expect(adapter).toBeInstanceOf(AWSSchedulerAdapter);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should auto-detect and create local adapter in development', () => {
|
|
242
|
+
process.env.STAGE = 'development';
|
|
243
|
+
|
|
244
|
+
const adapter = createSchedulerAdapter();
|
|
245
|
+
|
|
246
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should allow explicit override of auto-detection', () => {
|
|
250
|
+
process.env.STAGE = 'production';
|
|
251
|
+
|
|
252
|
+
const adapter = createSchedulerAdapter({ type: 'local' });
|
|
253
|
+
|
|
254
|
+
expect(adapter).toBeInstanceOf(LocalSchedulerAdapter);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { SchedulerAdapter } = require('../scheduler-adapter');
|
|
2
|
+
|
|
3
|
+
describe('SchedulerAdapter', () => {
|
|
4
|
+
let adapter;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
adapter = new SchedulerAdapter();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Abstract base class', () => {
|
|
11
|
+
it('should throw error for getName()', () => {
|
|
12
|
+
expect(() => adapter.getName()).toThrow(
|
|
13
|
+
'SchedulerAdapter.getName() must be implemented'
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should throw error for createSchedule()', async () => {
|
|
18
|
+
await expect(adapter.createSchedule({})).rejects.toThrow(
|
|
19
|
+
'SchedulerAdapter.createSchedule() must be implemented'
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should throw error for deleteSchedule()', async () => {
|
|
24
|
+
await expect(adapter.deleteSchedule('test')).rejects.toThrow(
|
|
25
|
+
'SchedulerAdapter.deleteSchedule() must be implemented'
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw error for setScheduleEnabled()', async () => {
|
|
30
|
+
await expect(adapter.setScheduleEnabled('test', true)).rejects.toThrow(
|
|
31
|
+
'SchedulerAdapter.setScheduleEnabled() must be implemented'
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should throw error for listSchedules()', async () => {
|
|
36
|
+
await expect(adapter.listSchedules()).rejects.toThrow(
|
|
37
|
+
'SchedulerAdapter.listSchedules() must be implemented'
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw error for getSchedule()', async () => {
|
|
42
|
+
await expect(adapter.getSchedule('test')).rejects.toThrow(
|
|
43
|
+
'SchedulerAdapter.getSchedule() must be implemented'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Inheritance', () => {
|
|
49
|
+
it('should be extendable by concrete implementations', () => {
|
|
50
|
+
class TestSchedulerAdapter extends SchedulerAdapter {
|
|
51
|
+
getName() {
|
|
52
|
+
return 'test-adapter';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async createSchedule(config) {
|
|
56
|
+
return { scheduleName: config.scriptName };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async deleteSchedule(scriptName) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async setScheduleEnabled(scriptName, enabled) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async listSchedules() {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getSchedule(scriptName) {
|
|
72
|
+
return { scriptName };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const testAdapter = new TestSchedulerAdapter();
|
|
77
|
+
|
|
78
|
+
expect(testAdapter).toBeInstanceOf(SchedulerAdapter);
|
|
79
|
+
expect(testAdapter.getName()).toBe('test-adapter');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should require all abstract methods to be implemented', async () => {
|
|
83
|
+
class IncompleteAdapter extends SchedulerAdapter {
|
|
84
|
+
getName() {
|
|
85
|
+
return 'incomplete';
|
|
86
|
+
}
|
|
87
|
+
// Missing other methods
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const incomplete = new IncompleteAdapter();
|
|
91
|
+
|
|
92
|
+
// Should work for implemented method
|
|
93
|
+
expect(incomplete.getName()).toBe('incomplete');
|
|
94
|
+
|
|
95
|
+
// Should throw for missing methods
|
|
96
|
+
await expect(incomplete.createSchedule({})).rejects.toThrow();
|
|
97
|
+
await expect(incomplete.deleteSchedule('test')).rejects.toThrow();
|
|
98
|
+
await expect(incomplete.setScheduleEnabled('test', true)).rejects.toThrow();
|
|
99
|
+
await expect(incomplete.listSchedules()).rejects.toThrow();
|
|
100
|
+
await expect(incomplete.getSchedule('test')).rejects.toThrow();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|