@curl-runner/cli 1.16.0 → 1.16.2
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 +2 -2
- package/src/ci-exit.test.ts +0 -216
- package/src/cli.ts +0 -1351
- package/src/commands/upgrade.ts +0 -262
- package/src/diff/baseline-manager.test.ts +0 -181
- package/src/diff/baseline-manager.ts +0 -266
- package/src/diff/diff-formatter.ts +0 -316
- package/src/diff/index.ts +0 -3
- package/src/diff/response-differ.test.ts +0 -330
- package/src/diff/response-differ.ts +0 -489
- package/src/executor/max-concurrency.test.ts +0 -139
- package/src/executor/profile-executor.test.ts +0 -132
- package/src/executor/profile-executor.ts +0 -167
- package/src/executor/request-executor.ts +0 -663
- package/src/parser/yaml.test.ts +0 -480
- package/src/parser/yaml.ts +0 -271
- package/src/snapshot/index.ts +0 -3
- package/src/snapshot/snapshot-differ.test.ts +0 -358
- package/src/snapshot/snapshot-differ.ts +0 -296
- package/src/snapshot/snapshot-formatter.ts +0 -170
- package/src/snapshot/snapshot-manager.test.ts +0 -204
- package/src/snapshot/snapshot-manager.ts +0 -342
- package/src/types/bun-yaml.d.ts +0 -11
- package/src/types/config.ts +0 -638
- package/src/utils/colors.ts +0 -30
- package/src/utils/condition-evaluator.test.ts +0 -415
- package/src/utils/condition-evaluator.ts +0 -327
- package/src/utils/curl-builder.test.ts +0 -165
- package/src/utils/curl-builder.ts +0 -209
- package/src/utils/installation-detector.test.ts +0 -52
- package/src/utils/installation-detector.ts +0 -123
- package/src/utils/logger.ts +0 -856
- package/src/utils/response-store.test.ts +0 -213
- package/src/utils/response-store.ts +0 -108
- package/src/utils/stats.test.ts +0 -161
- package/src/utils/stats.ts +0 -151
- package/src/utils/version-checker.ts +0 -158
- package/src/version.ts +0 -43
- package/src/watcher/file-watcher.test.ts +0 -186
- package/src/watcher/file-watcher.ts +0 -140
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConditionExpression,
|
|
3
|
-
ConditionOperator,
|
|
4
|
-
ResponseStoreContext,
|
|
5
|
-
WhenCondition,
|
|
6
|
-
} from '../types/config';
|
|
7
|
-
import { getValueByPath, valueToString } from './response-store';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Result of evaluating a condition.
|
|
11
|
-
*/
|
|
12
|
-
export interface ConditionResult {
|
|
13
|
-
shouldRun: boolean;
|
|
14
|
-
reason?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Parses a string shorthand condition into a ConditionExpression.
|
|
19
|
-
* Supports formats like:
|
|
20
|
-
* - "store.status == 200"
|
|
21
|
-
* - "store.userId exists"
|
|
22
|
-
* - "store.body.type contains user"
|
|
23
|
-
* - "store.version >= 2"
|
|
24
|
-
*
|
|
25
|
-
* @param condition - String condition to parse
|
|
26
|
-
* @returns Parsed ConditionExpression or null if invalid
|
|
27
|
-
*/
|
|
28
|
-
export function parseStringCondition(condition: string): ConditionExpression | null {
|
|
29
|
-
const trimmed = condition.trim();
|
|
30
|
-
|
|
31
|
-
// Check for exists/not-exists operators first (unary)
|
|
32
|
-
const existsMatch = trimmed.match(/^(.+?)\s+(exists|not-exists)$/i);
|
|
33
|
-
if (existsMatch) {
|
|
34
|
-
return {
|
|
35
|
-
left: existsMatch[1].trim(),
|
|
36
|
-
operator: existsMatch[2].toLowerCase() as ConditionOperator,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Binary operators pattern
|
|
41
|
-
const operatorPattern = /^(.+?)\s*(==|!=|>=|<=|>|<|contains|matches)\s*(.+)$/i;
|
|
42
|
-
const match = trimmed.match(operatorPattern);
|
|
43
|
-
|
|
44
|
-
if (!match) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const [, left, operator, right] = match;
|
|
49
|
-
const rightValue = parseRightValue(right.trim());
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
left: left.trim(),
|
|
53
|
-
operator: operator.toLowerCase() as ConditionOperator,
|
|
54
|
-
right: rightValue,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parses the right-hand value, converting to appropriate type.
|
|
60
|
-
*/
|
|
61
|
-
function parseRightValue(value: string): string | number | boolean {
|
|
62
|
-
// Boolean
|
|
63
|
-
if (value === 'true') {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (value === 'false') {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Number
|
|
71
|
-
const num = Number(value);
|
|
72
|
-
if (!Number.isNaN(num) && value !== '') {
|
|
73
|
-
return num;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// String - remove quotes if present
|
|
77
|
-
if (
|
|
78
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
80
|
-
) {
|
|
81
|
-
return value.slice(1, -1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Gets a value from the store context using a path.
|
|
89
|
-
* Handles "store.x" prefix by stripping it.
|
|
90
|
-
*/
|
|
91
|
-
function getStoreValue(path: string, context: ResponseStoreContext): unknown {
|
|
92
|
-
// Strip "store." prefix if present
|
|
93
|
-
const normalizedPath = path.startsWith('store.') ? path.slice(6) : path;
|
|
94
|
-
|
|
95
|
-
// First check if it's a direct key in context
|
|
96
|
-
if (normalizedPath in context) {
|
|
97
|
-
const value = context[normalizedPath];
|
|
98
|
-
// Try to parse JSON if it looks like an object/array
|
|
99
|
-
if (value.startsWith('{') || value.startsWith('[')) {
|
|
100
|
-
try {
|
|
101
|
-
return JSON.parse(value);
|
|
102
|
-
} catch {
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Try to parse as number
|
|
107
|
-
const num = Number(value);
|
|
108
|
-
if (!Number.isNaN(num) && value !== '') {
|
|
109
|
-
return num;
|
|
110
|
-
}
|
|
111
|
-
// Try to parse as boolean
|
|
112
|
-
if (value === 'true') {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
if (value === 'false') {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Handle nested paths like "body.data.id" where context has "body" as JSON string
|
|
122
|
-
const parts = normalizedPath.split('.');
|
|
123
|
-
const rootKey = parts[0];
|
|
124
|
-
|
|
125
|
-
if (rootKey in context) {
|
|
126
|
-
const rootValue = context[rootKey];
|
|
127
|
-
// Try to parse as JSON and navigate
|
|
128
|
-
try {
|
|
129
|
-
const parsed = JSON.parse(rootValue);
|
|
130
|
-
return getValueByPath(parsed, parts.slice(1).join('.'));
|
|
131
|
-
} catch {
|
|
132
|
-
// Not JSON, can't navigate further
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Compares two values based on case sensitivity setting.
|
|
142
|
-
*/
|
|
143
|
-
function compareStrings(a: string, b: string, caseSensitive: boolean): boolean {
|
|
144
|
-
if (caseSensitive) {
|
|
145
|
-
return a === b;
|
|
146
|
-
}
|
|
147
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Evaluates a single condition expression against the store context.
|
|
152
|
-
*/
|
|
153
|
-
export function evaluateExpression(
|
|
154
|
-
expr: ConditionExpression,
|
|
155
|
-
context: ResponseStoreContext,
|
|
156
|
-
): { passed: boolean; description: string } {
|
|
157
|
-
const leftValue = getStoreValue(expr.left, context);
|
|
158
|
-
const caseSensitive = expr.caseSensitive ?? false;
|
|
159
|
-
|
|
160
|
-
const formatValue = (v: unknown): string => {
|
|
161
|
-
if (v === undefined) {
|
|
162
|
-
return 'undefined';
|
|
163
|
-
}
|
|
164
|
-
if (v === null) {
|
|
165
|
-
return 'null';
|
|
166
|
-
}
|
|
167
|
-
if (typeof v === 'string') {
|
|
168
|
-
return `"${v}"`;
|
|
169
|
-
}
|
|
170
|
-
return String(v);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const description = `${expr.left} ${expr.operator}${expr.right !== undefined ? ` ${formatValue(expr.right)}` : ''}`;
|
|
174
|
-
|
|
175
|
-
switch (expr.operator) {
|
|
176
|
-
case 'exists': {
|
|
177
|
-
const passed = leftValue !== undefined && leftValue !== null && leftValue !== '';
|
|
178
|
-
return { passed, description };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
case 'not-exists': {
|
|
182
|
-
const passed = leftValue === undefined || leftValue === null || leftValue === '';
|
|
183
|
-
return { passed, description };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
case '==': {
|
|
187
|
-
let passed: boolean;
|
|
188
|
-
if (typeof leftValue === 'string' && typeof expr.right === 'string') {
|
|
189
|
-
passed = compareStrings(leftValue, expr.right, caseSensitive);
|
|
190
|
-
} else {
|
|
191
|
-
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for type coercion
|
|
192
|
-
passed = leftValue == expr.right;
|
|
193
|
-
}
|
|
194
|
-
return { passed, description };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
case '!=': {
|
|
198
|
-
let passed: boolean;
|
|
199
|
-
if (typeof leftValue === 'string' && typeof expr.right === 'string') {
|
|
200
|
-
passed = !compareStrings(leftValue, expr.right, caseSensitive);
|
|
201
|
-
} else {
|
|
202
|
-
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for type coercion
|
|
203
|
-
passed = leftValue != expr.right;
|
|
204
|
-
}
|
|
205
|
-
return { passed, description };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
case '>': {
|
|
209
|
-
const passed = Number(leftValue) > Number(expr.right);
|
|
210
|
-
return { passed, description };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
case '<': {
|
|
214
|
-
const passed = Number(leftValue) < Number(expr.right);
|
|
215
|
-
return { passed, description };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
case '>=': {
|
|
219
|
-
const passed = Number(leftValue) >= Number(expr.right);
|
|
220
|
-
return { passed, description };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
case '<=': {
|
|
224
|
-
const passed = Number(leftValue) <= Number(expr.right);
|
|
225
|
-
return { passed, description };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
case 'contains': {
|
|
229
|
-
const leftStr = valueToString(leftValue);
|
|
230
|
-
const rightStr = String(expr.right ?? '');
|
|
231
|
-
const passed = caseSensitive
|
|
232
|
-
? leftStr.includes(rightStr)
|
|
233
|
-
: leftStr.toLowerCase().includes(rightStr.toLowerCase());
|
|
234
|
-
return { passed, description };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
case 'matches': {
|
|
238
|
-
const leftStr = valueToString(leftValue);
|
|
239
|
-
const pattern = String(expr.right ?? '');
|
|
240
|
-
try {
|
|
241
|
-
const flags = caseSensitive ? '' : 'i';
|
|
242
|
-
const regex = new RegExp(pattern, flags);
|
|
243
|
-
const passed = regex.test(leftStr);
|
|
244
|
-
return { passed, description };
|
|
245
|
-
} catch {
|
|
246
|
-
// Invalid regex pattern
|
|
247
|
-
return { passed: false, description: `${description} (invalid regex)` };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
default:
|
|
252
|
-
return { passed: false, description: `unknown operator: ${expr.operator}` };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Evaluates a WhenCondition against the store context.
|
|
258
|
-
*
|
|
259
|
-
* @param condition - The condition to evaluate
|
|
260
|
-
* @param context - The store context with values from previous requests
|
|
261
|
-
* @returns Result indicating whether the request should run
|
|
262
|
-
*/
|
|
263
|
-
export function evaluateCondition(
|
|
264
|
-
condition: WhenCondition | string,
|
|
265
|
-
context: ResponseStoreContext,
|
|
266
|
-
): ConditionResult {
|
|
267
|
-
// Handle string shorthand
|
|
268
|
-
if (typeof condition === 'string') {
|
|
269
|
-
const parsed = parseStringCondition(condition);
|
|
270
|
-
if (!parsed) {
|
|
271
|
-
return { shouldRun: false, reason: `invalid condition syntax: "${condition}"` };
|
|
272
|
-
}
|
|
273
|
-
const result = evaluateExpression(parsed, context);
|
|
274
|
-
return {
|
|
275
|
-
shouldRun: result.passed,
|
|
276
|
-
reason: result.passed ? undefined : `condition not met: ${result.description}`,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Handle compound "all" condition (AND logic with short-circuit)
|
|
281
|
-
if (condition.all && condition.all.length > 0) {
|
|
282
|
-
for (const expr of condition.all) {
|
|
283
|
-
const result = evaluateExpression(expr, context);
|
|
284
|
-
if (!result.passed) {
|
|
285
|
-
return {
|
|
286
|
-
shouldRun: false,
|
|
287
|
-
reason: `condition not met: ${result.description}`,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return { shouldRun: true };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Handle compound "any" condition (OR logic with short-circuit)
|
|
295
|
-
if (condition.any && condition.any.length > 0) {
|
|
296
|
-
const descriptions: string[] = [];
|
|
297
|
-
for (const expr of condition.any) {
|
|
298
|
-
const result = evaluateExpression(expr, context);
|
|
299
|
-
if (result.passed) {
|
|
300
|
-
return { shouldRun: true };
|
|
301
|
-
}
|
|
302
|
-
descriptions.push(result.description);
|
|
303
|
-
}
|
|
304
|
-
return {
|
|
305
|
-
shouldRun: false,
|
|
306
|
-
reason: `no conditions met: ${descriptions.join(', ')}`,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Handle single condition (inline left/operator/right)
|
|
311
|
-
if (condition.left && condition.operator) {
|
|
312
|
-
const expr: ConditionExpression = {
|
|
313
|
-
left: condition.left,
|
|
314
|
-
operator: condition.operator,
|
|
315
|
-
right: condition.right,
|
|
316
|
-
caseSensitive: condition.caseSensitive,
|
|
317
|
-
};
|
|
318
|
-
const result = evaluateExpression(expr, context);
|
|
319
|
-
return {
|
|
320
|
-
shouldRun: result.passed,
|
|
321
|
-
reason: result.passed ? undefined : `condition not met: ${result.description}`,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// No valid condition found - default to run
|
|
326
|
-
return { shouldRun: true };
|
|
327
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { CurlBuilder } from './curl-builder';
|
|
3
|
-
|
|
4
|
-
describe('CurlBuilder', () => {
|
|
5
|
-
describe('buildCommand', () => {
|
|
6
|
-
test('should build basic GET request', () => {
|
|
7
|
-
const command = CurlBuilder.buildCommand({
|
|
8
|
-
url: 'https://example.com/api',
|
|
9
|
-
method: 'GET',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
expect(command).toContain('curl');
|
|
13
|
-
expect(command).toContain('-X GET');
|
|
14
|
-
expect(command).toContain('"https://example.com/api"');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('should build POST request with JSON body', () => {
|
|
18
|
-
const command = CurlBuilder.buildCommand({
|
|
19
|
-
url: 'https://example.com/api',
|
|
20
|
-
method: 'POST',
|
|
21
|
-
body: { name: 'test' },
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(command).toContain('-X POST');
|
|
25
|
-
expect(command).toContain('-d \'{"name":"test"}\'');
|
|
26
|
-
expect(command).toContain('Content-Type: application/json');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('should build POST request with form data', () => {
|
|
30
|
-
const command = CurlBuilder.buildCommand({
|
|
31
|
-
url: 'https://example.com/upload',
|
|
32
|
-
method: 'POST',
|
|
33
|
-
formData: {
|
|
34
|
-
username: 'john',
|
|
35
|
-
age: 30,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(command).toContain('-X POST');
|
|
40
|
-
expect(command).toContain("-F 'username=john'");
|
|
41
|
-
expect(command).toContain("-F 'age=30'");
|
|
42
|
-
expect(command).not.toContain('-d');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('should build POST request with file attachment', () => {
|
|
46
|
-
const command = CurlBuilder.buildCommand({
|
|
47
|
-
url: 'https://example.com/upload',
|
|
48
|
-
method: 'POST',
|
|
49
|
-
formData: {
|
|
50
|
-
document: {
|
|
51
|
-
file: './test.pdf',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(command).toContain("-F 'document=@./test.pdf'");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('should build POST request with file attachment and custom filename', () => {
|
|
60
|
-
const command = CurlBuilder.buildCommand({
|
|
61
|
-
url: 'https://example.com/upload',
|
|
62
|
-
method: 'POST',
|
|
63
|
-
formData: {
|
|
64
|
-
document: {
|
|
65
|
-
file: './test.pdf',
|
|
66
|
-
filename: 'report.pdf',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(command).toContain("-F 'document=@./test.pdf;filename=report.pdf'");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('should build POST request with file attachment and content type', () => {
|
|
75
|
-
const command = CurlBuilder.buildCommand({
|
|
76
|
-
url: 'https://example.com/upload',
|
|
77
|
-
method: 'POST',
|
|
78
|
-
formData: {
|
|
79
|
-
data: {
|
|
80
|
-
file: './data.json',
|
|
81
|
-
contentType: 'application/json',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
expect(command).toContain("-F 'data=@./data.json;type=application/json'");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('should build POST request with file attachment including all options', () => {
|
|
90
|
-
const command = CurlBuilder.buildCommand({
|
|
91
|
-
url: 'https://example.com/upload',
|
|
92
|
-
method: 'POST',
|
|
93
|
-
formData: {
|
|
94
|
-
document: {
|
|
95
|
-
file: './report.pdf',
|
|
96
|
-
filename: 'quarterly-report.pdf',
|
|
97
|
-
contentType: 'application/pdf',
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(command).toContain(
|
|
103
|
-
"-F 'document=@./report.pdf;filename=quarterly-report.pdf;type=application/pdf'",
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('should build POST request with mixed form data and files', () => {
|
|
108
|
-
const command = CurlBuilder.buildCommand({
|
|
109
|
-
url: 'https://example.com/upload',
|
|
110
|
-
method: 'POST',
|
|
111
|
-
formData: {
|
|
112
|
-
title: 'My Document',
|
|
113
|
-
description: 'Test upload',
|
|
114
|
-
file: {
|
|
115
|
-
file: './document.pdf',
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
expect(command).toContain("-F 'title=My Document'");
|
|
121
|
-
expect(command).toContain("-F 'description=Test upload'");
|
|
122
|
-
expect(command).toContain("-F 'file=@./document.pdf'");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('should escape single quotes in form field values', () => {
|
|
126
|
-
const command = CurlBuilder.buildCommand({
|
|
127
|
-
url: 'https://example.com/upload',
|
|
128
|
-
method: 'POST',
|
|
129
|
-
formData: {
|
|
130
|
-
message: "It's a test",
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(command).toContain("-F 'message=It'\\''s a test'");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('should prefer formData over body when both are present', () => {
|
|
138
|
-
const command = CurlBuilder.buildCommand({
|
|
139
|
-
url: 'https://example.com/api',
|
|
140
|
-
method: 'POST',
|
|
141
|
-
formData: {
|
|
142
|
-
field: 'value',
|
|
143
|
-
},
|
|
144
|
-
body: { name: 'test' },
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(command).toContain("-F 'field=value'");
|
|
148
|
-
expect(command).not.toContain('-d');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('should handle boolean form field values', () => {
|
|
152
|
-
const command = CurlBuilder.buildCommand({
|
|
153
|
-
url: 'https://example.com/api',
|
|
154
|
-
method: 'POST',
|
|
155
|
-
formData: {
|
|
156
|
-
active: true,
|
|
157
|
-
disabled: false,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(command).toContain("-F 'active=true'");
|
|
162
|
-
expect(command).toContain("-F 'disabled=false'");
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import type { FileAttachment, FormFieldValue, RequestConfig } from '../types/config';
|
|
2
|
-
|
|
3
|
-
interface CurlMetrics {
|
|
4
|
-
response_code?: number;
|
|
5
|
-
http_code?: number;
|
|
6
|
-
time_total?: number;
|
|
7
|
-
size_download?: number;
|
|
8
|
-
time_namelookup?: number;
|
|
9
|
-
time_connect?: number;
|
|
10
|
-
time_appconnect?: number;
|
|
11
|
-
time_starttransfer?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Checks if a form field value is a file attachment.
|
|
16
|
-
*/
|
|
17
|
-
function isFileAttachment(value: FormFieldValue): value is FileAttachment {
|
|
18
|
-
return typeof value === 'object' && value !== null && 'file' in value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Escapes a string value for use in curl -F flag.
|
|
23
|
-
*/
|
|
24
|
-
function escapeFormValue(value: string): string {
|
|
25
|
-
return value.replace(/'/g, "'\\''");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Using class for organization, but could be refactored to functions
|
|
29
|
-
export class CurlBuilder {
|
|
30
|
-
static buildCommand(config: RequestConfig): string {
|
|
31
|
-
const parts: string[] = ['curl'];
|
|
32
|
-
|
|
33
|
-
parts.push('-X', config.method || 'GET');
|
|
34
|
-
|
|
35
|
-
parts.push('-w', '"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"');
|
|
36
|
-
|
|
37
|
-
if (config.headers) {
|
|
38
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
39
|
-
parts.push('-H', `"${key}: ${value}"`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (config.auth) {
|
|
44
|
-
if (config.auth.type === 'basic' && config.auth.username && config.auth.password) {
|
|
45
|
-
parts.push('-u', `"${config.auth.username}:${config.auth.password}"`);
|
|
46
|
-
} else if (config.auth.type === 'bearer' && config.auth.token) {
|
|
47
|
-
parts.push('-H', `"Authorization: Bearer ${config.auth.token}"`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (config.formData) {
|
|
52
|
-
// Use -F flags for multipart/form-data
|
|
53
|
-
for (const [fieldName, fieldValue] of Object.entries(config.formData)) {
|
|
54
|
-
if (isFileAttachment(fieldValue)) {
|
|
55
|
-
// File attachment: -F "field=@filepath;filename=name;type=mimetype"
|
|
56
|
-
let fileSpec = `@${fieldValue.file}`;
|
|
57
|
-
if (fieldValue.filename) {
|
|
58
|
-
fileSpec += `;filename=${fieldValue.filename}`;
|
|
59
|
-
}
|
|
60
|
-
if (fieldValue.contentType) {
|
|
61
|
-
fileSpec += `;type=${fieldValue.contentType}`;
|
|
62
|
-
}
|
|
63
|
-
parts.push('-F', `'${fieldName}=${escapeFormValue(fileSpec)}'`);
|
|
64
|
-
} else {
|
|
65
|
-
// Regular form field: -F "field=value"
|
|
66
|
-
const strValue = String(fieldValue);
|
|
67
|
-
parts.push('-F', `'${fieldName}=${escapeFormValue(strValue)}'`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
} else if (config.body) {
|
|
71
|
-
const bodyStr = typeof config.body === 'string' ? config.body : JSON.stringify(config.body);
|
|
72
|
-
parts.push('-d', `'${bodyStr.replace(/'/g, "'\\''")}'`);
|
|
73
|
-
|
|
74
|
-
if (!config.headers?.['Content-Type']) {
|
|
75
|
-
parts.push('-H', '"Content-Type: application/json"');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (config.timeout) {
|
|
80
|
-
parts.push('--max-time', config.timeout.toString());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (config.followRedirects !== false) {
|
|
84
|
-
parts.push('-L');
|
|
85
|
-
if (config.maxRedirects) {
|
|
86
|
-
parts.push('--max-redirs', config.maxRedirects.toString());
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (config.proxy) {
|
|
91
|
-
parts.push('-x', config.proxy);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// SSL/TLS configuration
|
|
95
|
-
// insecure: true takes precedence (backwards compatibility)
|
|
96
|
-
// ssl.verify: false is equivalent to insecure: true
|
|
97
|
-
if (config.insecure || config.ssl?.verify === false) {
|
|
98
|
-
parts.push('-k');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// SSL certificate options
|
|
102
|
-
if (config.ssl) {
|
|
103
|
-
if (config.ssl.ca) {
|
|
104
|
-
parts.push('--cacert', `"${config.ssl.ca}"`);
|
|
105
|
-
}
|
|
106
|
-
if (config.ssl.cert) {
|
|
107
|
-
parts.push('--cert', `"${config.ssl.cert}"`);
|
|
108
|
-
}
|
|
109
|
-
if (config.ssl.key) {
|
|
110
|
-
parts.push('--key', `"${config.ssl.key}"`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (config.output) {
|
|
115
|
-
parts.push('-o', config.output);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
parts.push('-s', '-S');
|
|
119
|
-
|
|
120
|
-
let url = config.url;
|
|
121
|
-
if (config.params && Object.keys(config.params).length > 0) {
|
|
122
|
-
const queryString = new URLSearchParams(config.params).toString();
|
|
123
|
-
url += (url.includes('?') ? '&' : '?') + queryString;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
parts.push(`"${url}"`);
|
|
127
|
-
|
|
128
|
-
return parts.join(' ');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
static async executeCurl(command: string): Promise<{
|
|
132
|
-
success: boolean;
|
|
133
|
-
status?: number;
|
|
134
|
-
headers?: Record<string, string>;
|
|
135
|
-
body?: string;
|
|
136
|
-
metrics?: {
|
|
137
|
-
duration: number;
|
|
138
|
-
size?: number;
|
|
139
|
-
dnsLookup?: number;
|
|
140
|
-
tcpConnection?: number;
|
|
141
|
-
tlsHandshake?: number;
|
|
142
|
-
firstByte?: number;
|
|
143
|
-
download?: number;
|
|
144
|
-
};
|
|
145
|
-
error?: string;
|
|
146
|
-
}> {
|
|
147
|
-
try {
|
|
148
|
-
const proc = Bun.spawn(['sh', '-c', command], {
|
|
149
|
-
stdout: 'pipe',
|
|
150
|
-
stderr: 'pipe',
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const stdout = await new Response(proc.stdout).text();
|
|
154
|
-
const stderr = await new Response(proc.stderr).text();
|
|
155
|
-
|
|
156
|
-
await proc.exited;
|
|
157
|
-
|
|
158
|
-
if (proc.exitCode !== 0 && !stdout) {
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
error: stderr || `Command failed with exit code ${proc.exitCode}`,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
let responseBody = stdout;
|
|
166
|
-
let metrics: CurlMetrics = {};
|
|
167
|
-
|
|
168
|
-
const metricsMatch = stdout.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);
|
|
169
|
-
if (metricsMatch) {
|
|
170
|
-
responseBody = stdout.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/, '').trim();
|
|
171
|
-
try {
|
|
172
|
-
metrics = JSON.parse(metricsMatch[1]);
|
|
173
|
-
} catch (_e) {}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const responseHeaders: Record<string, string> = {};
|
|
177
|
-
if (metrics.response_code) {
|
|
178
|
-
const headerLines = stderr.split('\n').filter((line) => line.includes(':'));
|
|
179
|
-
for (const line of headerLines) {
|
|
180
|
-
const [key, ...valueParts] = line.split(':');
|
|
181
|
-
if (key && valueParts.length > 0) {
|
|
182
|
-
responseHeaders[key.trim()] = valueParts.join(':').trim();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
success: true,
|
|
189
|
-
status: metrics.response_code || metrics.http_code,
|
|
190
|
-
headers: responseHeaders,
|
|
191
|
-
body: responseBody,
|
|
192
|
-
metrics: {
|
|
193
|
-
duration: (metrics.time_total || 0) * 1000,
|
|
194
|
-
size: metrics.size_download,
|
|
195
|
-
dnsLookup: (metrics.time_namelookup || 0) * 1000,
|
|
196
|
-
tcpConnection: (metrics.time_connect || 0) * 1000,
|
|
197
|
-
tlsHandshake: (metrics.time_appconnect || 0) * 1000,
|
|
198
|
-
firstByte: (metrics.time_starttransfer || 0) * 1000,
|
|
199
|
-
download: (metrics.time_total || 0) * 1000,
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
} catch (error) {
|
|
203
|
-
return {
|
|
204
|
-
success: false,
|
|
205
|
-
error: error instanceof Error ? error.message : String(error),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|