@friggframework/admin-scripts 2.0.0--canary.545.b4ca16d.0 → 2.0.0--canary.517.ff03f2c.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.
Files changed (45) hide show
  1. package/README.md +272 -0
  2. package/index.js +22 -19
  3. package/package.json +6 -6
  4. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
  5. package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
  6. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
  7. package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
  8. package/src/adapters/aws-scheduler-adapter.js +55 -28
  9. package/src/adapters/local-scheduler-adapter.js +2 -2
  10. package/src/adapters/scheduler-adapter-factory.js +3 -1
  11. package/src/adapters/scheduler-adapter.js +9 -3
  12. package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
  13. package/src/application/__tests__/admin-script-base.test.js +23 -6
  14. package/src/application/__tests__/script-factory.test.js +30 -6
  15. package/src/application/__tests__/script-runner.test.js +113 -24
  16. package/src/application/__tests__/validate-script-input.test.js +54 -15
  17. package/src/application/admin-frigg-commands.js +21 -9
  18. package/src/application/admin-script-base.js +3 -2
  19. package/src/application/script-factory.js +3 -1
  20. package/src/application/script-runner.js +90 -48
  21. package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
  22. package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
  23. package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
  24. package/src/application/use-cases/delete-schedule-use-case.js +6 -4
  25. package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
  26. package/src/application/use-cases/index.js +3 -1
  27. package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
  28. package/src/application/validate-script-input.js +10 -3
  29. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
  30. package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
  31. package/src/infrastructure/admin-auth-middleware.js +3 -1
  32. package/src/infrastructure/admin-script-router.js +129 -14
  33. package/src/infrastructure/bootstrap.js +82 -0
  34. package/src/infrastructure/script-executor-handler.js +119 -52
  35. package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
  36. package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
  37. package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
  38. package/src/application/dry-run-http-interceptor.js +0 -296
  39. package/src/application/dry-run-repository-wrapper.js +0 -261
  40. package/src/application/schedule-management-use-case.js +0 -230
  41. package/src/builtins/__tests__/integration-health-check.test.js +0 -607
  42. package/src/builtins/__tests__/oauth-token-refresh.test.js +0 -354
  43. package/src/builtins/index.js +0 -28
  44. package/src/builtins/integration-health-check.js +0 -278
  45. package/src/builtins/oauth-token-refresh.js +0 -220
@@ -1,607 +0,0 @@
1
- const { IntegrationHealthCheckScript } = require('../integration-health-check');
2
-
3
- describe('IntegrationHealthCheckScript', () => {
4
- describe('Definition', () => {
5
- it('should have correct name and metadata', () => {
6
- expect(IntegrationHealthCheckScript.Definition.name).toBe('integration-health-check');
7
- expect(IntegrationHealthCheckScript.Definition.version).toBe('1.0.0');
8
- expect(IntegrationHealthCheckScript.Definition.source).toBe('BUILTIN');
9
- expect(IntegrationHealthCheckScript.Definition.config.requireIntegrationInstance).toBe(true);
10
- });
11
-
12
- it('should have valid input schema', () => {
13
- const schema = IntegrationHealthCheckScript.Definition.inputSchema;
14
- expect(schema.type).toBe('object');
15
- expect(schema.properties.integrationIds).toBeDefined();
16
- expect(schema.properties.checkCredentials).toBeDefined();
17
- expect(schema.properties.checkConnectivity).toBeDefined();
18
- expect(schema.properties.updateStatus).toBeDefined();
19
- });
20
-
21
- it('should have valid output schema', () => {
22
- const schema = IntegrationHealthCheckScript.Definition.outputSchema;
23
- expect(schema.type).toBe('object');
24
- expect(schema.properties.healthy).toBeDefined();
25
- expect(schema.properties.unhealthy).toBeDefined();
26
- expect(schema.properties.unknown).toBeDefined();
27
- expect(schema.properties.results).toBeDefined();
28
- });
29
-
30
- it('should have schedule configuration', () => {
31
- const schedule = IntegrationHealthCheckScript.Definition.schedule;
32
- expect(schedule).toBeDefined();
33
- expect(schedule.enabled).toBe(false);
34
- expect(schedule.cronExpression).toBe('cron(0 6 * * ? *)');
35
- });
36
-
37
- it('should have appropriate timeout configuration', () => {
38
- expect(IntegrationHealthCheckScript.Definition.config.timeout).toBe(900000); // 15 minutes
39
- });
40
-
41
- it('should have clean display object', () => {
42
- // Display should only have UI-specific fields
43
- expect(IntegrationHealthCheckScript.Definition.display.category).toBe('maintenance');
44
- // Should NOT have redundant label/description - they're derived from top-level
45
- });
46
- });
47
-
48
- describe('execute()', () => {
49
- let script;
50
- let mockContext;
51
-
52
- beforeEach(() => {
53
- mockContext = {
54
- log: jest.fn(),
55
- integrationRepository: {
56
- findIntegrations: jest.fn(),
57
- findIntegrationById: jest.fn(),
58
- updateIntegrationStatus: jest.fn(),
59
- },
60
- instantiate: jest.fn(),
61
- };
62
- script = new IntegrationHealthCheckScript({ context: mockContext });
63
- });
64
-
65
- it('should return empty results when no integrations found', async () => {
66
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([]);
67
-
68
- const result = await script.execute({});
69
-
70
- expect(result.healthy).toBe(0);
71
- expect(result.unhealthy).toBe(0);
72
- expect(result.unknown).toBe(0);
73
- expect(result.results).toEqual([]);
74
- });
75
-
76
- it('should return healthy for valid integrations', async () => {
77
- const integration = {
78
- id: 'int-1',
79
- config: {
80
- type: 'hubspot',
81
- credentials: {
82
- access_token: 'token123',
83
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
84
- }
85
- }
86
- };
87
-
88
- const mockInstance = {
89
- primary: {
90
- api: {
91
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
92
- }
93
- }
94
- };
95
-
96
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
97
- mockContext.instantiate.mockResolvedValue(mockInstance);
98
-
99
- const result = await script.execute({
100
- checkCredentials: true,
101
- checkConnectivity: true
102
- });
103
-
104
- expect(result.healthy).toBe(1);
105
- expect(result.unhealthy).toBe(0);
106
- expect(result.results[0]).toMatchObject({
107
- integrationId: 'int-1',
108
- status: 'healthy',
109
- issues: []
110
- });
111
- expect(mockInstance.primary.api.getAuthenticationInfo).toHaveBeenCalled();
112
- });
113
-
114
- it('should return unhealthy for missing access token', async () => {
115
- const integration = {
116
- id: 'int-1',
117
- config: {
118
- type: 'hubspot',
119
- credentials: {} // No access_token
120
- }
121
- };
122
-
123
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
124
-
125
- const result = await script.execute({
126
- checkCredentials: true,
127
- checkConnectivity: false
128
- });
129
-
130
- expect(result.healthy).toBe(0);
131
- expect(result.unhealthy).toBe(1);
132
- expect(result.results[0]).toMatchObject({
133
- integrationId: 'int-1',
134
- status: 'unhealthy',
135
- issues: ['Missing access token']
136
- });
137
- });
138
-
139
- it('should return unhealthy for expired credentials', async () => {
140
- const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
141
- const integration = {
142
- id: 'int-1',
143
- config: {
144
- type: 'hubspot',
145
- credentials: {
146
- access_token: 'token123',
147
- expires_at: pastDate.toISOString()
148
- }
149
- }
150
- };
151
-
152
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
153
-
154
- const result = await script.execute({
155
- checkCredentials: true,
156
- checkConnectivity: false
157
- });
158
-
159
- expect(result.unhealthy).toBe(1);
160
- expect(result.results[0]).toMatchObject({
161
- integrationId: 'int-1',
162
- status: 'unhealthy',
163
- issues: ['Access token expired']
164
- });
165
- });
166
-
167
- it('should return unhealthy for connectivity failures', async () => {
168
- const integration = {
169
- id: 'int-1',
170
- config: {
171
- type: 'hubspot',
172
- credentials: {
173
- access_token: 'token123',
174
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
175
- }
176
- }
177
- };
178
-
179
- const mockInstance = {
180
- primary: {
181
- api: {
182
- getAuthenticationInfo: jest.fn().mockRejectedValue(new Error('Network error'))
183
- }
184
- }
185
- };
186
-
187
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
188
- mockContext.instantiate.mockResolvedValue(mockInstance);
189
-
190
- const result = await script.execute({
191
- checkCredentials: true,
192
- checkConnectivity: true
193
- });
194
-
195
- expect(result.unhealthy).toBe(1);
196
- expect(result.results[0].status).toBe('unhealthy');
197
- expect(result.results[0].issues).toContainEqual(expect.stringContaining('API connectivity failed'));
198
- });
199
-
200
- it('should update integration status when updateStatus is true', async () => {
201
- const integration = {
202
- id: 'int-1',
203
- config: {
204
- type: 'hubspot',
205
- credentials: {
206
- access_token: 'token123',
207
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
208
- }
209
- }
210
- };
211
-
212
- const mockInstance = {
213
- primary: {
214
- api: {
215
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
216
- }
217
- }
218
- };
219
-
220
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
221
- mockContext.instantiate.mockResolvedValue(mockInstance);
222
- mockContext.integrationRepository.updateIntegrationStatus.mockResolvedValue(undefined);
223
-
224
- const result = await script.execute({
225
- checkCredentials: true,
226
- checkConnectivity: true,
227
- updateStatus: true
228
- });
229
-
230
- expect(result.healthy).toBe(1);
231
- expect(mockContext.integrationRepository.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ACTIVE');
232
- });
233
-
234
- it('should update integration status to ERROR for unhealthy integrations', async () => {
235
- const integration = {
236
- id: 'int-1',
237
- config: {
238
- type: 'hubspot',
239
- credentials: {} // Missing credentials
240
- }
241
- };
242
-
243
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
244
- mockContext.integrationRepository.updateIntegrationStatus.mockResolvedValue(undefined);
245
-
246
- const result = await script.execute({
247
- checkCredentials: true,
248
- checkConnectivity: false,
249
- updateStatus: true
250
- });
251
-
252
- expect(result.unhealthy).toBe(1);
253
- expect(mockContext.integrationRepository.updateIntegrationStatus).toHaveBeenCalledWith('int-1', 'ERROR');
254
- });
255
-
256
- it('should not update status when updateStatus is false', async () => {
257
- const integration = {
258
- id: 'int-1',
259
- config: {
260
- type: 'hubspot',
261
- credentials: {
262
- access_token: 'token123',
263
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
264
- }
265
- }
266
- };
267
-
268
- const mockInstance = {
269
- primary: {
270
- api: {
271
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
272
- }
273
- }
274
- };
275
-
276
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
277
- mockContext.instantiate.mockResolvedValue(mockInstance);
278
-
279
- await script.execute({
280
- checkCredentials: true,
281
- checkConnectivity: true,
282
- updateStatus: false
283
- });
284
-
285
- expect(mockContext.integrationRepository.updateIntegrationStatus).not.toHaveBeenCalled();
286
- });
287
-
288
- it('should handle status update failures gracefully', async () => {
289
- const integration = {
290
- id: 'int-1',
291
- config: {
292
- type: 'hubspot',
293
- credentials: {
294
- access_token: 'token123',
295
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
296
- }
297
- }
298
- };
299
-
300
- const mockInstance = {
301
- primary: {
302
- api: {
303
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
304
- }
305
- }
306
- };
307
-
308
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
309
- mockContext.instantiate.mockResolvedValue(mockInstance);
310
- mockContext.integrationRepository.updateIntegrationStatus.mockRejectedValue(new Error('Update failed'));
311
-
312
- const result = await script.execute({
313
- checkCredentials: true,
314
- checkConnectivity: true,
315
- updateStatus: true
316
- });
317
-
318
- expect(result.healthy).toBe(1); // Should still report healthy
319
- expect(mockContext.log).toHaveBeenCalledWith(
320
- 'warn',
321
- expect.stringContaining('Failed to update status'),
322
- expect.any(Object)
323
- );
324
- });
325
-
326
- it('should filter by specific integration IDs', async () => {
327
- const integration1 = {
328
- id: 'int-1',
329
- config: { type: 'hubspot', credentials: { access_token: 'token1' } }
330
- };
331
- const integration2 = {
332
- id: 'int-2',
333
- config: { type: 'salesforce', credentials: { access_token: 'token2' } }
334
- };
335
-
336
- mockContext.integrationRepository.findIntegrationById.mockImplementation((id) => {
337
- if (id === 'int-1') return Promise.resolve(integration1);
338
- if (id === 'int-2') return Promise.resolve(integration2);
339
- return Promise.reject(new Error('Not found'));
340
- });
341
-
342
- const result = await script.execute({
343
- integrationIds: ['int-1', 'int-2'],
344
- checkCredentials: true,
345
- checkConnectivity: false
346
- });
347
-
348
- expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-1');
349
- expect(mockContext.integrationRepository.findIntegrationById).toHaveBeenCalledWith('int-2');
350
- expect(mockContext.integrationRepository.findIntegrations).not.toHaveBeenCalled();
351
- expect(result.results).toHaveLength(2);
352
- });
353
-
354
- it('should handle errors when checking integrations', async () => {
355
- const integration = {
356
- id: 'int-1',
357
- config: {
358
- type: 'hubspot',
359
- credentials: {
360
- access_token: 'token123',
361
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
362
- }
363
- }
364
- };
365
-
366
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
367
- mockContext.instantiate.mockRejectedValue(new Error('Instantiation failed'));
368
-
369
- const result = await script.execute({
370
- checkCredentials: true,
371
- checkConnectivity: true
372
- });
373
-
374
- // Should still complete but mark as unknown or unhealthy
375
- expect(result.results).toHaveLength(1);
376
- expect(result.results[0].integrationId).toBe('int-1');
377
- });
378
-
379
- it('should skip credential check when checkCredentials is false', async () => {
380
- const integration = {
381
- id: 'int-1',
382
- config: {
383
- type: 'hubspot',
384
- credentials: {} // Missing credentials, but check is disabled
385
- }
386
- };
387
-
388
- const mockInstance = {
389
- primary: {
390
- api: {
391
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
392
- }
393
- }
394
- };
395
-
396
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
397
- mockContext.instantiate.mockResolvedValue(mockInstance);
398
-
399
- const result = await script.execute({
400
- checkCredentials: false,
401
- checkConnectivity: true
402
- });
403
-
404
- expect(result.results[0].checks.credentials).toBeUndefined();
405
- expect(result.results[0].checks.connectivity).toBeDefined();
406
- });
407
-
408
- it('should skip connectivity check when checkConnectivity is false', async () => {
409
- const integration = {
410
- id: 'int-1',
411
- config: {
412
- type: 'hubspot',
413
- credentials: {
414
- access_token: 'token123',
415
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
416
- }
417
- }
418
- };
419
-
420
- mockContext.integrationRepository.findIntegrations.mockResolvedValue([integration]);
421
-
422
- const result = await script.execute({
423
- checkCredentials: true,
424
- checkConnectivity: false
425
- });
426
-
427
- expect(result.results[0].checks.credentials).toBeDefined();
428
- expect(result.results[0].checks.connectivity).toBeUndefined();
429
- expect(mockContext.instantiate).not.toHaveBeenCalled();
430
- });
431
- });
432
-
433
- describe('checkCredentialValidity()', () => {
434
- let script;
435
-
436
- beforeEach(() => {
437
- script = new IntegrationHealthCheckScript({ context: { log: jest.fn() } });
438
- });
439
-
440
- it('should return valid for integrations with valid credentials', () => {
441
- const integration = {
442
- id: 'int-1',
443
- config: {
444
- credentials: {
445
- access_token: 'token123',
446
- expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
447
- }
448
- }
449
- };
450
-
451
- const result = script.checkCredentialValidity(integration);
452
-
453
- expect(result.valid).toBe(true);
454
- expect(result.issue).toBeNull();
455
- });
456
-
457
- it('should return invalid for missing access token', () => {
458
- const integration = {
459
- id: 'int-1',
460
- config: {
461
- credentials: {}
462
- }
463
- };
464
-
465
- const result = script.checkCredentialValidity(integration);
466
-
467
- expect(result.valid).toBe(false);
468
- expect(result.issue).toBe('Missing access token');
469
- });
470
-
471
- it('should return invalid for expired tokens', () => {
472
- const integration = {
473
- id: 'int-1',
474
- config: {
475
- credentials: {
476
- access_token: 'token123',
477
- expires_at: new Date(Date.now() - 1000).toISOString() // Expired
478
- }
479
- }
480
- };
481
-
482
- const result = script.checkCredentialValidity(integration);
483
-
484
- expect(result.valid).toBe(false);
485
- expect(result.issue).toBe('Access token expired');
486
- });
487
-
488
- it('should return valid for credentials without expiry', () => {
489
- const integration = {
490
- id: 'int-1',
491
- config: {
492
- credentials: {
493
- access_token: 'token123'
494
- // No expires_at
495
- }
496
- }
497
- };
498
-
499
- const result = script.checkCredentialValidity(integration);
500
-
501
- expect(result.valid).toBe(true);
502
- expect(result.issue).toBeNull();
503
- });
504
- });
505
-
506
- describe('checkApiConnectivity()', () => {
507
- let script;
508
- let mockContext;
509
-
510
- beforeEach(() => {
511
- mockContext = {
512
- log: jest.fn(),
513
- instantiate: jest.fn(),
514
- };
515
- script = new IntegrationHealthCheckScript({ context: mockContext });
516
- });
517
-
518
- it('should return valid for successful API calls', async () => {
519
- const integration = {
520
- id: 'int-1',
521
- config: { type: 'hubspot' }
522
- };
523
-
524
- const mockInstance = {
525
- primary: {
526
- api: {
527
- getAuthenticationInfo: jest.fn().mockResolvedValue({ user: 'test' })
528
- }
529
- }
530
- };
531
-
532
- mockContext.instantiate.mockResolvedValue(mockInstance);
533
-
534
- const result = await script.checkApiConnectivity(integration);
535
-
536
- expect(result.valid).toBe(true);
537
- expect(result.issue).toBeNull();
538
- expect(result.responseTime).toBeGreaterThanOrEqual(0);
539
- });
540
-
541
- it('should try getCurrentUser if getAuthenticationInfo is not available', async () => {
542
- const integration = {
543
- id: 'int-1',
544
- config: { type: 'hubspot' }
545
- };
546
-
547
- const mockInstance = {
548
- primary: {
549
- api: {
550
- getCurrentUser: jest.fn().mockResolvedValue({ user: 'test' })
551
- }
552
- }
553
- };
554
-
555
- mockContext.instantiate.mockResolvedValue(mockInstance);
556
-
557
- const result = await script.checkApiConnectivity(integration);
558
-
559
- expect(result.valid).toBe(true);
560
- expect(mockInstance.primary.api.getCurrentUser).toHaveBeenCalled();
561
- });
562
-
563
- it('should return note when no health check endpoint is available', async () => {
564
- const integration = {
565
- id: 'int-1',
566
- config: { type: 'hubspot' }
567
- };
568
-
569
- const mockInstance = {
570
- primary: {
571
- api: {} // No health check methods
572
- }
573
- };
574
-
575
- mockContext.instantiate.mockResolvedValue(mockInstance);
576
-
577
- const result = await script.checkApiConnectivity(integration);
578
-
579
- expect(result.valid).toBe(true);
580
- expect(result.issue).toBeNull();
581
- expect(result.note).toBe('No health check endpoint available');
582
- });
583
-
584
- it('should return invalid for API failures', async () => {
585
- const integration = {
586
- id: 'int-1',
587
- config: { type: 'hubspot' }
588
- };
589
-
590
- const mockInstance = {
591
- primary: {
592
- api: {
593
- getAuthenticationInfo: jest.fn().mockRejectedValue(new Error('Network error'))
594
- }
595
- }
596
- };
597
-
598
- mockContext.instantiate.mockResolvedValue(mockInstance);
599
-
600
- const result = await script.checkApiConnectivity(integration);
601
-
602
- expect(result.valid).toBe(false);
603
- expect(result.issue).toContain('API connectivity failed');
604
- expect(result.issue).toContain('Network error');
605
- });
606
- });
607
- });