fix(curl): properly export get requests with body payload (#25)
This commit is contained in:
@@ -44,7 +44,7 @@ const requestBase: PendingRequest = {
|
||||
};
|
||||
|
||||
describe('generateCurlCommand', () => {
|
||||
it('builds curl command with method, headers, and body', () => {
|
||||
it('builds curl command with method, headers, and body [POST]', () => {
|
||||
const { command, hasSpecialAuth } = generateCurlCommand(
|
||||
requestBase,
|
||||
'https://api.example.com',
|
||||
@@ -59,6 +59,76 @@ describe('generateCurlCommand', () => {
|
||||
expect(hasSpecialAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('builds curl command with method, headers, and body [GET]', () => {
|
||||
const getRequestBase = Object.assign({}, requestBase);
|
||||
|
||||
getRequestBase.method = 'GET';
|
||||
|
||||
getRequestBase.body.GET = getRequestBase.body.POST;
|
||||
|
||||
const { command, hasSpecialAuth } = generateCurlCommand(
|
||||
getRequestBase,
|
||||
'https://api.example.com',
|
||||
);
|
||||
|
||||
expect(command).toContain('curl');
|
||||
expect(command).toContain('"https://api.example.com/users?page=1&name=Jane"');
|
||||
expect(command).toContain('-H "Authorization: Bearer token"');
|
||||
expect(command).toContain('-H "Accept: application/json"');
|
||||
expect(hasSpecialAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('builds curl command with method, headers, and nested body [POST]', () => {
|
||||
const getRequestBase = Object.assign({}, requestBase);
|
||||
|
||||
getRequestBase.body.POST = {
|
||||
[RequestBodyTypeEnum.JSON]: JSON.stringify({
|
||||
user: { firstName: 'Jane', lastName: 'Doe' },
|
||||
username: 'foobar',
|
||||
}),
|
||||
};
|
||||
|
||||
const { command, hasSpecialAuth } = generateCurlCommand(
|
||||
getRequestBase,
|
||||
'https://api.example.com',
|
||||
);
|
||||
|
||||
expect(command).toContain('curl');
|
||||
expect(command).toContain('"https://api.example.com/users?page=1');
|
||||
expect(command).toContain('-H "Authorization: Bearer token"');
|
||||
expect(command).toContain('-H "Accept: application/json"');
|
||||
expect(command).toContain(
|
||||
'{"user":{"firstName":"Jane","lastName":"Doe"},"username":"foobar"}',
|
||||
);
|
||||
expect(hasSpecialAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('builds curl command with method, headers, and nested body [GET]', () => {
|
||||
const getRequestBase = Object.assign({}, requestBase);
|
||||
|
||||
getRequestBase.method = 'GET';
|
||||
|
||||
getRequestBase.body.GET = {
|
||||
[RequestBodyTypeEnum.JSON]: JSON.stringify({
|
||||
user: { firstName: 'Jane', lastName: 'Doe' },
|
||||
username: 'foobar',
|
||||
}),
|
||||
};
|
||||
|
||||
const { command, hasSpecialAuth } = generateCurlCommand(
|
||||
getRequestBase,
|
||||
'https://api.example.com',
|
||||
);
|
||||
|
||||
expect(command).toContain('curl');
|
||||
expect(command).toContain(
|
||||
'"https://api.example.com/users?page=1&user%5BfirstName%5D=Jane&user%5BlastName%5D=Doe&username=foobar"',
|
||||
);
|
||||
expect(command).toContain('-H "Authorization: Bearer token"');
|
||||
expect(command).toContain('-H "Accept: application/json"');
|
||||
expect(hasSpecialAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('flags special authorization types', () => {
|
||||
const request: PendingRequest = {
|
||||
...requestBase,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ParametersExternalContract } from '@/interfaces';
|
||||
import { AuthorizationContract } from '@/interfaces/auth/authorization';
|
||||
import { AuthorizationType } from '@/interfaces/generated';
|
||||
import { PendingRequest, RequestBodyTypeEnum, RequestHeader } from '@/interfaces/http';
|
||||
import { buildRequestUrl } from './request-url-builder';
|
||||
import { buildRequestUrl } from '@/utils';
|
||||
|
||||
/**
|
||||
* Result of cURL command generation.
|
||||
@@ -21,16 +22,18 @@ export function generateCurlCommand(
|
||||
request: PendingRequest,
|
||||
baseUrl: string,
|
||||
): CurlGenerationResult {
|
||||
const { queryParameters, requestBody } =
|
||||
getEffectiveQueryParametersAndBodyValue(request);
|
||||
|
||||
const methodPart = buildHttpMethodPart(request.method);
|
||||
// TODO [Bug] Properly create the url when the method is GET (body becomes query params).
|
||||
const urlPart = buildRequestUrlPart(request, baseUrl);
|
||||
const fullUrl = buildRequestUrl(baseUrl, request.endpoint, queryParameters);
|
||||
const headerParts = buildRequestHeaderParts(request);
|
||||
const authPart = buildAuthorizationHeaderPart(request.authorization);
|
||||
const bodyParts = getRequestBodyParts(request);
|
||||
const bodyParts = buildRequestBodyParts(requestBody);
|
||||
|
||||
const command = ['curl']
|
||||
.concat(methodPart ? [methodPart] : [])
|
||||
.concat([urlPart])
|
||||
.concat([`"${fullUrl}"`])
|
||||
.concat(headerParts)
|
||||
.concat(authPart ? [authPart] : [])
|
||||
.concat(bodyParts)
|
||||
@@ -42,6 +45,36 @@ export function generateCurlCommand(
|
||||
};
|
||||
}
|
||||
|
||||
function getEffectiveQueryParametersAndBodyValue(request: PendingRequest): {
|
||||
queryParameters: ParametersExternalContract[];
|
||||
requestBody: FormData | string | null;
|
||||
} {
|
||||
const requestBody = getRequestEffectiveBody(request);
|
||||
|
||||
const isGetRequest = request.method.toLowerCase() === 'get';
|
||||
|
||||
// In GET requests, we want to move the body content to query parameters and discard the body.
|
||||
if (isGetRequest) {
|
||||
const requestBodyKeyValuePairs = transformRequestBodyToKeyValuePairs(
|
||||
requestBody,
|
||||
request.payloadType,
|
||||
);
|
||||
|
||||
return {
|
||||
queryParameters: [
|
||||
...request.queryParameters,
|
||||
...convertKeyValuePairsToQueryParameters(requestBodyKeyValuePairs),
|
||||
],
|
||||
requestBody: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
queryParameters: request.queryParameters,
|
||||
requestBody: requestBody,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds HTTP method part.
|
||||
*/
|
||||
@@ -56,15 +89,6 @@ function buildHttpMethodPart(method: string): string | null {
|
||||
return `-X ${upperMethod}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds request URL part.
|
||||
*/
|
||||
function buildRequestUrlPart(request: PendingRequest, baseUrl: string): string {
|
||||
const fullUrl = buildRequestUrl(baseUrl, request.endpoint, request.queryParameters);
|
||||
|
||||
return `"${fullUrl}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds request header parts.
|
||||
*/
|
||||
@@ -102,17 +126,14 @@ function buildAuthorizationHeaderPart(
|
||||
return `-H "${authHeader}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds request body parts.
|
||||
*/
|
||||
function getRequestBodyParts(request: PendingRequest): string[] {
|
||||
const bodyData = extractRequestBody(request);
|
||||
|
||||
if (bodyData === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return bodyData;
|
||||
function convertKeyValuePairsToQueryParameters(
|
||||
keyValuePairs: Record<string, string>,
|
||||
): ParametersExternalContract[] {
|
||||
return Object.entries(keyValuePairs).map(([key, value]) => ({
|
||||
type: 'text',
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +188,7 @@ function buildBasicAuthHeader(authValue: {
|
||||
return `Authorization: Basic ${credentials}`;
|
||||
}
|
||||
|
||||
function extractRequestBody(request: PendingRequest): string[] | null {
|
||||
function getRequestEffectiveBody(request: PendingRequest): FormData | string | null {
|
||||
const bodyData = request.body;
|
||||
|
||||
const methodBodies = bodyData[request.method];
|
||||
@@ -182,43 +203,62 @@ function extractRequestBody(request: PendingRequest): string[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return formatBodyValue(body, request.payloadType);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds request body parts.
|
||||
*/
|
||||
function buildRequestBodyParts(requestBody: FormData | string | null): string[] {
|
||||
if (requestBody === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return convertBodyValueToRequestParts(requestBody);
|
||||
}
|
||||
|
||||
function transformRequestBodyToKeyValuePairs(
|
||||
bodyValue: string | FormData | null,
|
||||
payloadType: RequestBodyTypeEnum,
|
||||
): Record<string, string> {
|
||||
if (bodyValue === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof bodyValue === 'string' && payloadType === RequestBodyTypeEnum.JSON) {
|
||||
return JSON.parse(bodyValue);
|
||||
}
|
||||
|
||||
if (bodyValue instanceof FormData) {
|
||||
return Object.fromEntries(
|
||||
Array.from(bodyValue.entries()).map(([key, value]) => [
|
||||
key,
|
||||
value instanceof File ? `@${value}` : value,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return bodyValue.split('\n').reduce<Record<string, string>>(function (
|
||||
carry,
|
||||
bodyLine,
|
||||
) {
|
||||
const { 0: key, 1: value } = bodyLine.split('=');
|
||||
|
||||
carry[key] = value;
|
||||
|
||||
return carry as Record<string, string>;
|
||||
}, {}) as Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats body value based on payload type.
|
||||
*/
|
||||
function formatBodyValue(
|
||||
bodyValue: string | FormData | null,
|
||||
payloadType: RequestBodyTypeEnum,
|
||||
): string[] | null {
|
||||
switch (payloadType) {
|
||||
case RequestBodyTypeEnum.JSON:
|
||||
return typeof bodyValue === 'string' ? [bodyValue] : null;
|
||||
|
||||
case RequestBodyTypeEnum.FORM_DATA:
|
||||
return formatFormDataValue(bodyValue);
|
||||
|
||||
case RequestBodyTypeEnum.PLAIN_TEXT:
|
||||
return typeof bodyValue === 'string' ? [bodyValue] : null;
|
||||
|
||||
case RequestBodyTypeEnum.EMPTY:
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats form data value for cURL command.
|
||||
*/
|
||||
function formatFormDataValue(bodyValue: string | FormData | null): string[] | null {
|
||||
if (bodyValue instanceof FormData) {
|
||||
return convertFormDataToCUrlFields(bodyValue);
|
||||
function convertBodyValueToRequestParts(bodyValue: string | FormData): string[] {
|
||||
if (typeof bodyValue === 'string') {
|
||||
return [`-d ${bodyValue}`];
|
||||
}
|
||||
|
||||
return null;
|
||||
return convertFormDataToCUrlFields(bodyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,10 +267,10 @@ function formatFormDataValue(bodyValue: string | FormData | null): string[] | nu
|
||||
function convertFormDataToCUrlFields(formData: FormData): string[] {
|
||||
return Array.from(formData.entries()).map(([key, value]) => {
|
||||
if (value instanceof File) {
|
||||
return `${key}=@${value.name}`;
|
||||
return `-F ${key}=@${value.name}`;
|
||||
}
|
||||
|
||||
return `${key}=${value}`;
|
||||
return `-F ${key}=${value}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,34 @@ export function buildRequestUrl(
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.append(parameter.key, parameter.value);
|
||||
appendQueryParam(url.searchParams, parameter.key, parameter.value);
|
||||
});
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively flattens an object into query parameters using bracket notation.
|
||||
*/
|
||||
function appendQueryParam(
|
||||
searchParams: URLSearchParams,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Append each array element with [] notation
|
||||
value.forEach(item => appendQueryParam(searchParams, `${key}[]`, item));
|
||||
} else if (typeof value === 'object') {
|
||||
// Recursively handle nested objects
|
||||
Object.entries(value).forEach(([subKey, subValue]) => {
|
||||
appendQueryParam(searchParams, `${key}[${subKey}]`, subValue);
|
||||
});
|
||||
} else {
|
||||
// Primitive value: append directly
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user