mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-11 03:26:58 +02:00
Merge main into proxy branch (formatting and docs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@yaak/importer-curl",
|
||||
"displayName": "cURL Importer",
|
||||
"description": "Import requests from cURL commands",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Import requests from cURL commands",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
"test": "vitest --run tests"
|
||||
"test": "vp test --run tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"shlex": "^3.0.0"
|
||||
|
||||
@@ -6,38 +6,38 @@ import type {
|
||||
HttpUrlParameter,
|
||||
PluginDefinition,
|
||||
Workspace,
|
||||
} from '@yaakapp/api';
|
||||
import { split } from 'shlex';
|
||||
} from "@yaakapp/api";
|
||||
import { split } from "shlex";
|
||||
|
||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
||||
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId">[];
|
||||
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
||||
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
||||
}
|
||||
|
||||
const DATA_FLAGS = ['d', 'data', 'data-raw', 'data-urlencode', 'data-binary', 'data-ascii'];
|
||||
const DATA_FLAGS = ["d", "data", "data-raw", "data-urlencode", "data-binary", "data-ascii"];
|
||||
const SUPPORTED_FLAGS = [
|
||||
['cookie', 'b'],
|
||||
['d', 'data'], // Add url encoded data
|
||||
['data-ascii'],
|
||||
['data-binary'],
|
||||
['data-raw'],
|
||||
['data-urlencode'],
|
||||
['digest'], // Apply auth as digest
|
||||
['form', 'F'], // Add multipart data
|
||||
['get', 'G'], // Put the post data in the URL
|
||||
['header', 'H'],
|
||||
['request', 'X'], // Request method
|
||||
['url'], // Specify the URL explicitly
|
||||
['url-query'],
|
||||
['user', 'u'], // Authentication
|
||||
["cookie", "b"],
|
||||
["d", "data"], // Add url encoded data
|
||||
["data-ascii"],
|
||||
["data-binary"],
|
||||
["data-raw"],
|
||||
["data-urlencode"],
|
||||
["digest"], // Apply auth as digest
|
||||
["form", "F"], // Add multipart data
|
||||
["get", "G"], // Put the post data in the URL
|
||||
["header", "H"],
|
||||
["request", "X"], // Request method
|
||||
["url"], // Specify the URL explicitly
|
||||
["url-query"],
|
||||
["user", "u"], // Authentication
|
||||
DATA_FLAGS,
|
||||
].flat();
|
||||
|
||||
const BOOLEAN_FLAGS = ['G', 'get', 'digest'];
|
||||
const BOOLEAN_FLAGS = ["G", "get", "digest"];
|
||||
|
||||
type FlagValue = string | boolean;
|
||||
|
||||
@@ -45,10 +45,10 @@ type FlagsByName = Record<string, FlagValue[]>;
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
importer: {
|
||||
name: 'cURL',
|
||||
description: 'Import cURL commands',
|
||||
name: "cURL",
|
||||
description: "Import cURL commands",
|
||||
onImport(_ctx: Context, args: { text: string }) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
return convertCurl(args.text) as any;
|
||||
},
|
||||
},
|
||||
@@ -60,14 +60,14 @@ export const plugin: PluginDefinition = {
|
||||
*/
|
||||
function splitCommands(rawData: string): string[] {
|
||||
// Join line continuations (backslash-newline, and backslash-CRLF for Windows)
|
||||
const joined = rawData.replace(/\\\r?\n/g, ' ');
|
||||
const joined = rawData.replace(/\\\r?\n/g, " ");
|
||||
|
||||
// Count consecutive backslashes immediately before position i.
|
||||
// An even count means the quote at i is NOT escaped; odd means it IS escaped.
|
||||
function isEscaped(i: number): boolean {
|
||||
let backslashes = 0;
|
||||
let j = i - 1;
|
||||
while (j >= 0 && joined[j] === '\\') {
|
||||
while (j >= 0 && joined[j] === "\\") {
|
||||
backslashes++;
|
||||
j--;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ function splitCommands(rawData: string): string[] {
|
||||
|
||||
// Split on semicolons and newlines to separate commands
|
||||
const commands: string[] = [];
|
||||
let current = '';
|
||||
let current = "";
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let inDollarQuote = false;
|
||||
@@ -108,7 +108,7 @@ function splitCommands(rawData: string): string[] {
|
||||
current += ch;
|
||||
continue;
|
||||
}
|
||||
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && ch === '$' && next === "'") {
|
||||
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && ch === "$" && next === "'") {
|
||||
inDollarQuote = true;
|
||||
current += ch + next;
|
||||
i++; // Skip the opening quote
|
||||
@@ -126,13 +126,13 @@ function splitCommands(rawData: string): string[] {
|
||||
if (
|
||||
!inQuote &&
|
||||
!isEscaped(i) &&
|
||||
(ch === ';' || ch === '\n' || (ch === '\r' && next === '\n'))
|
||||
(ch === ";" || ch === "\n" || (ch === "\r" && next === "\n"))
|
||||
) {
|
||||
if (ch === '\r') i++; // Skip the \n in \r\n
|
||||
if (ch === "\r") i++; // Skip the \n in \r\n
|
||||
if (current.trim()) {
|
||||
commands.push(current.trim());
|
||||
}
|
||||
current = '';
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -156,21 +156,21 @@ export function convertCurl(rawData: string) {
|
||||
|
||||
// Break up squished arguments like `-XPOST` into `-X POST`
|
||||
return tokens.flatMap((token) => {
|
||||
if (token.startsWith('-') && !token.startsWith('--') && token.length > 2) {
|
||||
if (token.startsWith("-") && !token.startsWith("--") && token.length > 2) {
|
||||
return [token.slice(0, 2), token.slice(2)];
|
||||
}
|
||||
return token;
|
||||
});
|
||||
});
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: generateId('workspace'),
|
||||
name: 'Curl Import',
|
||||
const workspace: ExportResources["workspaces"][0] = {
|
||||
model: "workspace",
|
||||
id: generateId("workspace"),
|
||||
name: "Curl Import",
|
||||
};
|
||||
|
||||
const requests: ExportResources['httpRequests'] = commands
|
||||
.filter((command) => command[0] === 'curl')
|
||||
const requests: ExportResources["httpRequests"] = commands
|
||||
.filter((command) => command[0] === "curl")
|
||||
.map((v) => importCommand(v, workspace.id));
|
||||
|
||||
return {
|
||||
@@ -191,13 +191,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Start at 1 so we can skip the ^curl part
|
||||
for (let i = 1; i < parseEntries.length; i++) {
|
||||
let parseEntry = parseEntries[i];
|
||||
if (typeof parseEntry === 'string') {
|
||||
if (typeof parseEntry === "string") {
|
||||
parseEntry = parseEntry.trim();
|
||||
}
|
||||
|
||||
if (typeof parseEntry === 'string' && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === '-' && parseEntry[1] !== '-';
|
||||
let name = parseEntry.replace(/^-{1,2}/, '');
|
||||
if (typeof parseEntry === "string" && parseEntry.match(/^-{1,2}[\w-]+/)) {
|
||||
const isSingleDash = parseEntry[0] === "-" && parseEntry[1] !== "-";
|
||||
let name = parseEntry.replace(/^-{1,2}/, "");
|
||||
|
||||
if (!SUPPORTED_FLAGS.includes(name)) {
|
||||
continue;
|
||||
@@ -211,13 +211,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// - Double dash followed by a letter: --data-raw, --header
|
||||
// This prevents mistaking data that starts with dashes (like multipart boundaries ------) as flags
|
||||
const nextEntryIsFlag =
|
||||
typeof nextEntry === 'string' &&
|
||||
typeof nextEntry === "string" &&
|
||||
(nextEntry.match(/^-[a-zA-Z]/) || nextEntry.match(/^--[a-zA-Z]/));
|
||||
if (isSingleDash && name.length > 1) {
|
||||
// Handle squished arguments like -XPOST
|
||||
value = name.slice(1);
|
||||
name = name.slice(0, 1);
|
||||
} else if (typeof nextEntry === 'string' && hasValue && !nextEntryIsFlag) {
|
||||
} else if (typeof nextEntry === "string" && hasValue && !nextEntryIsFlag) {
|
||||
// Next arg is not a flag, so assign it as the value
|
||||
value = nextEntry;
|
||||
i++; // Skip next one
|
||||
@@ -236,14 +236,14 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Build the request //
|
||||
// ~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
|
||||
const [baseUrl, search] = splitOnce(urlArg, '?');
|
||||
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || "", ["url"]);
|
||||
const [baseUrl, search] = splitOnce(urlArg, "?");
|
||||
const urlParameters: HttpUrlParameter[] =
|
||||
search?.split('&').map((p) => {
|
||||
const v = splitOnce(p, '=');
|
||||
search?.split("&").map((p) => {
|
||||
const v = splitOnce(p, "=");
|
||||
return {
|
||||
name: decodeURIComponent(v[0] ?? ''),
|
||||
value: decodeURIComponent(v[1] ?? ''),
|
||||
name: decodeURIComponent(v[0] ?? ""),
|
||||
value: decodeURIComponent(v[1] ?? ""),
|
||||
enabled: true,
|
||||
};
|
||||
}) ?? [];
|
||||
@@ -251,27 +251,27 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
const url = baseUrl ?? urlArg;
|
||||
|
||||
// Query params
|
||||
for (const p of flagsByName['url-query'] ?? []) {
|
||||
if (typeof p !== 'string') {
|
||||
for (const p of flagsByName["url-query"] ?? []) {
|
||||
if (typeof p !== "string") {
|
||||
continue;
|
||||
}
|
||||
const [name, value] = p.split('=');
|
||||
const [name, value] = p.split("=");
|
||||
urlParameters.push({
|
||||
name: name ?? '',
|
||||
value: value ?? '',
|
||||
name: name ?? "",
|
||||
value: value ?? "",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication
|
||||
const [username, password] = getPairValue(flagsByName, '', ['u', 'user']).split(/:(.*)$/);
|
||||
const [username, password] = getPairValue(flagsByName, "", ["u", "user"]).split(/:(.*)$/);
|
||||
|
||||
const isDigest = getPairValue(flagsByName, false, ['digest']);
|
||||
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
|
||||
const isDigest = getPairValue(flagsByName, false, ["digest"]);
|
||||
const authenticationType = username ? (isDigest ? "digest" : "basic") : null;
|
||||
const authentication = username
|
||||
? {
|
||||
username: username.trim(),
|
||||
password: (password ?? '').trim(),
|
||||
password: (password ?? "").trim(),
|
||||
}
|
||||
: {};
|
||||
|
||||
@@ -284,13 +284,13 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// remove final colon from header name if present
|
||||
if (!value) {
|
||||
return {
|
||||
name: (name ?? '').trim().replace(/;$/, ''),
|
||||
value: '',
|
||||
name: (name ?? "").trim().replace(/;$/, ""),
|
||||
value: "",
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: (name ?? '').trim(),
|
||||
name: (name ?? "").trim(),
|
||||
value: value.trim(),
|
||||
enabled: true,
|
||||
};
|
||||
@@ -302,14 +302,14 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
...((flagsByName.b as string[] | undefined) || []),
|
||||
]
|
||||
.map((str) => {
|
||||
const name = str.split('=', 1)[0];
|
||||
const value = str.replace(`${name}=`, '');
|
||||
const name = str.split("=", 1)[0];
|
||||
const value = str.replace(`${name}=`, "");
|
||||
return `${name}=${value}`;
|
||||
})
|
||||
.join('; ');
|
||||
.join("; ");
|
||||
|
||||
// Convert cookie value to header
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === 'cookie');
|
||||
const existingCookieHeader = headers.find((header) => header.name.toLowerCase() === "cookie");
|
||||
|
||||
if (cookieHeaderValue && existingCookieHeader) {
|
||||
// Has existing cookie header, so let's update it
|
||||
@@ -317,15 +317,15 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
} else if (cookieHeaderValue) {
|
||||
// No existing cookie header, so let's make a new one
|
||||
headers.push({
|
||||
name: 'Cookie',
|
||||
name: "Cookie",
|
||||
value: cookieHeaderValue,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Body (Text or Blob)
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === 'content-type');
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(';')[0]?.trim() : null;
|
||||
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
|
||||
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0]?.trim() : null;
|
||||
|
||||
// Extract boundary from Content-Type header for multipart parsing
|
||||
const boundaryMatch = contentTypeHeader?.value.match(/boundary=([^\s;]+)/i);
|
||||
@@ -333,19 +333,19 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
|
||||
// Get raw data from --data-raw flags (before splitting by &)
|
||||
const rawDataValues = [
|
||||
...((flagsByName['data-raw'] as string[] | undefined) || []),
|
||||
...((flagsByName["data-raw"] as string[] | undefined) || []),
|
||||
...((flagsByName.d as string[] | undefined) || []),
|
||||
...((flagsByName.data as string[] | undefined) || []),
|
||||
...((flagsByName['data-binary'] as string[] | undefined) || []),
|
||||
...((flagsByName['data-ascii'] as string[] | undefined) || []),
|
||||
...((flagsByName["data-binary"] as string[] | undefined) || []),
|
||||
...((flagsByName["data-ascii"] as string[] | undefined) || []),
|
||||
];
|
||||
|
||||
// Check if this is multipart form data in --data-raw (Chrome DevTools format)
|
||||
let multipartFormDataFromRaw:
|
||||
| { name: string; value?: string; file?: string; enabled: boolean }[]
|
||||
| null = null;
|
||||
if (mimeType === 'multipart/form-data' && boundary && rawDataValues.length > 0) {
|
||||
const rawBody = rawDataValues.join('');
|
||||
if (mimeType === "multipart/form-data" && boundary && rawDataValues.length > 0) {
|
||||
const rawBody = rawDataValues.join("");
|
||||
multipartFormDataFromRaw = parseMultipartFormData(rawBody, boundary);
|
||||
}
|
||||
|
||||
@@ -356,15 +356,15 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
...((flagsByName.form as string[] | undefined) || []),
|
||||
...((flagsByName.F as string[] | undefined) || []),
|
||||
].map((str) => {
|
||||
const parts = str.split('=');
|
||||
const name = parts[0] ?? '';
|
||||
const value = parts[1] ?? '';
|
||||
const parts = str.split("=");
|
||||
const name = parts[0] ?? "";
|
||||
const value = parts[1] ?? "";
|
||||
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
|
||||
name,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (value.indexOf('@') === 0) {
|
||||
if (value.indexOf("@") === 0) {
|
||||
item.file = value.slice(1);
|
||||
} else {
|
||||
item.value = value;
|
||||
@@ -376,11 +376,11 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
// Body
|
||||
let body = {};
|
||||
let bodyType: string | null = null;
|
||||
const bodyAsGET = getPairValue(flagsByName, false, ['G', 'get']);
|
||||
const bodyAsGET = getPairValue(flagsByName, false, ["G", "get"]);
|
||||
|
||||
if (multipartFormDataFromRaw) {
|
||||
// Handle multipart form data parsed from --data-raw (Chrome DevTools format)
|
||||
bodyType = 'multipart/form-data';
|
||||
bodyType = "multipart/form-data";
|
||||
body = {
|
||||
form: multipartFormDataFromRaw,
|
||||
};
|
||||
@@ -388,57 +388,57 @@ function importCommand(parseEntries: string[], workspaceId: string) {
|
||||
urlParameters.push(...dataParameters);
|
||||
} else if (
|
||||
dataParameters.length > 0 &&
|
||||
(mimeType == null || mimeType === 'application/x-www-form-urlencoded')
|
||||
(mimeType == null || mimeType === "application/x-www-form-urlencoded")
|
||||
) {
|
||||
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
|
||||
bodyType = mimeType ?? "application/x-www-form-urlencoded";
|
||||
body = {
|
||||
form: dataParameters.map((parameter) => ({
|
||||
...parameter,
|
||||
name: decodeURIComponent(parameter.name || ''),
|
||||
value: decodeURIComponent(parameter.value || ''),
|
||||
name: decodeURIComponent(parameter.name || ""),
|
||||
value: decodeURIComponent(parameter.value || ""),
|
||||
})),
|
||||
};
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
});
|
||||
} else if (dataParameters.length > 0) {
|
||||
bodyType =
|
||||
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
|
||||
mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain"
|
||||
? mimeType
|
||||
: 'other';
|
||||
: "other";
|
||||
body = {
|
||||
text: dataParameters
|
||||
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
|
||||
.join('&'),
|
||||
.join("&"),
|
||||
};
|
||||
} else if (formDataParams.length) {
|
||||
bodyType = mimeType ?? 'multipart/form-data';
|
||||
bodyType = mimeType ?? "multipart/form-data";
|
||||
body = {
|
||||
form: formDataParams,
|
||||
};
|
||||
if (mimeType == null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method
|
||||
let method = getPairValue(flagsByName, '', ['X', 'request']).toUpperCase();
|
||||
let method = getPairValue(flagsByName, "", ["X", "request"]).toUpperCase();
|
||||
|
||||
if (method === '' && body) {
|
||||
method = 'text' in body || 'form' in body ? 'POST' : 'GET';
|
||||
if (method === "" && body) {
|
||||
method = "text" in body || "form" in body ? "POST" : "GET";
|
||||
}
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
id: generateId('http_request'),
|
||||
model: 'http_request',
|
||||
const request: ExportResources["httpRequests"][0] = {
|
||||
id: generateId("http_request"),
|
||||
model: "http_request",
|
||||
workspaceId,
|
||||
name: '',
|
||||
name: "",
|
||||
urlParameters,
|
||||
url,
|
||||
method,
|
||||
@@ -473,22 +473,22 @@ function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
|
||||
}
|
||||
|
||||
for (const p of pairs) {
|
||||
if (typeof p !== 'string') continue;
|
||||
const params = p.split('&');
|
||||
if (typeof p !== "string") continue;
|
||||
const params = p.split("&");
|
||||
for (const param of params) {
|
||||
const [name, value] = splitOnce(param, '=');
|
||||
if (param.startsWith('@')) {
|
||||
const [name, value] = splitOnce(param, "=");
|
||||
if (param.startsWith("@")) {
|
||||
// Yaak doesn't support files in url-encoded data, so
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: '',
|
||||
name: name ?? "",
|
||||
value: "",
|
||||
filePath: param.slice(1),
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : (value ?? ''),
|
||||
name: name ?? "",
|
||||
value: flagName === "data-urlencode" ? encodeURIComponent(value ?? "") : (value ?? ""),
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
@@ -537,12 +537,12 @@ function parseMultipartFormData(
|
||||
|
||||
for (const part of parts) {
|
||||
// Skip empty parts and the closing boundary marker
|
||||
if (!part || part.trim() === '--' || part.trim() === '--\r\n') {
|
||||
if (!part || part.trim() === "--" || part.trim() === "--\r\n") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each part has headers and content separated by \r\n\r\n
|
||||
const headerContentSplit = part.indexOf('\r\n\r\n');
|
||||
const headerContentSplit = part.indexOf("\r\n\r\n");
|
||||
if (headerContentSplit === -1) {
|
||||
continue;
|
||||
}
|
||||
@@ -551,7 +551,7 @@ function parseMultipartFormData(
|
||||
let content = part.slice(headerContentSplit + 4); // Skip \r\n\r\n
|
||||
|
||||
// Remove trailing \r\n from content
|
||||
if (content.endsWith('\r\n')) {
|
||||
if (content.endsWith("\r\n")) {
|
||||
content = content.slice(0, -2);
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ function parseMultipartFormData(
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = contentDispositionMatch[1] ?? '';
|
||||
const name = contentDispositionMatch[1] ?? "";
|
||||
const filename = contentDispositionMatch[2];
|
||||
|
||||
const item: { name: string; value?: string; file?: string; enabled: boolean } = {
|
||||
|
||||
@@ -1,159 +1,182 @@
|
||||
import type { HttpRequest, Workspace } from '@yaakapp/api';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { convertCurl } from '../src';
|
||||
import type { HttpRequest, Workspace } from "@yaakapp/api";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { convertCurl } from "../src";
|
||||
|
||||
describe('importer-curl', () => {
|
||||
test('Imports basic GET', () => {
|
||||
expect(convertCurl('curl https://yaak.app')).toEqual({
|
||||
describe("importer-curl", () => {
|
||||
test("Imports basic GET", () => {
|
||||
expect(convertCurl("curl https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Explicit URL', () => {
|
||||
expect(convertCurl('curl --url https://yaak.app')).toEqual({
|
||||
test("Explicit URL", () => {
|
||||
expect(convertCurl("curl --url https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Missing URL', () => {
|
||||
expect(convertCurl('curl -X POST')).toEqual({
|
||||
test("Missing URL", () => {
|
||||
expect(convertCurl("curl -X POST")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('URL between', () => {
|
||||
expect(convertCurl('curl -v https://yaak.app -X POST')).toEqual({
|
||||
test("URL between", () => {
|
||||
expect(convertCurl("curl -v https://yaak.app -X POST")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Random flags', () => {
|
||||
expect(convertCurl('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
|
||||
test("Random flags", () => {
|
||||
expect(convertCurl("curl --random -Z -Y -S --foo https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports --request method', () => {
|
||||
expect(convertCurl('curl --request POST https://yaak.app')).toEqual({
|
||||
test("Imports --request method", () => {
|
||||
expect(convertCurl("curl --request POST https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports -XPOST method', () => {
|
||||
expect(convertCurl('curl -XPOST --request POST https://yaak.app')).toEqual({
|
||||
test("Imports -XPOST method", () => {
|
||||
expect(convertCurl("curl -XPOST --request POST https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple requests', () => {
|
||||
test("Imports multiple requests", () => {
|
||||
expect(
|
||||
convertCurl('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({ url: 'https://yaak.app' }),
|
||||
baseRequest({ url: 'example.com' }),
|
||||
baseRequest({ url: 'foo.com' }),
|
||||
baseRequest({ url: "https://yaak.app" }),
|
||||
baseRequest({ url: "example.com" }),
|
||||
baseRequest({ url: "foo.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports with Windows CRLF line endings', () => {
|
||||
expect(
|
||||
convertCurl('curl \\\r\n -X POST \\\r\n https://yaak.app'),
|
||||
).toEqual({
|
||||
test("Imports with Windows CRLF line endings", () => {
|
||||
expect(convertCurl("curl \\\r\n -X POST \\\r\n https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({ url: 'https://yaak.app', method: 'POST' }),
|
||||
],
|
||||
httpRequests: [baseRequest({ url: "https://yaak.app", method: "POST" })],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Throws on malformed quotes', () => {
|
||||
expect(() =>
|
||||
convertCurl('curl -X POST -F "a=aaa" -F b=bbb" https://yaak.app'),
|
||||
).toThrow();
|
||||
test("Throws on malformed quotes", () => {
|
||||
expect(() => convertCurl('curl -X POST -F "a=aaa" -F b=bbb" https://yaak.app')).toThrow();
|
||||
});
|
||||
|
||||
test('Imports form data', () => {
|
||||
expect(
|
||||
convertCurl('curl -X POST -F "a=aaa" -F b=bbb -F f=@filepath https://yaak.app'),
|
||||
).toEqual({
|
||||
test("Imports form data", () => {
|
||||
expect(convertCurl('curl -X POST -F "a=aaa" -F b=bbb -F f=@filepath https://yaak.app')).toEqual(
|
||||
{
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ enabled: true, name: "a", value: "aaa" },
|
||||
{ enabled: true, name: "b", value: "bbb" },
|
||||
{ enabled: true, name: "f", file: "filepath" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Imports data params as form url-encoded", () => {
|
||||
expect(convertCurl("curl -d a -d b -d c=ccc https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: [
|
||||
{ enabled: true, name: 'a', value: 'aaa' },
|
||||
{ enabled: true, name: 'b', value: 'bbb' },
|
||||
{ enabled: true, name: 'f', file: 'filepath' },
|
||||
{ name: "a", value: "", enabled: true },
|
||||
{ name: "b", value: "", enabled: true },
|
||||
{ name: "c", value: "ccc", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -162,56 +185,27 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as form url-encoded', () => {
|
||||
expect(convertCurl('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: '', enabled: true },
|
||||
{ name: 'b', value: '', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports combined data params as form url-encoded', () => {
|
||||
test("Imports combined data params as form url-encoded", () => {
|
||||
expect(convertCurl(`curl -d 'a=aaa&b=bbb&c' https://yaak.app`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'a', value: 'aaa', enabled: true },
|
||||
{ name: 'b', value: 'bbb', enabled: true },
|
||||
{ name: 'c', value: '', enabled: true },
|
||||
{ name: "a", value: "aaa", enabled: true },
|
||||
{ name: "b", value: "bbb", enabled: true },
|
||||
{ name: "c", value: "", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -220,38 +214,38 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data params as text', () => {
|
||||
test("Imports data params as text", () => {
|
||||
expect(
|
||||
convertCurl('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
|
||||
convertCurl("curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app"),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }],
|
||||
bodyType: 'text/plain',
|
||||
body: { text: 'a&b&c=ccc' },
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Content-Type", value: "text/plain", enabled: true }],
|
||||
bodyType: "text/plain",
|
||||
body: { text: "a&b&c=ccc" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports post data into URL', () => {
|
||||
expect(convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3')).toEqual({
|
||||
test("Imports post data into URL", () => {
|
||||
expect(convertCurl("curl -G https://api.stripe.com/v1/payment_links -d limit=3")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'GET',
|
||||
url: 'https://api.stripe.com/v1/payment_links',
|
||||
method: "GET",
|
||||
url: "https://api.stripe.com/v1/payment_links",
|
||||
urlParameters: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'limit',
|
||||
value: '3',
|
||||
name: "limit",
|
||||
value: "3",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -260,7 +254,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multi-line JSON', () => {
|
||||
test("Imports multi-line JSON", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
|
||||
@@ -270,10 +264,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
method: "POST",
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{\n "foo":"bar"\n}' },
|
||||
}),
|
||||
],
|
||||
@@ -281,20 +275,20 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple headers', () => {
|
||||
test("Imports multiple headers", () => {
|
||||
expect(
|
||||
convertCurl('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
|
||||
convertCurl("curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app"),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
headers: [
|
||||
{ name: 'Name', value: '', enabled: true },
|
||||
{ name: 'Foo', value: 'bar', enabled: true },
|
||||
{ name: 'AAA', value: 'bbb', enabled: true },
|
||||
{ name: '', value: 'ccc', enabled: true },
|
||||
{ name: "Name", value: "", enabled: true },
|
||||
{ name: "Foo", value: "bar", enabled: true },
|
||||
{ name: "AAA", value: "bbb", enabled: true },
|
||||
{ name: "", value: "ccc", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -302,17 +296,17 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports basic auth', () => {
|
||||
expect(convertCurl('curl --user user:pass https://yaak.app')).toEqual({
|
||||
test("Imports basic auth", () => {
|
||||
expect(convertCurl("curl --user user:pass https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'basic',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -320,17 +314,17 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports digest auth', () => {
|
||||
expect(convertCurl('curl --digest --user user:pass https://yaak.app')).toEqual({
|
||||
test("Imports digest auth", () => {
|
||||
expect(convertCurl("curl --digest --user user:pass https://yaak.app")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
authenticationType: 'digest',
|
||||
url: "https://yaak.app",
|
||||
authenticationType: "digest",
|
||||
authentication: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
username: "user",
|
||||
password: "pass",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -338,30 +332,30 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports cookie as header', () => {
|
||||
test("Imports cookie as header", () => {
|
||||
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }],
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "Cookie", value: "foo=bar", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params', () => {
|
||||
test("Imports query params", () => {
|
||||
expect(convertCurl('curl "https://yaak.app" --url-query foo=bar --url-query baz=qux')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'qux', enabled: true },
|
||||
{ name: "foo", value: "bar", enabled: true },
|
||||
{ name: "baz", value: "qux", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -369,16 +363,16 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports query params from the URL', () => {
|
||||
test("Imports query params from the URL", () => {
|
||||
expect(convertCurl('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'a a', enabled: true },
|
||||
{ name: "foo", value: "bar", enabled: true },
|
||||
{ name: "baz", value: "a a", enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -386,23 +380,23 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports weird body', () => {
|
||||
test("Imports weird body", () => {
|
||||
expect(convertCurl(`curl 'https://yaak.app' -X POST --data-raw 'foo=bar=baz'`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'foo', value: 'bar=baz', enabled: true }],
|
||||
form: [{ name: "foo", value: "bar=baz", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -411,7 +405,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data with Unicode escape sequences', () => {
|
||||
test("Imports data with Unicode escape sequences", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{"query":"SearchQueryInput\\u0021"}' -X POST`,
|
||||
@@ -421,10 +415,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{"query":"SearchQueryInput!"}' },
|
||||
}),
|
||||
],
|
||||
@@ -432,7 +426,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports data with multiple escape sequences', () => {
|
||||
test("Imports data with multiple escape sequences", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' --data-raw $'Line1\\nLine2\\tTab\\u0021Exclamation' -X POST`,
|
||||
@@ -442,17 +436,17 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'Line1\nLine2\tTab!Exclamation', value: '', enabled: true }],
|
||||
form: [{ name: "Line1\nLine2\tTab!Exclamation", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -461,7 +455,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multipart form data from --data-raw (Chrome DevTools format)', () => {
|
||||
test("Imports multipart form data from --data-raw (Chrome DevTools format)", () => {
|
||||
// This is the format Chrome DevTools uses when copying a multipart form submission as cURL
|
||||
const curlCommand = `curl 'http://localhost:8080/system' \
|
||||
-H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd' \
|
||||
@@ -472,21 +466,21 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'http://localhost:8080/system',
|
||||
method: 'POST',
|
||||
url: "http://localhost:8080/system",
|
||||
method: "POST",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'username', value: 'jsgj', enabled: true },
|
||||
{ name: 'password', value: '654321', enabled: true },
|
||||
{ name: 'captcha', file: 'test.xlsx', enabled: true },
|
||||
{ name: "username", value: "jsgj", enabled: true },
|
||||
{ name: "password", value: "654321", enabled: true },
|
||||
{ name: "captcha", file: "test.xlsx", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -495,7 +489,7 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports JSON body with newlines in $quotes', () => {
|
||||
test("Imports JSON body with newlines in $quotes", () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{\\n "foo": "bar",\\n "baz": "qux"\\n}' -X POST`,
|
||||
@@ -505,10 +499,10 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
||||
bodyType: "application/json",
|
||||
body: { text: '{\n "foo": "bar",\n "baz": "qux"\n}' },
|
||||
}),
|
||||
],
|
||||
@@ -516,110 +510,94 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles double-quoted string ending with even backslashes before semicolon', () => {
|
||||
test("Handles double-quoted string ending with even backslashes before semicolon", () => {
|
||||
// "C:\\" has two backslashes which escape each other, so the closing " is real.
|
||||
// The ; after should split into a second command.
|
||||
expect(
|
||||
convertCurl(
|
||||
'curl -d "C:\\\\" https://yaak.app;curl https://example.com',
|
||||
),
|
||||
).toEqual({
|
||||
expect(convertCurl('curl -d "C:\\\\" https://yaak.app;curl https://example.com')).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'C:\\', value: '', enabled: true }],
|
||||
form: [{ name: "C:\\", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
baseRequest({ url: 'https://example.com' }),
|
||||
baseRequest({ url: "https://example.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Handles $quoted string ending with a literal backslash before semicolon', () => {
|
||||
test("Handles $quoted string ending with a literal backslash before semicolon", () => {
|
||||
// $'C:\\\\' has two backslashes which become one literal backslash.
|
||||
// The closing ' must not be misinterpreted as escaped.
|
||||
// The ; after should split into a second command.
|
||||
expect(
|
||||
convertCurl(
|
||||
"curl -d $'C:\\\\' https://yaak.app;curl https://example.com",
|
||||
),
|
||||
).toEqual({
|
||||
expect(convertCurl("curl -d $'C:\\\\' https://yaak.app;curl https://example.com")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
url: "https://yaak.app",
|
||||
method: "POST",
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: [{ name: 'C:\\', value: '', enabled: true }],
|
||||
form: [{ name: "C:\\", value: "", enabled: true }],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
baseRequest({ url: 'https://example.com' }),
|
||||
baseRequest({ url: "https://example.com" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports $quoted header with escaped single quotes', () => {
|
||||
expect(
|
||||
convertCurl(
|
||||
`curl https://yaak.app -H $'X-Custom: it\\'s a test'`,
|
||||
),
|
||||
).toEqual({
|
||||
test("Imports $quoted header with escaped single quotes", () => {
|
||||
expect(convertCurl(`curl https://yaak.app -H $'X-Custom: it\\'s a test'`)).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'X-Custom', value: "it's a test", enabled: true }],
|
||||
url: "https://yaak.app",
|
||||
headers: [{ name: "X-Custom", value: "it's a test", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Does not split on escaped semicolon outside quotes', () => {
|
||||
test("Does not split on escaped semicolon outside quotes", () => {
|
||||
// In shell, \; is a literal semicolon and should not split commands.
|
||||
// This should be treated as a single curl command with the URL "https://yaak.app?a=1;b=2"
|
||||
expect(
|
||||
convertCurl('curl https://yaak.app?a=1\\;b=2'),
|
||||
).toEqual({
|
||||
expect(convertCurl("curl https://yaak.app?a=1\\;b=2")).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'a', value: '1;b=2', enabled: true },
|
||||
],
|
||||
url: "https://yaak.app",
|
||||
urlParameters: [{ name: "a", value: "1;b=2", enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multipart form data with text-only fields from --data-raw', () => {
|
||||
test("Imports multipart form data with text-only fields from --data-raw", () => {
|
||||
const curlCommand = `curl 'http://example.com/api' \
|
||||
-H 'Content-Type: multipart/form-data; boundary=----FormBoundary123' \
|
||||
--data-raw $'------FormBoundary123\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n------FormBoundary123\r\nContent-Disposition: form-data; name="field2"\r\n\r\nvalue2\r\n------FormBoundary123--\r\n'`;
|
||||
@@ -629,20 +607,20 @@ describe('importer-curl', () => {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'http://example.com/api',
|
||||
method: 'POST',
|
||||
url: "http://example.com/api",
|
||||
method: "POST",
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data; boundary=----FormBoundary123',
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data; boundary=----FormBoundary123",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: [
|
||||
{ name: 'field1', value: 'value1', enabled: true },
|
||||
{ name: 'field2', value: 'value2', enabled: true },
|
||||
{ name: "field1", value: "value1", enabled: true },
|
||||
{ name: "field2", value: "value2", enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -658,17 +636,17 @@ function baseRequest(mergeWith: Partial<HttpRequest>) {
|
||||
idCount.http_request = (idCount.http_request ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`,
|
||||
model: 'http_request',
|
||||
model: "http_request",
|
||||
authentication: {},
|
||||
authenticationType: null,
|
||||
body: {},
|
||||
bodyType: null,
|
||||
folderId: null,
|
||||
headers: [],
|
||||
method: 'GET',
|
||||
name: '',
|
||||
method: "GET",
|
||||
name: "",
|
||||
sortPriority: 0,
|
||||
url: '',
|
||||
url: "",
|
||||
urlParameters: [],
|
||||
workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
...mergeWith,
|
||||
@@ -679,8 +657,8 @@ function baseWorkspace(mergeWith: Partial<Workspace> = {}) {
|
||||
idCount.workspace = (idCount.workspace ?? -1) + 1;
|
||||
return {
|
||||
id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`,
|
||||
model: 'workspace',
|
||||
name: 'Curl Import',
|
||||
model: "workspace",
|
||||
name: "Curl Import",
|
||||
...mergeWith,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user