@friggframework/admin-scripts 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.21b69ac.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/PR_517_REVIEW_TRACKER.md +56 -0
- package/index.js +12 -3
- package/package.json +6 -9
- package/src/application/__tests__/admin-frigg-commands.test.js +19 -19
- package/src/application/__tests__/admin-script-base.test.js +100 -84
- package/src/application/__tests__/script-runner.test.js +146 -16
- package/src/application/admin-frigg-commands.js +20 -32
- package/src/application/admin-script-base.js +20 -99
- package/src/application/script-runner.js +131 -135
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +168 -0
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +114 -0
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +201 -0
- package/src/application/use-cases/delete-schedule-use-case.js +108 -0
- package/src/application/use-cases/get-effective-schedule-use-case.js +78 -0
- package/src/application/use-cases/index.js +18 -0
- package/src/application/use-cases/upsert-schedule-use-case.js +127 -0
- package/src/builtins/__tests__/integration-health-check.test.js +67 -60
- package/src/builtins/__tests__/oauth-token-refresh.test.js +45 -37
- package/src/builtins/integration-health-check.js +23 -24
- package/src/builtins/oauth-token-refresh.js +19 -20
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +32 -95
- package/src/infrastructure/__tests__/admin-script-router.test.js +46 -47
- package/src/infrastructure/admin-auth-middleware.js +5 -43
- package/src/infrastructure/admin-script-router.js +38 -32
- package/src/infrastructure/script-executor-handler.js +2 -2
- package/src/application/__tests__/dry-run-http-interceptor.test.js +0 -313
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +0 -257
- package/src/application/__tests__/schedule-management-use-case.test.js +0 -276
- package/src/application/dry-run-http-interceptor.js +0 -296
- package/src/application/dry-run-repository-wrapper.js +0 -261
- package/src/application/schedule-management-use-case.js +0 -230
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
const { ScheduleManagementUseCase } = require('../schedule-management-use-case');
|
|
2
|
-
|
|
3
|
-
describe('ScheduleManagementUseCase', () => {
|
|
4
|
-
let useCase;
|
|
5
|
-
let mockCommands;
|
|
6
|
-
let mockSchedulerAdapter;
|
|
7
|
-
let mockScriptFactory;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
mockCommands = {
|
|
11
|
-
getScheduleByScriptName: jest.fn(),
|
|
12
|
-
upsertSchedule: jest.fn(),
|
|
13
|
-
updateScheduleAwsInfo: jest.fn(),
|
|
14
|
-
deleteSchedule: jest.fn(),
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
mockSchedulerAdapter = {
|
|
18
|
-
createSchedule: jest.fn(),
|
|
19
|
-
deleteSchedule: jest.fn(),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
mockScriptFactory = {
|
|
23
|
-
has: jest.fn(),
|
|
24
|
-
get: jest.fn(),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
useCase = new ScheduleManagementUseCase({
|
|
28
|
-
commands: mockCommands,
|
|
29
|
-
schedulerAdapter: mockSchedulerAdapter,
|
|
30
|
-
scriptFactory: mockScriptFactory,
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('getEffectiveSchedule', () => {
|
|
35
|
-
it('should return database schedule when override exists', async () => {
|
|
36
|
-
const dbSchedule = {
|
|
37
|
-
scriptName: 'test-script',
|
|
38
|
-
enabled: true,
|
|
39
|
-
cronExpression: '0 9 * * *',
|
|
40
|
-
timezone: 'UTC',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
44
|
-
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
45
|
-
mockCommands.getScheduleByScriptName.mockResolvedValue(dbSchedule);
|
|
46
|
-
|
|
47
|
-
const result = await useCase.getEffectiveSchedule('test-script');
|
|
48
|
-
|
|
49
|
-
expect(result.source).toBe('database');
|
|
50
|
-
expect(result.schedule).toEqual(dbSchedule);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should return definition schedule when no database override', async () => {
|
|
54
|
-
const definitionSchedule = {
|
|
55
|
-
enabled: true,
|
|
56
|
-
cronExpression: '0 12 * * *',
|
|
57
|
-
timezone: 'America/New_York',
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
61
|
-
mockScriptFactory.get.mockReturnValue({
|
|
62
|
-
Definition: { schedule: definitionSchedule },
|
|
63
|
-
});
|
|
64
|
-
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
65
|
-
|
|
66
|
-
const result = await useCase.getEffectiveSchedule('test-script');
|
|
67
|
-
|
|
68
|
-
expect(result.source).toBe('definition');
|
|
69
|
-
expect(result.schedule.enabled).toBe(true);
|
|
70
|
-
expect(result.schedule.cronExpression).toBe('0 12 * * *');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should return none when no schedule configured', async () => {
|
|
74
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
75
|
-
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
76
|
-
mockCommands.getScheduleByScriptName.mockResolvedValue(null);
|
|
77
|
-
|
|
78
|
-
const result = await useCase.getEffectiveSchedule('test-script');
|
|
79
|
-
|
|
80
|
-
expect(result.source).toBe('none');
|
|
81
|
-
expect(result.schedule.enabled).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should throw error when script not found', async () => {
|
|
85
|
-
mockScriptFactory.has.mockReturnValue(false);
|
|
86
|
-
|
|
87
|
-
await expect(useCase.getEffectiveSchedule('non-existent'))
|
|
88
|
-
.rejects.toThrow('Script "non-existent" not found');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('upsertSchedule', () => {
|
|
93
|
-
it('should create schedule and provision EventBridge when enabled', async () => {
|
|
94
|
-
const savedSchedule = {
|
|
95
|
-
scriptName: 'test-script',
|
|
96
|
-
enabled: true,
|
|
97
|
-
cronExpression: '0 12 * * *',
|
|
98
|
-
timezone: 'UTC',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
102
|
-
mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
|
|
103
|
-
mockSchedulerAdapter.createSchedule.mockResolvedValue({
|
|
104
|
-
scheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
105
|
-
scheduleName: 'frigg-script-test-script',
|
|
106
|
-
});
|
|
107
|
-
mockCommands.updateScheduleAwsInfo.mockResolvedValue({
|
|
108
|
-
...savedSchedule,
|
|
109
|
-
awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const result = await useCase.upsertSchedule('test-script', {
|
|
113
|
-
enabled: true,
|
|
114
|
-
cronExpression: '0 12 * * *',
|
|
115
|
-
timezone: 'UTC',
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
expect(result.success).toBe(true);
|
|
119
|
-
expect(result.schedule.scriptName).toBe('test-script');
|
|
120
|
-
expect(mockSchedulerAdapter.createSchedule).toHaveBeenCalledWith({
|
|
121
|
-
scriptName: 'test-script',
|
|
122
|
-
cronExpression: '0 12 * * *',
|
|
123
|
-
timezone: 'UTC',
|
|
124
|
-
});
|
|
125
|
-
expect(mockCommands.updateScheduleAwsInfo).toHaveBeenCalled();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should delete EventBridge schedule when disabling', async () => {
|
|
129
|
-
const existingSchedule = {
|
|
130
|
-
scriptName: 'test-script',
|
|
131
|
-
enabled: false,
|
|
132
|
-
cronExpression: null,
|
|
133
|
-
timezone: 'UTC',
|
|
134
|
-
awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
138
|
-
mockCommands.upsertSchedule.mockResolvedValue(existingSchedule);
|
|
139
|
-
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
140
|
-
mockCommands.updateScheduleAwsInfo.mockResolvedValue({
|
|
141
|
-
...existingSchedule,
|
|
142
|
-
awsScheduleArn: null,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const result = await useCase.upsertSchedule('test-script', {
|
|
146
|
-
enabled: false,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
expect(result.success).toBe(true);
|
|
150
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should handle scheduler errors gracefully', async () => {
|
|
154
|
-
const savedSchedule = {
|
|
155
|
-
scriptName: 'test-script',
|
|
156
|
-
enabled: true,
|
|
157
|
-
cronExpression: '0 12 * * *',
|
|
158
|
-
timezone: 'UTC',
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
162
|
-
mockCommands.upsertSchedule.mockResolvedValue(savedSchedule);
|
|
163
|
-
mockSchedulerAdapter.createSchedule.mockRejectedValue(
|
|
164
|
-
new Error('AWS Scheduler API error')
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const result = await useCase.upsertSchedule('test-script', {
|
|
168
|
-
enabled: true,
|
|
169
|
-
cronExpression: '0 12 * * *',
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Should succeed with warning, not fail
|
|
173
|
-
expect(result.success).toBe(true);
|
|
174
|
-
expect(result.schedulerWarning).toBe('AWS Scheduler API error');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should throw error when script not found', async () => {
|
|
178
|
-
mockScriptFactory.has.mockReturnValue(false);
|
|
179
|
-
|
|
180
|
-
await expect(useCase.upsertSchedule('non-existent', { enabled: true }))
|
|
181
|
-
.rejects.toThrow('Script "non-existent" not found');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should throw error when enabled without cronExpression', async () => {
|
|
185
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
186
|
-
|
|
187
|
-
await expect(useCase.upsertSchedule('test-script', { enabled: true }))
|
|
188
|
-
.rejects.toThrow('cronExpression is required when enabled is true');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('deleteSchedule', () => {
|
|
193
|
-
it('should delete schedule and EventBridge rule', async () => {
|
|
194
|
-
const deletedSchedule = {
|
|
195
|
-
scriptName: 'test-script',
|
|
196
|
-
awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
200
|
-
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
201
|
-
mockCommands.deleteSchedule.mockResolvedValue({
|
|
202
|
-
deletedCount: 1,
|
|
203
|
-
deleted: deletedSchedule,
|
|
204
|
-
});
|
|
205
|
-
mockSchedulerAdapter.deleteSchedule.mockResolvedValue();
|
|
206
|
-
|
|
207
|
-
const result = await useCase.deleteSchedule('test-script');
|
|
208
|
-
|
|
209
|
-
expect(result.success).toBe(true);
|
|
210
|
-
expect(result.deletedCount).toBe(1);
|
|
211
|
-
expect(mockSchedulerAdapter.deleteSchedule).toHaveBeenCalledWith('test-script');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('should not call scheduler when no AWS rule exists', async () => {
|
|
215
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
216
|
-
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
217
|
-
mockCommands.deleteSchedule.mockResolvedValue({
|
|
218
|
-
deletedCount: 1,
|
|
219
|
-
deleted: { scriptName: 'test-script' }, // No awsScheduleArn
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const result = await useCase.deleteSchedule('test-script');
|
|
223
|
-
|
|
224
|
-
expect(result.success).toBe(true);
|
|
225
|
-
expect(mockSchedulerAdapter.deleteSchedule).not.toHaveBeenCalled();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should handle scheduler delete errors gracefully', async () => {
|
|
229
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
230
|
-
mockScriptFactory.get.mockReturnValue({ Definition: {} });
|
|
231
|
-
mockCommands.deleteSchedule.mockResolvedValue({
|
|
232
|
-
deletedCount: 1,
|
|
233
|
-
deleted: {
|
|
234
|
-
scriptName: 'test-script',
|
|
235
|
-
awsScheduleArn: 'arn:aws:scheduler:us-east-1:123:schedule/test',
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
mockSchedulerAdapter.deleteSchedule.mockRejectedValue(
|
|
239
|
-
new Error('AWS delete failed')
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const result = await useCase.deleteSchedule('test-script');
|
|
243
|
-
|
|
244
|
-
expect(result.success).toBe(true);
|
|
245
|
-
expect(result.schedulerWarning).toBe('AWS delete failed');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should return effective schedule after deletion', async () => {
|
|
249
|
-
const definitionSchedule = {
|
|
250
|
-
enabled: true,
|
|
251
|
-
cronExpression: '0 6 * * *',
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
mockScriptFactory.has.mockReturnValue(true);
|
|
255
|
-
mockScriptFactory.get.mockReturnValue({
|
|
256
|
-
Definition: { schedule: definitionSchedule },
|
|
257
|
-
});
|
|
258
|
-
mockCommands.deleteSchedule.mockResolvedValue({
|
|
259
|
-
deletedCount: 1,
|
|
260
|
-
deleted: { scriptName: 'test-script' },
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const result = await useCase.deleteSchedule('test-script');
|
|
264
|
-
|
|
265
|
-
expect(result.effectiveSchedule.source).toBe('definition');
|
|
266
|
-
expect(result.effectiveSchedule.enabled).toBe(true);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should throw error when script not found', async () => {
|
|
270
|
-
mockScriptFactory.has.mockReturnValue(false);
|
|
271
|
-
|
|
272
|
-
await expect(useCase.deleteSchedule('non-existent'))
|
|
273
|
-
.rejects.toThrow('Script "non-existent" not found');
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
});
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dry-Run HTTP Interceptor
|
|
3
|
-
*
|
|
4
|
-
* Creates a mock HTTP client that logs requests instead of executing them.
|
|
5
|
-
* Used to intercept API module calls during dry-run.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Sanitize headers to remove authentication tokens
|
|
10
|
-
* @param {Object} headers - HTTP headers
|
|
11
|
-
* @returns {Object} Sanitized headers
|
|
12
|
-
*/
|
|
13
|
-
function sanitizeHeaders(headers) {
|
|
14
|
-
if (!headers || typeof headers !== 'object') {
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const safe = { ...headers };
|
|
19
|
-
|
|
20
|
-
// Remove common auth headers
|
|
21
|
-
const sensitiveHeaders = [
|
|
22
|
-
'authorization',
|
|
23
|
-
'Authorization',
|
|
24
|
-
'x-api-key',
|
|
25
|
-
'X-API-Key',
|
|
26
|
-
'x-auth-token',
|
|
27
|
-
'X-Auth-Token',
|
|
28
|
-
'api-key',
|
|
29
|
-
'API-Key',
|
|
30
|
-
'apikey',
|
|
31
|
-
'ApiKey',
|
|
32
|
-
'token',
|
|
33
|
-
'Token',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
for (const header of sensitiveHeaders) {
|
|
37
|
-
if (safe[header]) {
|
|
38
|
-
safe[header] = '[REDACTED]';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return safe;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Detect service name from base URL
|
|
47
|
-
* @param {string} baseURL - Base URL of the API
|
|
48
|
-
* @returns {string} Service name
|
|
49
|
-
*/
|
|
50
|
-
function detectService(baseURL) {
|
|
51
|
-
if (!baseURL) return 'unknown';
|
|
52
|
-
|
|
53
|
-
const url = baseURL.toLowerCase();
|
|
54
|
-
|
|
55
|
-
// CRM Systems
|
|
56
|
-
if (url.includes('hubspot') || url.includes('hubapi')) return 'HubSpot';
|
|
57
|
-
if (url.includes('salesforce')) return 'Salesforce';
|
|
58
|
-
if (url.includes('pipedrive')) return 'Pipedrive';
|
|
59
|
-
if (url.includes('zoho')) return 'Zoho CRM';
|
|
60
|
-
if (url.includes('attio')) return 'Attio';
|
|
61
|
-
|
|
62
|
-
// Communication
|
|
63
|
-
if (url.includes('slack')) return 'Slack';
|
|
64
|
-
if (url.includes('discord')) return 'Discord';
|
|
65
|
-
if (url.includes('teams.microsoft')) return 'Microsoft Teams';
|
|
66
|
-
|
|
67
|
-
// Project Management
|
|
68
|
-
if (url.includes('asana')) return 'Asana';
|
|
69
|
-
if (url.includes('monday')) return 'Monday.com';
|
|
70
|
-
if (url.includes('trello')) return 'Trello';
|
|
71
|
-
if (url.includes('clickup')) return 'ClickUp';
|
|
72
|
-
|
|
73
|
-
// Storage
|
|
74
|
-
if (url.includes('googleapis.com/drive')) return 'Google Drive';
|
|
75
|
-
if (url.includes('dropbox')) return 'Dropbox';
|
|
76
|
-
if (url.includes('box.com')) return 'Box';
|
|
77
|
-
|
|
78
|
-
// Email & Marketing
|
|
79
|
-
if (url.includes('sendgrid')) return 'SendGrid';
|
|
80
|
-
if (url.includes('mailchimp')) return 'Mailchimp';
|
|
81
|
-
if (url.includes('gmail')) return 'Gmail';
|
|
82
|
-
|
|
83
|
-
// Accounting
|
|
84
|
-
if (url.includes('quickbooks')) return 'QuickBooks';
|
|
85
|
-
if (url.includes('xero')) return 'Xero';
|
|
86
|
-
|
|
87
|
-
// Other
|
|
88
|
-
if (url.includes('stripe')) return 'Stripe';
|
|
89
|
-
if (url.includes('shopify')) return 'Shopify';
|
|
90
|
-
if (url.includes('github')) return 'GitHub';
|
|
91
|
-
if (url.includes('gitlab')) return 'GitLab';
|
|
92
|
-
|
|
93
|
-
return 'unknown';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Sanitize request data to remove sensitive information
|
|
98
|
-
* @param {*} data - Request data
|
|
99
|
-
* @returns {*} Sanitized data
|
|
100
|
-
*/
|
|
101
|
-
function sanitizeData(data) {
|
|
102
|
-
if (data === null || data === undefined) {
|
|
103
|
-
return data;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (typeof data !== 'object') {
|
|
107
|
-
return data;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (Array.isArray(data)) {
|
|
111
|
-
return data.map(sanitizeData);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const sanitized = {};
|
|
115
|
-
for (const [key, value] of Object.entries(data)) {
|
|
116
|
-
const lowerKey = key.toLowerCase();
|
|
117
|
-
|
|
118
|
-
// Check if this is a leaf node that should be redacted
|
|
119
|
-
const isSensitiveField =
|
|
120
|
-
lowerKey === 'password' ||
|
|
121
|
-
lowerKey === 'token' ||
|
|
122
|
-
lowerKey === 'secret' ||
|
|
123
|
-
lowerKey === 'apikey' ||
|
|
124
|
-
lowerKey.endsWith('password') ||
|
|
125
|
-
lowerKey.endsWith('token') ||
|
|
126
|
-
lowerKey.endsWith('secret') ||
|
|
127
|
-
lowerKey.endsWith('key') && !lowerKey.endsWith('publickey');
|
|
128
|
-
|
|
129
|
-
// Only redact if it's a primitive value (not an object/array)
|
|
130
|
-
if (isSensitiveField && typeof value !== 'object') {
|
|
131
|
-
sanitized[key] = '[REDACTED]';
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Recursively sanitize nested objects
|
|
136
|
-
if (typeof value === 'object' && value !== null) {
|
|
137
|
-
sanitized[key] = sanitizeData(value);
|
|
138
|
-
} else {
|
|
139
|
-
sanitized[key] = value;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return sanitized;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Create a dry-run HTTP client
|
|
148
|
-
*
|
|
149
|
-
* @param {Array} operationLog - Array to append logged HTTP requests
|
|
150
|
-
* @returns {Object} Mock HTTP client compatible with axios interface
|
|
151
|
-
*/
|
|
152
|
-
function createDryRunHttpClient(operationLog) {
|
|
153
|
-
/**
|
|
154
|
-
* Mock HTTP request handler
|
|
155
|
-
* @param {Object} config - Request configuration
|
|
156
|
-
* @returns {Promise<Object>} Mock response
|
|
157
|
-
*/
|
|
158
|
-
const mockRequest = async (config) => {
|
|
159
|
-
// Build full URL
|
|
160
|
-
let fullUrl = config.url;
|
|
161
|
-
if (config.baseURL && !config.url.startsWith('http')) {
|
|
162
|
-
fullUrl = `${config.baseURL}${config.url.startsWith('/') ? '' : '/'}${config.url}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Log the request that WOULD have been made
|
|
166
|
-
const logEntry = {
|
|
167
|
-
operation: 'HTTP_REQUEST',
|
|
168
|
-
method: (config.method || 'GET').toUpperCase(),
|
|
169
|
-
url: fullUrl,
|
|
170
|
-
baseURL: config.baseURL,
|
|
171
|
-
path: config.url,
|
|
172
|
-
service: detectService(config.baseURL || fullUrl),
|
|
173
|
-
headers: sanitizeHeaders(config.headers),
|
|
174
|
-
timestamp: new Date().toISOString(),
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Include request data for write operations
|
|
178
|
-
if (config.data && ['POST', 'PUT', 'PATCH'].includes(logEntry.method)) {
|
|
179
|
-
logEntry.data = sanitizeData(config.data);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Include query params
|
|
183
|
-
if (config.params) {
|
|
184
|
-
logEntry.params = sanitizeData(config.params);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
operationLog.push(logEntry);
|
|
188
|
-
|
|
189
|
-
// Return mock response
|
|
190
|
-
return {
|
|
191
|
-
status: 200,
|
|
192
|
-
statusText: 'OK (Dry-Run)',
|
|
193
|
-
data: {
|
|
194
|
-
_dryRun: true,
|
|
195
|
-
_message: 'This is a dry-run mock response',
|
|
196
|
-
_wouldHaveExecuted: `${logEntry.method} ${fullUrl}`,
|
|
197
|
-
_service: logEntry.service,
|
|
198
|
-
},
|
|
199
|
-
headers: {
|
|
200
|
-
'content-type': 'application/json',
|
|
201
|
-
'x-dry-run': 'true',
|
|
202
|
-
},
|
|
203
|
-
config,
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Return axios-compatible interface
|
|
208
|
-
return {
|
|
209
|
-
request: mockRequest,
|
|
210
|
-
get: (url, config = {}) => mockRequest({ ...config, method: 'GET', url }),
|
|
211
|
-
post: (url, data, config = {}) => mockRequest({ ...config, method: 'POST', url, data }),
|
|
212
|
-
put: (url, data, config = {}) => mockRequest({ ...config, method: 'PUT', url, data }),
|
|
213
|
-
patch: (url, data, config = {}) =>
|
|
214
|
-
mockRequest({ ...config, method: 'PATCH', url, data }),
|
|
215
|
-
delete: (url, config = {}) => mockRequest({ ...config, method: 'DELETE', url }),
|
|
216
|
-
head: (url, config = {}) => mockRequest({ ...config, method: 'HEAD', url }),
|
|
217
|
-
options: (url, config = {}) => mockRequest({ ...config, method: 'OPTIONS', url }),
|
|
218
|
-
|
|
219
|
-
// Axios-specific properties
|
|
220
|
-
defaults: {
|
|
221
|
-
headers: {
|
|
222
|
-
common: {},
|
|
223
|
-
get: {},
|
|
224
|
-
post: {},
|
|
225
|
-
put: {},
|
|
226
|
-
patch: {},
|
|
227
|
-
delete: {},
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
// Interceptors (no-op in dry-run)
|
|
232
|
-
interceptors: {
|
|
233
|
-
request: { use: () => {}, eject: () => {} },
|
|
234
|
-
response: { use: () => {}, eject: () => {} },
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Inject dry-run HTTP client into an integration instance
|
|
241
|
-
*
|
|
242
|
-
* @param {Object} integrationInstance - Integration instance from integrationFactory
|
|
243
|
-
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
244
|
-
*/
|
|
245
|
-
function injectDryRunHttpClient(integrationInstance, dryRunHttpClient) {
|
|
246
|
-
if (!integrationInstance) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Inject into primary API module
|
|
251
|
-
if (integrationInstance.primary?.api) {
|
|
252
|
-
injectIntoApiModule(integrationInstance.primary.api, dryRunHttpClient);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Inject into target API module
|
|
256
|
-
if (integrationInstance.target?.api) {
|
|
257
|
-
injectIntoApiModule(integrationInstance.target.api, dryRunHttpClient);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Inject dry-run HTTP client into an API module
|
|
263
|
-
* @param {Object} apiModule - API module instance
|
|
264
|
-
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
265
|
-
*/
|
|
266
|
-
function injectIntoApiModule(apiModule, dryRunHttpClient) {
|
|
267
|
-
// Common property names for HTTP clients in API modules
|
|
268
|
-
const httpClientProps = [
|
|
269
|
-
'_httpClient',
|
|
270
|
-
'httpClient',
|
|
271
|
-
'client',
|
|
272
|
-
'axios',
|
|
273
|
-
'request',
|
|
274
|
-
'api',
|
|
275
|
-
'http',
|
|
276
|
-
];
|
|
277
|
-
|
|
278
|
-
for (const prop of httpClientProps) {
|
|
279
|
-
if (apiModule[prop] && typeof apiModule[prop] === 'object') {
|
|
280
|
-
apiModule[prop] = dryRunHttpClient;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Also check if the API module itself has request methods
|
|
285
|
-
if (typeof apiModule.request === 'function') {
|
|
286
|
-
Object.assign(apiModule, dryRunHttpClient);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
module.exports = {
|
|
291
|
-
createDryRunHttpClient,
|
|
292
|
-
injectDryRunHttpClient,
|
|
293
|
-
sanitizeHeaders,
|
|
294
|
-
sanitizeData,
|
|
295
|
-
detectService,
|
|
296
|
-
};
|