@friggframework/admin-scripts 2.0.0--canary.517.179491e.0 → 2.0.0--canary.522.893db5d.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/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +37 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +8 -11
- package/src/adapters/aws-scheduler-adapter.js +18 -7
- package/src/adapters/local-scheduler-adapter.js +6 -1
- 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/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/infrastructure/script-executor-handler.js +23 -2
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/admin-scripts",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.522.893db5d.0",
|
|
5
5
|
"description": "Admin Script Runner for Frigg - Execute maintenance and operational scripts in hosted environments",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@aws-sdk/client-scheduler": "^3.588.0",
|
|
8
|
-
"@friggframework/core": "2.0.0--canary.
|
|
8
|
+
"@friggframework/core": "2.0.0--canary.522.893db5d.0",
|
|
9
9
|
"express": "^4.18.2",
|
|
10
10
|
"serverless-http": "^3.2.0"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
14
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.
|
|
13
|
+
"@friggframework/eslint-config": "2.0.0--canary.522.893db5d.0",
|
|
14
|
+
"@friggframework/prettier-config": "2.0.0--canary.522.893db5d.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.522.893db5d.0",
|
|
16
16
|
"eslint": "^8.22.0",
|
|
17
17
|
"jest": "^29.7.0",
|
|
18
18
|
"prettier": "^2.7.1",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"maintenance",
|
|
44
44
|
"operations"
|
|
45
45
|
],
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "893db5dc366945920987fecc1e8ab53754f76f63"
|
|
47
47
|
}
|
|
@@ -206,6 +206,43 @@ describe('AWSSchedulerAdapter', () => {
|
|
|
206
206
|
const command = mockSend.mock.calls[0][0];
|
|
207
207
|
expect(command.params.FlexibleTimeWindow).toEqual({ Mode: 'OFF' });
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
it('should fall back to UpdateScheduleCommand on ConflictException', async () => {
|
|
211
|
+
const conflictError = new Error('Schedule already exists');
|
|
212
|
+
conflictError.name = 'ConflictException';
|
|
213
|
+
|
|
214
|
+
mockSend
|
|
215
|
+
.mockRejectedValueOnce(conflictError)
|
|
216
|
+
.mockResolvedValueOnce({
|
|
217
|
+
ScheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await adapter.createSchedule({
|
|
221
|
+
scriptName: 'test-script',
|
|
222
|
+
cronExpression: 'cron(0 0 * * ? *)',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual({
|
|
226
|
+
scheduleArn: 'arn:aws:scheduler:us-east-1:123456789012:schedule/frigg-admin-scripts/frigg-script-test-script',
|
|
227
|
+
scheduleName: 'frigg-script-test-script',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(mockSend).toHaveBeenCalledTimes(2);
|
|
231
|
+
expect(mockSend.mock.calls[0][0]._type).toBe('CreateScheduleCommand');
|
|
232
|
+
expect(mockSend.mock.calls[1][0]._type).toBe('UpdateScheduleCommand');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should rethrow non-conflict errors', async () => {
|
|
236
|
+
const otherError = new Error('Access denied');
|
|
237
|
+
otherError.name = 'AccessDeniedException';
|
|
238
|
+
|
|
239
|
+
mockSend.mockRejectedValue(otherError);
|
|
240
|
+
|
|
241
|
+
await expect(adapter.createSchedule({
|
|
242
|
+
scriptName: 'test-script',
|
|
243
|
+
cronExpression: 'cron(0 0 * * ? *)',
|
|
244
|
+
})).rejects.toThrow('Access denied');
|
|
245
|
+
});
|
|
209
246
|
});
|
|
210
247
|
|
|
211
248
|
describe('deleteSchedule()', () => {
|
|
@@ -210,12 +210,12 @@ describe('LocalSchedulerAdapter', () => {
|
|
|
210
210
|
const schedules = await adapter.listSchedules();
|
|
211
211
|
|
|
212
212
|
expect(schedules).toHaveLength(3);
|
|
213
|
-
expect(schedules.map((s) => s.
|
|
214
|
-
expect(schedules.map((s) => s.
|
|
215
|
-
expect(schedules.map((s) => s.
|
|
213
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-1');
|
|
214
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-2');
|
|
215
|
+
expect(schedules.map((s) => s.Name)).toContain('frigg-script-script-3');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('should include all schedule properties', async () => {
|
|
218
|
+
it('should include all schedule properties in normalized format', async () => {
|
|
219
219
|
await adapter.createSchedule({
|
|
220
220
|
scriptName: 'test-script',
|
|
221
221
|
cronExpression: '0 0 * * *',
|
|
@@ -226,14 +226,11 @@ describe('LocalSchedulerAdapter', () => {
|
|
|
226
226
|
const schedules = await adapter.listSchedules();
|
|
227
227
|
|
|
228
228
|
expect(schedules[0]).toMatchObject({
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
enabled: true,
|
|
229
|
+
Name: 'frigg-script-test-script',
|
|
230
|
+
State: 'ENABLED',
|
|
231
|
+
ScheduleExpression: '0 0 * * *',
|
|
232
|
+
ScheduleExpressionTimezone: 'America/New_York',
|
|
234
233
|
});
|
|
235
|
-
expect(schedules[0]).toHaveProperty('createdAt');
|
|
236
|
-
expect(schedules[0]).toHaveProperty('updatedAt');
|
|
237
234
|
});
|
|
238
235
|
});
|
|
239
236
|
|
|
@@ -60,7 +60,7 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
|
60
60
|
const client = this.getSchedulerClient();
|
|
61
61
|
const scheduleName = `frigg-script-${scriptName}`;
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const scheduleParams = {
|
|
64
64
|
Name: scheduleName,
|
|
65
65
|
GroupName: this.scheduleGroupName,
|
|
66
66
|
ScheduleExpression: cronExpression,
|
|
@@ -76,13 +76,24 @@ class AWSSchedulerAdapter extends SchedulerAdapter {
|
|
|
76
76
|
}),
|
|
77
77
|
},
|
|
78
78
|
State: 'ENABLED',
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const response = await client.send(command);
|
|
82
|
-
return {
|
|
83
|
-
scheduleArn: response.ScheduleArn,
|
|
84
|
-
scheduleName: scheduleName,
|
|
85
79
|
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await client.send(new CreateScheduleCommand(scheduleParams));
|
|
83
|
+
return {
|
|
84
|
+
scheduleArn: response.ScheduleArn,
|
|
85
|
+
scheduleName: scheduleName,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.name === 'ConflictException') {
|
|
89
|
+
const response = await client.send(new UpdateScheduleCommand(scheduleParams));
|
|
90
|
+
return {
|
|
91
|
+
scheduleArn: response.ScheduleArn,
|
|
92
|
+
scheduleName: scheduleName,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
async deleteSchedule(scriptName) {
|
|
@@ -57,7 +57,12 @@ class LocalSchedulerAdapter extends SchedulerAdapter {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async listSchedules() {
|
|
60
|
-
return Array.from(this.schedules.values())
|
|
60
|
+
return Array.from(this.schedules.values()).map((schedule) => ({
|
|
61
|
+
Name: `frigg-script-${schedule.scriptName}`,
|
|
62
|
+
State: schedule.enabled ? 'ENABLED' : 'DISABLED',
|
|
63
|
+
ScheduleExpression: schedule.cronExpression,
|
|
64
|
+
ScheduleExpressionTimezone: schedule.timezone,
|
|
65
|
+
}));
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
async getSchedule(scriptName) {
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createDryRunHttpClient,
|
|
3
|
+
injectDryRunHttpClient,
|
|
4
|
+
sanitizeHeaders,
|
|
5
|
+
sanitizeData,
|
|
6
|
+
detectService,
|
|
7
|
+
} = require('../dry-run-http-interceptor');
|
|
8
|
+
|
|
9
|
+
describe('Dry-Run HTTP Interceptor', () => {
|
|
10
|
+
describe('sanitizeHeaders', () => {
|
|
11
|
+
test('should redact authorization headers', () => {
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Authorization: 'Bearer secret-token',
|
|
15
|
+
'X-API-Key': 'api-key-123',
|
|
16
|
+
'User-Agent': 'frigg/1.0',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const sanitized = sanitizeHeaders(headers);
|
|
20
|
+
|
|
21
|
+
expect(sanitized['Content-Type']).toBe('application/json');
|
|
22
|
+
expect(sanitized['User-Agent']).toBe('frigg/1.0');
|
|
23
|
+
expect(sanitized.Authorization).toBe('[REDACTED]');
|
|
24
|
+
expect(sanitized['X-API-Key']).toBe('[REDACTED]');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should handle case variations', () => {
|
|
28
|
+
const headers = {
|
|
29
|
+
authorization: 'Bearer token',
|
|
30
|
+
Authorization: 'Bearer token',
|
|
31
|
+
'x-api-key': 'key1',
|
|
32
|
+
'X-API-Key': 'key2',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const sanitized = sanitizeHeaders(headers);
|
|
36
|
+
|
|
37
|
+
expect(sanitized.authorization).toBe('[REDACTED]');
|
|
38
|
+
expect(sanitized.Authorization).toBe('[REDACTED]');
|
|
39
|
+
expect(sanitized['x-api-key']).toBe('[REDACTED]');
|
|
40
|
+
expect(sanitized['X-API-Key']).toBe('[REDACTED]');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should handle null/undefined', () => {
|
|
44
|
+
expect(sanitizeHeaders(null)).toEqual({});
|
|
45
|
+
expect(sanitizeHeaders(undefined)).toEqual({});
|
|
46
|
+
expect(sanitizeHeaders({})).toEqual({});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('detectService', () => {
|
|
51
|
+
test('should detect CRM services', () => {
|
|
52
|
+
expect(detectService('https://api.hubapi.com')).toBe('HubSpot');
|
|
53
|
+
expect(detectService('https://login.salesforce.com')).toBe('Salesforce');
|
|
54
|
+
expect(detectService('https://api.pipedrive.com')).toBe('Pipedrive');
|
|
55
|
+
expect(detectService('https://api.attio.com')).toBe('Attio');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should detect communication services', () => {
|
|
59
|
+
expect(detectService('https://slack.com/api')).toBe('Slack');
|
|
60
|
+
expect(detectService('https://discord.com/api')).toBe('Discord');
|
|
61
|
+
expect(detectService('https://graph.teams.microsoft.com')).toBe('Microsoft Teams');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should detect project management tools', () => {
|
|
65
|
+
expect(detectService('https://app.asana.com/api')).toBe('Asana');
|
|
66
|
+
expect(detectService('https://api.monday.com')).toBe('Monday.com');
|
|
67
|
+
expect(detectService('https://api.trello.com')).toBe('Trello');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should return unknown for unrecognized services', () => {
|
|
71
|
+
expect(detectService('https://example.com/api')).toBe('unknown');
|
|
72
|
+
expect(detectService(null)).toBe('unknown');
|
|
73
|
+
expect(detectService(undefined)).toBe('unknown');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should be case insensitive', () => {
|
|
77
|
+
expect(detectService('HTTPS://API.HUBSPOT.COM')).toBe('HubSpot');
|
|
78
|
+
expect(detectService('https://API.SLACK.COM')).toBe('Slack');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('sanitizeData', () => {
|
|
83
|
+
test('should redact sensitive fields', () => {
|
|
84
|
+
const data = {
|
|
85
|
+
name: 'Test User',
|
|
86
|
+
email: 'test@example.com',
|
|
87
|
+
password: 'secret123',
|
|
88
|
+
apiToken: 'token-abc',
|
|
89
|
+
authKey: 'key-xyz',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sanitized = sanitizeData(data);
|
|
93
|
+
|
|
94
|
+
expect(sanitized.name).toBe('Test User');
|
|
95
|
+
expect(sanitized.email).toBe('test@example.com');
|
|
96
|
+
expect(sanitized.password).toBe('[REDACTED]');
|
|
97
|
+
expect(sanitized.apiToken).toBe('[REDACTED]');
|
|
98
|
+
expect(sanitized.authKey).toBe('[REDACTED]');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should handle nested objects', () => {
|
|
102
|
+
const data = {
|
|
103
|
+
user: {
|
|
104
|
+
name: 'Test',
|
|
105
|
+
credentials: {
|
|
106
|
+
password: 'secret',
|
|
107
|
+
token: 'abc123',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const sanitized = sanitizeData(data);
|
|
113
|
+
|
|
114
|
+
expect(sanitized.user.name).toBe('Test');
|
|
115
|
+
expect(sanitized.user.credentials.password).toBe('[REDACTED]');
|
|
116
|
+
expect(sanitized.user.credentials.token).toBe('[REDACTED]');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should handle arrays', () => {
|
|
120
|
+
const data = [
|
|
121
|
+
{ id: '1', password: 'secret1' },
|
|
122
|
+
{ id: '2', apiKey: 'key2' },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const sanitized = sanitizeData(data);
|
|
126
|
+
|
|
127
|
+
expect(sanitized[0].id).toBe('1');
|
|
128
|
+
expect(sanitized[0].password).toBe('[REDACTED]');
|
|
129
|
+
expect(sanitized[1].apiKey).toBe('[REDACTED]');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should preserve primitives', () => {
|
|
133
|
+
expect(sanitizeData('string')).toBe('string');
|
|
134
|
+
expect(sanitizeData(123)).toBe(123);
|
|
135
|
+
expect(sanitizeData(true)).toBe(true);
|
|
136
|
+
expect(sanitizeData(null)).toBe(null);
|
|
137
|
+
expect(sanitizeData(undefined)).toBe(undefined);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('createDryRunHttpClient', () => {
|
|
142
|
+
let operationLog;
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
operationLog = [];
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should log GET requests', async () => {
|
|
149
|
+
const client = createDryRunHttpClient(operationLog);
|
|
150
|
+
|
|
151
|
+
const response = await client.get('/contacts', {
|
|
152
|
+
baseURL: 'https://api.hubapi.com',
|
|
153
|
+
headers: { Authorization: 'Bearer token' },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(operationLog).toHaveLength(1);
|
|
157
|
+
expect(operationLog[0]).toMatchObject({
|
|
158
|
+
operation: 'HTTP_REQUEST',
|
|
159
|
+
method: 'GET',
|
|
160
|
+
url: 'https://api.hubapi.com/contacts',
|
|
161
|
+
service: 'HubSpot',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(operationLog[0].headers.Authorization).toBe('[REDACTED]');
|
|
165
|
+
expect(response.data._dryRun).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should log POST requests with data', async () => {
|
|
169
|
+
const client = createDryRunHttpClient(operationLog);
|
|
170
|
+
|
|
171
|
+
const postData = {
|
|
172
|
+
name: 'John Doe',
|
|
173
|
+
email: 'john@example.com',
|
|
174
|
+
password: 'secret123',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
await client.post('/users', postData, {
|
|
178
|
+
baseURL: 'https://api.example.com',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(operationLog).toHaveLength(1);
|
|
182
|
+
expect(operationLog[0].method).toBe('POST');
|
|
183
|
+
expect(operationLog[0].data.name).toBe('John Doe');
|
|
184
|
+
expect(operationLog[0].data.email).toBe('john@example.com');
|
|
185
|
+
expect(operationLog[0].data.password).toBe('[REDACTED]');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should log PUT requests', async () => {
|
|
189
|
+
const client = createDryRunHttpClient(operationLog);
|
|
190
|
+
|
|
191
|
+
await client.put('/users/123', { status: 'active' }, {
|
|
192
|
+
baseURL: 'https://api.example.com',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(operationLog).toHaveLength(1);
|
|
196
|
+
expect(operationLog[0].method).toBe('PUT');
|
|
197
|
+
expect(operationLog[0].data.status).toBe('active');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should log PATCH requests', async () => {
|
|
201
|
+
const client = createDryRunHttpClient(operationLog);
|
|
202
|
+
|
|
203
|
+
await client.patch('/users/123', { name: 'Updated' });
|
|
204
|
+
|
|
205
|
+
expect(operationLog).toHaveLength(1);
|
|
206
|
+
expect(operationLog[0].method).toBe('PATCH');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('should log DELETE requests', async () => {
|
|
210
|
+
const client = createDryRunHttpClient(operationLog);
|
|
211
|
+
|
|
212
|
+
await client.delete('/users/123', {
|
|
213
|
+
baseURL: 'https://api.example.com',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(operationLog).toHaveLength(1);
|
|
217
|
+
expect(operationLog[0].method).toBe('DELETE');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should return mock response', async () => {
|
|
221
|
+
const client = createDryRunHttpClient(operationLog);
|
|
222
|
+
|
|
223
|
+
const response = await client.get('/test');
|
|
224
|
+
|
|
225
|
+
expect(response.status).toBe(200);
|
|
226
|
+
expect(response.statusText).toContain('Dry-Run');
|
|
227
|
+
expect(response.data._dryRun).toBe(true);
|
|
228
|
+
expect(response.headers['x-dry-run']).toBe('true');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should include query params in log', async () => {
|
|
232
|
+
const client = createDryRunHttpClient(operationLog);
|
|
233
|
+
|
|
234
|
+
await client.get('/search', {
|
|
235
|
+
baseURL: 'https://api.example.com',
|
|
236
|
+
params: { q: 'test', limit: 10 },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(operationLog[0].params).toEqual({ q: 'test', limit: 10 });
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('injectDryRunHttpClient', () => {
|
|
244
|
+
let operationLog;
|
|
245
|
+
let dryRunClient;
|
|
246
|
+
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
operationLog = [];
|
|
249
|
+
dryRunClient = createDryRunHttpClient(operationLog);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('should inject into primary API module', () => {
|
|
253
|
+
const integrationInstance = {
|
|
254
|
+
primary: {
|
|
255
|
+
api: {
|
|
256
|
+
_httpClient: { get: jest.fn() },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
injectDryRunHttpClient(integrationInstance, dryRunClient);
|
|
262
|
+
|
|
263
|
+
expect(integrationInstance.primary.api._httpClient).toBe(dryRunClient);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('should inject into target API module', () => {
|
|
267
|
+
const integrationInstance = {
|
|
268
|
+
target: {
|
|
269
|
+
api: {
|
|
270
|
+
_httpClient: { get: jest.fn() },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
injectDryRunHttpClient(integrationInstance, dryRunClient);
|
|
276
|
+
|
|
277
|
+
expect(integrationInstance.target.api._httpClient).toBe(dryRunClient);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('should inject into both primary and target', () => {
|
|
281
|
+
const integrationInstance = {
|
|
282
|
+
primary: {
|
|
283
|
+
api: { _httpClient: { get: jest.fn() } },
|
|
284
|
+
},
|
|
285
|
+
target: {
|
|
286
|
+
api: { _httpClient: { get: jest.fn() } },
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
injectDryRunHttpClient(integrationInstance, dryRunClient);
|
|
291
|
+
|
|
292
|
+
expect(integrationInstance.primary.api._httpClient).toBe(dryRunClient);
|
|
293
|
+
expect(integrationInstance.target.api._httpClient).toBe(dryRunClient);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('should handle missing api modules gracefully', () => {
|
|
297
|
+
const integrationInstance = {
|
|
298
|
+
primary: {},
|
|
299
|
+
target: null,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
expect(() => {
|
|
303
|
+
injectDryRunHttpClient(integrationInstance, dryRunClient);
|
|
304
|
+
}).not.toThrow();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('should handle null integration instance', () => {
|
|
308
|
+
expect(() => {
|
|
309
|
+
injectDryRunHttpClient(null, dryRunClient);
|
|
310
|
+
}).not.toThrow();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|