@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.
- package/README.md +272 -0
- package/index.js +22 -19
- package/package.json +6 -6
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +106 -45
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +24 -10
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +30 -9
- package/src/adapters/__tests__/scheduler-adapter.test.js +6 -2
- package/src/adapters/aws-scheduler-adapter.js +55 -28
- package/src/adapters/local-scheduler-adapter.js +2 -2
- package/src/adapters/scheduler-adapter-factory.js +3 -1
- package/src/adapters/scheduler-adapter.js +9 -3
- package/src/application/__tests__/admin-frigg-commands.test.js +73 -30
- package/src/application/__tests__/admin-script-base.test.js +23 -6
- package/src/application/__tests__/script-factory.test.js +30 -6
- package/src/application/__tests__/script-runner.test.js +113 -24
- package/src/application/__tests__/validate-script-input.test.js +54 -15
- package/src/application/admin-frigg-commands.js +21 -9
- package/src/application/admin-script-base.js +3 -2
- package/src/application/script-factory.js +3 -1
- package/src/application/script-runner.js +90 -48
- package/src/application/use-cases/__tests__/delete-schedule-use-case.test.js +16 -7
- package/src/application/use-cases/__tests__/get-effective-schedule-use-case.test.js +9 -4
- package/src/application/use-cases/__tests__/upsert-schedule-use-case.test.js +21 -10
- package/src/application/use-cases/delete-schedule-use-case.js +6 -4
- package/src/application/use-cases/get-effective-schedule-use-case.js +3 -1
- package/src/application/use-cases/index.js +3 -1
- package/src/application/use-cases/upsert-schedule-use-case.js +30 -11
- package/src/application/validate-script-input.js +10 -3
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +9 -3
- package/src/infrastructure/__tests__/admin-script-router.test.js +125 -52
- package/src/infrastructure/admin-auth-middleware.js +3 -1
- package/src/infrastructure/admin-script-router.js +129 -14
- package/src/infrastructure/bootstrap.js +82 -0
- package/src/infrastructure/script-executor-handler.js +119 -52
- 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
- package/src/builtins/__tests__/integration-health-check.test.js +0 -607
- package/src/builtins/__tests__/oauth-token-refresh.test.js +0 -354
- package/src/builtins/index.js +0 -28
- package/src/builtins/integration-health-check.js +0 -278
- package/src/builtins/oauth-token-refresh.js +0 -220
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { getScriptFactory } = require('../application/script-factory');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin Script Bootstrap
|
|
5
|
+
*
|
|
6
|
+
* Loads the host app's definition at Lambda runtime and registers its admin
|
|
7
|
+
* scripts (and the built-ins, when enabled) into the global ScriptFactory, so
|
|
8
|
+
* the router and SQS worker can resolve scripts by name. Also constructs the
|
|
9
|
+
* integrationFactory used by scripts that need hydrated integration instances.
|
|
10
|
+
*
|
|
11
|
+
* Runs once per process (memoized) and never throws — a missing/unloadable app
|
|
12
|
+
* definition is logged and leaves the factory empty rather than crashing the
|
|
13
|
+
* Lambda cold start.
|
|
14
|
+
*/
|
|
15
|
+
let bootstrapped = false;
|
|
16
|
+
let integrationFactory = null;
|
|
17
|
+
|
|
18
|
+
function registerScripts(factory, scriptClasses) {
|
|
19
|
+
for (const ScriptClass of scriptClasses || []) {
|
|
20
|
+
const name = ScriptClass?.Definition?.name;
|
|
21
|
+
// Guard against re-registration (register() throws on name collision)
|
|
22
|
+
if (name && !factory.has(name)) {
|
|
23
|
+
factory.register(ScriptClass);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an integration factory backed by the framework's runtime hydration.
|
|
30
|
+
* Scripts call `context.instantiate(integrationId)` which delegates here.
|
|
31
|
+
* The require is lazy so the admin-scripts package has no load-time coupling
|
|
32
|
+
* to core's backend utilities (and unit tests never hit this path).
|
|
33
|
+
*/
|
|
34
|
+
function createIntegrationFactory() {
|
|
35
|
+
return {
|
|
36
|
+
async getInstanceFromIntegrationId({ integrationId }) {
|
|
37
|
+
const {
|
|
38
|
+
loadIntegrationForWebhook,
|
|
39
|
+
} = require('@friggframework/core/handlers/backend-utils');
|
|
40
|
+
return loadIntegrationForWebhook(integrationId);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {{ integrationFactory: object }}
|
|
47
|
+
*/
|
|
48
|
+
function bootstrapAdminScripts() {
|
|
49
|
+
if (bootstrapped) {
|
|
50
|
+
return { integrationFactory };
|
|
51
|
+
}
|
|
52
|
+
bootstrapped = true;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const {
|
|
56
|
+
loadAppDefinition,
|
|
57
|
+
} = require('@friggframework/core/handlers/app-definition-loader');
|
|
58
|
+
const { adminScripts = [] } = loadAppDefinition();
|
|
59
|
+
|
|
60
|
+
const factory = getScriptFactory();
|
|
61
|
+
registerScripts(factory, adminScripts);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(
|
|
64
|
+
'[admin-scripts] bootstrap: could not load app definition:',
|
|
65
|
+
error.message
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
integrationFactory = createIntegrationFactory();
|
|
70
|
+
return { integrationFactory };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Test-only: reset memoized bootstrap state. */
|
|
74
|
+
function _resetBootstrapForTests() {
|
|
75
|
+
bootstrapped = false;
|
|
76
|
+
integrationFactory = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
bootstrapAdminScripts,
|
|
81
|
+
_resetBootstrapForTests,
|
|
82
|
+
};
|
|
@@ -1,69 +1,136 @@
|
|
|
1
1
|
const { createScriptRunner } = require('../application/script-runner');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
createAdminScriptCommands,
|
|
4
|
+
} = require('@friggframework/core/application/commands/admin-script-commands');
|
|
5
|
+
const { bootstrapAdminScripts } = require('./bootstrap');
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Processes script execution messages from AdminScriptQueue.
|
|
8
|
-
* Thin adapter: parses SQS messages and delegates to ScriptRunner.
|
|
9
|
-
* ScriptRunner handles execution tracking, error recording, and status updates.
|
|
8
|
+
* Run a single execution message through the ScriptRunner.
|
|
9
|
+
* @private
|
|
10
10
|
*/
|
|
11
|
-
async function
|
|
12
|
-
const
|
|
11
|
+
async function runMessage(message, integrationFactory) {
|
|
12
|
+
const { scriptName, executionId, trigger, params, parentExecutionId } =
|
|
13
|
+
message;
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (!scriptName) {
|
|
16
|
+
throw new Error('Invalid message: missing scriptName');
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
console.log(
|
|
20
|
+
`Processing script: ${scriptName}${
|
|
21
|
+
executionId ? `, executionId: ${executionId}` : ''
|
|
22
|
+
}`
|
|
23
|
+
);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const runner = createScriptRunner({ integrationFactory });
|
|
26
|
+
const result = await runner.execute(scriptName, params, {
|
|
27
|
+
trigger: trigger || 'QUEUE',
|
|
28
|
+
mode: 'async',
|
|
29
|
+
// executionId is optional — when absent, ScriptRunner creates the record.
|
|
30
|
+
// Scheduled direct invokes and queueScript continuations have no id yet.
|
|
31
|
+
...(executionId && { executionId }),
|
|
32
|
+
...(parentExecutionId && { parentExecutionId }),
|
|
33
|
+
});
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
return {
|
|
36
|
+
scriptName,
|
|
37
|
+
status: result.status,
|
|
38
|
+
executionId: result.executionId,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Mark an admin process FAILED when the worker itself blows up (parse error,
|
|
44
|
+
* runner construction), so the record doesn't stay stuck in a non-terminal
|
|
45
|
+
* state. No-op when there is no execution id.
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
async function markFailed(executionId, error) {
|
|
49
|
+
if (!executionId) return;
|
|
50
|
+
try {
|
|
51
|
+
const commands = createAdminScriptCommands();
|
|
52
|
+
await commands.completeAdminProcess(executionId, {
|
|
53
|
+
state: 'FAILED',
|
|
54
|
+
error: {
|
|
55
|
+
name: error.name,
|
|
56
|
+
message: error.message,
|
|
57
|
+
stack: error.stack,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} catch (updateError) {
|
|
61
|
+
console.error(
|
|
62
|
+
`Failed to update execution ${executionId} state:`,
|
|
63
|
+
updateError
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
35
67
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Admin Script Executor Lambda Handler
|
|
70
|
+
*
|
|
71
|
+
* Handles two invocation shapes:
|
|
72
|
+
* - SQS: `event.Records[]` — each record body is a JSON execution message
|
|
73
|
+
* (manual async executions and queueScript continuations).
|
|
74
|
+
* - EventBridge Scheduler direct invoke: the event itself is the message
|
|
75
|
+
* (`{ scriptName, trigger: 'SCHEDULED', params }`) with no `Records` wrapper.
|
|
76
|
+
*
|
|
77
|
+
* Thin adapter: parses the event and delegates to ScriptRunner, which owns
|
|
78
|
+
* execution tracking, error recording, and status updates.
|
|
79
|
+
*/
|
|
80
|
+
async function handler(event) {
|
|
81
|
+
const { integrationFactory } = bootstrapAdminScripts();
|
|
46
82
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
// EventBridge Scheduler invokes the Lambda directly (no Records wrapper)
|
|
84
|
+
if (!event.Records) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await runMessage(event, integrationFactory);
|
|
87
|
+
console.log(
|
|
88
|
+
`Script completed: ${result.scriptName}, status: ${result.status}`
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
statusCode: 200,
|
|
92
|
+
body: JSON.stringify({ processed: 1, results: [result] }),
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(
|
|
96
|
+
'Unexpected error processing scheduled invoke:',
|
|
97
|
+
error
|
|
98
|
+
);
|
|
99
|
+
await markFailed(event.executionId, error);
|
|
100
|
+
return {
|
|
101
|
+
statusCode: 200,
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
processed: 1,
|
|
104
|
+
results: [
|
|
105
|
+
{
|
|
106
|
+
scriptName: event.scriptName || 'unknown',
|
|
107
|
+
status: 'FAILED',
|
|
108
|
+
error: error.message,
|
|
58
109
|
},
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
64
115
|
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const record of event.Records) {
|
|
118
|
+
let message = {};
|
|
119
|
+
try {
|
|
120
|
+
message = JSON.parse(record.body);
|
|
121
|
+
const result = await runMessage(message, integrationFactory);
|
|
122
|
+
console.log(
|
|
123
|
+
`Script completed: ${result.scriptName}, status: ${result.status}`
|
|
124
|
+
);
|
|
125
|
+
results.push(result);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Only unexpected failures reach here (message parse errors, runner
|
|
128
|
+
// construction). Script execution errors are handled by ScriptRunner
|
|
129
|
+
// and returned as { status: 'FAILED' }.
|
|
130
|
+
console.error('Unexpected error processing record:', error);
|
|
131
|
+
await markFailed(message.executionId, error);
|
|
65
132
|
results.push({
|
|
66
|
-
scriptName: scriptName || 'unknown',
|
|
133
|
+
scriptName: message.scriptName || 'unknown',
|
|
67
134
|
status: 'FAILED',
|
|
68
135
|
error: error.message,
|
|
69
136
|
});
|
|
@@ -1,313 +0,0 @@
|
|
|
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
|
-
});
|