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