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({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", }), ], }, }); }); test("Explicit URL", () => { expect(convertCurl("curl --url https://yaak.app")).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", }), ], }, }); }); test("Missing URL", () => { expect(convertCurl("curl -X POST")).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ method: "POST", }), ], }, }); }); test("URL between", () => { expect(convertCurl("curl -v https://yaak.app -X POST")).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", }), ], }, }); }); test("Random flags", () => { expect(convertCurl("curl --random -Z -Y -S --foo https://yaak.app")).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", }), ], }, }); }); test("Imports --request method", () => { expect(convertCurl("curl --request POST https://yaak.app")).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", }), ], }, }); }); 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", }), ], }, }); }); 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" }), ], }, }); }); 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" })], }, }); }); 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( { 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", 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", () => { 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", headers: [ { 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 }, ], }, }), ], }, }); }); test("Imports data params as text", () => { expect( 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" }, }), ], }, }); }); 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", urlParameters: [ { enabled: true, name: "limit", value: "3", }, ], }), ], }, }); }); test("Imports multi-line JSON", () => { expect( convertCurl( `curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`, ), ).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ method: "POST", url: "https://yaak.app", headers: [{ name: "Content-Type", value: "application/json", enabled: true }], bodyType: "application/json", body: { text: '{\n "foo":"bar"\n}' }, }), ], }, }); }); test("Imports multiple headers", () => { expect( 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", headers: [ { name: "Name", value: "", enabled: true }, { name: "Foo", value: "bar", enabled: true }, { name: "AAA", value: "bbb", enabled: true }, { name: "", value: "ccc", enabled: true }, ], }), ], }, }); }); 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", authentication: { username: "user", password: "pass", }, }), ], }, }); }); 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", authentication: { username: "user", password: "pass", }, }), ], }, }); }); 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 }], }), ], }, }); }); 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", urlParameters: [ { name: "foo", value: "bar", enabled: true }, { name: "baz", value: "qux", enabled: true }, ], }), ], }, }); }); 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", urlParameters: [ { name: "foo", value: "bar", enabled: true }, { name: "baz", value: "a a", enabled: true }, ], }), ], }, }); }); 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", body: { form: [{ name: "foo", value: "bar=baz", enabled: true }], }, headers: [ { enabled: true, name: "Content-Type", value: "application/x-www-form-urlencoded", }, ], }), ], }, }); }); 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`, ), ).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", headers: [{ name: "Content-Type", value: "application/json", enabled: true }], bodyType: "application/json", body: { text: '{"query":"SearchQueryInput!"}' }, }), ], }, }); }); test("Imports data with multiple escape sequences", () => { expect( convertCurl( `curl 'https://yaak.app' --data-raw $'Line1\\nLine2\\tTab\\u0021Exclamation' -X POST`, ), ).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", bodyType: "application/x-www-form-urlencoded", body: { form: [{ name: "Line1\nLine2\tTab!Exclamation", value: "", enabled: true }], }, headers: [ { enabled: true, name: "Content-Type", value: "application/x-www-form-urlencoded", }, ], }), ], }, }); }); 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' \ --data-raw $'------WebKitFormBoundaryHwsXKi4rKA6P5VBd\r\nContent-Disposition: form-data; name="username"\r\n\r\njsgj\r\n------WebKitFormBoundaryHwsXKi4rKA6P5VBd\r\nContent-Disposition: form-data; name="password"\r\n\r\n654321\r\n------WebKitFormBoundaryHwsXKi4rKA6P5VBd\r\nContent-Disposition: form-data; name="captcha"; filename="test.xlsx"\r\nContent-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n\r\n\r\n------WebKitFormBoundaryHwsXKi4rKA6P5VBd--\r\n'`; expect(convertCurl(curlCommand)).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "http://localhost:8080/system", method: "POST", headers: [ { name: "Content-Type", value: "multipart/form-data; boundary=----WebKitFormBoundaryHwsXKi4rKA6P5VBd", enabled: true, }, ], 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 }, ], }, }), ], }, }); }); 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`, ), ).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ 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}' }, }), ], }, }); }); 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({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", bodyType: "application/x-www-form-urlencoded", body: { form: [{ name: "C:\\", value: "", enabled: true }], }, headers: [ { name: "Content-Type", value: "application/x-www-form-urlencoded", enabled: true, }, ], }), baseRequest({ url: "https://example.com" }), ], }, }); }); 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({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", method: "POST", bodyType: "application/x-www-form-urlencoded", body: { form: [{ name: "C:\\", value: "", enabled: true }], }, headers: [ { name: "Content-Type", value: "application/x-www-form-urlencoded", enabled: true, }, ], }), 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({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "https://yaak.app", headers: [{ name: "X-Custom", value: "it's a test", enabled: true }], }), ], }, }); }); 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({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ 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", () => { 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'`; expect(convertCurl(curlCommand)).toEqual({ resources: { workspaces: [baseWorkspace()], httpRequests: [ baseRequest({ url: "http://example.com/api", method: "POST", headers: [ { name: "Content-Type", value: "multipart/form-data; boundary=----FormBoundary123", enabled: true, }, ], bodyType: "multipart/form-data", body: { form: [ { name: "field1", value: "value1", enabled: true }, { name: "field2", value: "value2", enabled: true }, ], }, }), ], }, }); }); }); const idCount: Partial> = {}; function baseRequest(mergeWith: Partial) { idCount.http_request = (idCount.http_request ?? -1) + 1; return { id: `GENERATE_ID::HTTP_REQUEST_${idCount.http_request}`, model: "http_request", authentication: {}, authenticationType: null, body: {}, bodyType: null, folderId: null, headers: [], method: "GET", name: "", sortPriority: 0, url: "", urlParameters: [], workspaceId: `GENERATE_ID::WORKSPACE_${idCount.workspace}`, ...mergeWith, }; } function baseWorkspace(mergeWith: Partial = {}) { idCount.workspace = (idCount.workspace ?? -1) + 1; return { id: `GENERATE_ID::WORKSPACE_${idCount.workspace}`, model: "workspace", name: "Curl Import", ...mergeWith, }; }