diff --git a/plugins/importer-postman/src/index.ts b/plugins/importer-postman/src/index.ts index 3c720108..248241d4 100644 --- a/plugins/importer-postman/src/index.ts +++ b/plugins/importer-postman/src/index.ts @@ -30,6 +30,19 @@ const OAUTH1_SIGNATURE_METHODS: Record = { }; const DEFAULT_OAUTH1_SIGNATURE_METHOD = "HMAC-SHA1"; +// Postman's dynamic variables that a Yaak template function reproduces exactly. +// The faker-backed ones ({{$randomFirstName}} and friends) have no equivalent and +// are left alone. Arguments have to be single-quoted; the template parser reads a +// double-quoted call as raw text. +const POSTMAN_DYNAMIC_VARIABLES: Record = { + $guid: "uuid.v4()", + $randomUUID: "uuid.v4()", + $timestamp: "timestamp.unix()", + $isoTimestamp: "timestamp.iso8601()", + // Postman's $randomInt is an integer from 0 to 1000 + $randomInt: "random.range(min='0',max='1000',decimals='0')", +}; + type AtLeast = Partial & Pick; interface ExportResources { @@ -604,10 +617,14 @@ function importDescription(rawDescription: unknown): string | undefined { /** Recursively render all nested object properties */ function convertTemplateSyntax(obj: T): T { if (typeof obj === "string") { - return obj.replace( - /{{\s*(_\.)?([^}]*)\s*}}/g, - (_m, _dot, expr) => `\${[${expr.trim().replace(/^vault:/, "")}]}`, - ) as T; + return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => { + const name = String(expr).trim(); + // hasOwn, so a collection using {{constructor}} doesn't reach Object.prototype + if (Object.hasOwn(POSTMAN_DYNAMIC_VARIABLES, name)) { + return `\${[${POSTMAN_DYNAMIC_VARIABLES[name]}]}`; + } + return `\${[${name.replace(/^vault:/, "")}]}`; + }) as T; } if (Array.isArray(obj) && obj != null) { return obj.map(convertTemplateSyntax) as T; diff --git a/plugins/importer-postman/tests/fixtures/dynamic-variables.input.json b/plugins/importer-postman/tests/fixtures/dynamic-variables.input.json new file mode 100644 index 00000000..1e29247a --- /dev/null +++ b/plugins/importer-postman/tests/fixtures/dynamic-variables.input.json @@ -0,0 +1,67 @@ +{ + "info": { + "_postman_id": "5c0ffee0-dyn0-4var-9abc-000000000001", + "name": "Dynamic Variables", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Dynamic Variables", + "request": { + "method": "POST", + "header": [ + { + "key": "X-Request-Id", + "value": "{{$guid}}" + }, + { + "key": "X-Correlation-Id", + "value": "{{$randomUUID}}" + }, + { + "key": "X-Sent-At", + "value": "{{$timestamp}}" + }, + { + "key": "X-Sent-At-Iso", + "value": "{{$isoTimestamp}}" + }, + { + "key": "X-Spaced", + "value": "{{ $guid }}" + }, + { + "key": "X-Faker", + "value": "{{$randomFirstName}}" + }, + { + "key": "X-Plain", + "value": "{{base_url}}" + } + ], + "url": { + "raw": "https://yaak.app/x/echo?n={{$randomInt}}", + "protocol": "https", + "host": ["yaak", "app"], + "path": ["x", "echo"], + "query": [ + { + "key": "n", + "value": "{{$randomInt}}" + } + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"{{$guid}}\",\n \"at\": {{$timestamp}},\n \"mixed\": \"{{$guid}}/{{base_url}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "response": [] + } + ] +} diff --git a/plugins/importer-postman/tests/fixtures/dynamic-variables.output.json b/plugins/importer-postman/tests/fixtures/dynamic-variables.output.json new file mode 100644 index 00000000..a489a62d --- /dev/null +++ b/plugins/importer-postman/tests/fixtures/dynamic-variables.output.json @@ -0,0 +1,95 @@ +{ + "resources": { + "workspaces": [ + { + "model": "workspace", + "id": "GENERATE_ID::WORKSPACE_1", + "name": "Dynamic Variables", + "authenticationType": null, + "authentication": {} + } + ], + "environments": [ + { + "model": "environment", + "id": "GENERATE_ID::ENVIRONMENT_1", + "name": "Global Variables", + "workspaceId": "GENERATE_ID::WORKSPACE_1", + "parentModel": "workspace", + "parentId": null, + "variables": [] + } + ], + "httpRequests": [ + { + "model": "http_request", + "id": "GENERATE_ID::HTTP_REQUEST_15", + "workspaceId": "GENERATE_ID::WORKSPACE_1", + "folderId": null, + "name": "Dynamic Variables", + "method": "POST", + "url": "https://yaak.app/x/echo", + "urlParameters": [ + { + "name": "n", + "value": "${[random.range(min='0',max='1000',decimals='0')]}", + "enabled": true + } + ], + "body": { + "text": "{\n \"id\": \"${[uuid.v4()]}\",\n \"at\": ${[timestamp.unix()]},\n \"mixed\": \"${[uuid.v4()]}/${[base_url]}\"\n}" + }, + "bodyType": "application/json", + "sortPriority": 0, + "headers": [ + { + "name": "X-Request-Id", + "value": "${[uuid.v4()]}", + "enabled": true + }, + { + "name": "X-Correlation-Id", + "value": "${[uuid.v4()]}", + "enabled": true + }, + { + "name": "X-Sent-At", + "value": "${[timestamp.unix()]}", + "enabled": true + }, + { + "name": "X-Sent-At-Iso", + "value": "${[timestamp.iso8601()]}", + "enabled": true + }, + { + "name": "X-Spaced", + "value": "${[uuid.v4()]}", + "enabled": true + }, + { + "name": "X-Faker", + "value": "${[$randomFirstName]}", + "enabled": true + }, + { + "name": "X-Plain", + "value": "${[base_url]}", + "enabled": true + }, + { + "name": "Content-Type", + "value": "application/json", + "enabled": true + } + ], + "authenticationType": null, + "authentication": {} + } + ], + "folders": [] + }, + "sourceKeys": { + "GENERATE_ID::WORKSPACE_1": "collection:5c0ffee0-dyn0-4var-9abc-000000000001" + } +} diff --git a/plugins/importer-postman/tests/fixtures/nested.output.json b/plugins/importer-postman/tests/fixtures/nested.output.json index 1b8bb8cb..8b888e31 100644 --- a/plugins/importer-postman/tests/fixtures/nested.output.json +++ b/plugins/importer-postman/tests/fixtures/nested.output.json @@ -3,7 +3,7 @@ "workspaces": [ { "model": "workspace", - "id": "GENERATE_ID::WORKSPACE_1", + "id": "GENERATE_ID::WORKSPACE_2", "name": "New Collection", "authenticationType": null, "authentication": {} @@ -12,9 +12,9 @@ "environments": [ { "model": "environment", - "id": "GENERATE_ID::ENVIRONMENT_1", + "id": "GENERATE_ID::ENVIRONMENT_2", "name": "Global Variables", - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "parentModel": "workspace", "parentId": null, "variables": [] @@ -23,8 +23,8 @@ "httpRequests": [ { "model": "http_request", - "id": "GENERATE_ID::HTTP_REQUEST_15", - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "id": "GENERATE_ID::HTTP_REQUEST_16", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "folderId": "GENERATE_ID::FOLDER_2", "name": "Request 1", "method": "GET", @@ -39,8 +39,8 @@ }, { "model": "http_request", - "id": "GENERATE_ID::HTTP_REQUEST_16", - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "id": "GENERATE_ID::HTTP_REQUEST_17", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "folderId": "GENERATE_ID::FOLDER_1", "name": "Request 2", "method": "GET", @@ -55,8 +55,8 @@ }, { "model": "http_request", - "id": "GENERATE_ID::HTTP_REQUEST_17", - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "id": "GENERATE_ID::HTTP_REQUEST_18", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "folderId": null, "name": "Request 3", "method": "GET", @@ -74,7 +74,7 @@ { "model": "folder", "sortPriority": 0, - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "id": "GENERATE_ID::FOLDER_1", "name": "Top Folder", "folderId": null, @@ -84,7 +84,7 @@ { "model": "folder", "sortPriority": 1, - "workspaceId": "GENERATE_ID::WORKSPACE_1", + "workspaceId": "GENERATE_ID::WORKSPACE_2", "id": "GENERATE_ID::FOLDER_2", "name": "Nested Folder", "folderId": "GENERATE_ID::FOLDER_1", @@ -94,6 +94,6 @@ ] }, "sourceKeys": { - "GENERATE_ID::WORKSPACE_1": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d" + "GENERATE_ID::WORKSPACE_2": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d" } } diff --git a/plugins/importer-postman/tests/fixtures/params.output.json b/plugins/importer-postman/tests/fixtures/params.output.json index 914834b5..ca9cf9bf 100644 --- a/plugins/importer-postman/tests/fixtures/params.output.json +++ b/plugins/importer-postman/tests/fixtures/params.output.json @@ -3,7 +3,7 @@ "workspaces": [ { "model": "workspace", - "id": "GENERATE_ID::WORKSPACE_2", + "id": "GENERATE_ID::WORKSPACE_3", "name": "New Collection", "authenticationType": "basic", "authentication": { @@ -15,9 +15,9 @@ "environments": [ { "model": "environment", - "id": "GENERATE_ID::ENVIRONMENT_2", + "id": "GENERATE_ID::ENVIRONMENT_3", "name": "Global Variables", - "workspaceId": "GENERATE_ID::WORKSPACE_2", + "workspaceId": "GENERATE_ID::WORKSPACE_3", "parentModel": "workspace", "parentId": null, "variables": [ @@ -31,8 +31,8 @@ "httpRequests": [ { "model": "http_request", - "id": "GENERATE_ID::HTTP_REQUEST_18", - "workspaceId": "GENERATE_ID::WORKSPACE_2", + "id": "GENERATE_ID::HTTP_REQUEST_19", + "workspaceId": "GENERATE_ID::WORKSPACE_3", "folderId": null, "name": "Form URL", "method": "POST", @@ -102,6 +102,6 @@ "folders": [] }, "sourceKeys": { - "GENERATE_ID::WORKSPACE_2": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d" + "GENERATE_ID::WORKSPACE_3": "collection:9e6dfada-256c-49ea-a38f-7d1b05b7ca2d" } } diff --git a/plugins/importer-postman/tests/index.test.ts b/plugins/importer-postman/tests/index.test.ts index 38feec49..90fc2119 100644 --- a/plugins/importer-postman/tests/index.test.ts +++ b/plugins/importer-postman/tests/index.test.ts @@ -154,6 +154,35 @@ describe("importer-postman", () => { ]); }); + test("Leaves {{constructor}} alone instead of reaching Object.prototype", () => { + const result = convertPostman( + JSON.stringify({ + info: { + name: "Prototype Key", + schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + }, + item: [ + { + name: "Request", + request: { + method: "GET", + url: "https://yaak.app", + header: [ + { key: "X-A", value: "{{constructor}}" }, + { key: "X-B", value: "{{toString}}" }, + ], + }, + }, + ], + }), + ); + + expect(result?.resources.httpRequests[0]?.headers).toEqual([ + { name: "X-A", value: "${[constructor]}", enabled: true }, + { name: "X-B", value: "${[toString]}", enabled: true }, + ]); + }); + test("Omits keys for items the collection never identified", () => { const result = convertPostman( JSON.stringify({