From dfa951e574c94903a4c0b42232aae266ee8d934a Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Thu, 8 May 2025 21:51:21 +0530 Subject: [PATCH] Feature: postman to bru translator (#4534) --- .gitignore | 3 +- package-lock.json | 397 +++++++++- packages/bruno-app/rsbuild.config.mjs | 12 +- .../Sidebar/ImportCollection/index.js | 99 ++- .../src/utils/importers/postman-collection.js | 20 +- packages/bruno-converters/package.json | 2 + packages/bruno-converters/rollup.config.js | 9 +- packages/bruno-converters/src/common/index.js | 1 + packages/bruno-converters/src/index.js | 3 +- .../src/postman/postman-to-bruno.js | 477 ++++++------ .../src/postman/postman-translations.js | 46 +- .../src/utils/jscode-shift-translator.js | 680 ++++++++++++++++++ .../src/workers/postman-translator-worker.js | 211 ++++++ .../scripts/translate-postman-scripts.js | 44 ++ .../postman-to-bruno/collection-auth.spec.js | 18 +- .../postman-to-bruno/folder-auth.spec.js | 16 +- .../postman-to-bruno/postman-to-bruno.spec.js | 2 +- .../postman-response.spec.js | 2 +- .../postman-to-bruno/request-auth.spec.js | 12 +- .../postman-comments.spec.js | 2 +- .../transpiler-tests/combined.test.js | 418 +++++++++++ .../transpiler-tests/environment.test.js | 242 +++++++ .../transpiler-tests/exec-flow.test.js | 64 ++ .../legacy-tests-syntax.test.js | 283 ++++++++ .../transpiler-tests/multiline-syntax.test.js | 283 ++++++++ .../postman-references.test.js | 132 ++++ .../transpiler-tests/request.test.js | 108 +++ .../transpiler-tests/response.test.js | 489 +++++++++++++ .../transpiler-tests/scoped-variables.test.js | 51 ++ .../testing-framework.test.js | 399 ++++++++++ .../variable-chaining.test.js | 91 +++ .../transpiler-tests/variables.test.js | 128 ++++ .../utils/getMemberExpressionString.test.js | 53 ++ packages/bruno-electron/package.json | 1 + packages/bruno-electron/src/ipc/collection.js | 15 + packages/bruno-tests/collection/bruno.json | 2 +- .../collection/echo/echo headers.bru | 22 + 37 files changed, 4529 insertions(+), 308 deletions(-) create mode 100644 packages/bruno-converters/src/utils/jscode-shift-translator.js create mode 100644 packages/bruno-converters/src/workers/postman-translator-worker.js create mode 100644 packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js create mode 100644 packages/bruno-converters/tests/utils/getMemberExpressionString.test.js create mode 100644 packages/bruno-tests/collection/echo/echo headers.bru diff --git a/.gitignore b/.gitignore index 0da494ea2..b97cd17e3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ yarn-error.log* #dev editor bruno.iml -.idea \ No newline at end of file +.idea +.vscode \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c0f7d3617..0b45baf9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1703,9 +1703,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2007,6 +2007,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", + "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", @@ -2192,7 +2207,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2483,6 +2497,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", + "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-flow": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", @@ -2957,7 +2987,6 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -3119,6 +3148,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-flow": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.25.9.tgz", + "integrity": "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -3137,7 +3183,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3153,6 +3198,56 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/register": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz", + "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", @@ -8195,6 +8290,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -8217,6 +8313,7 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -8227,6 +8324,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -8440,6 +8538,66 @@ "node": ">=6.0" } }, + "node_modules/@web/rollup-plugin-copy": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@web/rollup-plugin-copy/-/rollup-plugin-copy-0.5.1.tgz", + "integrity": "sha512-crDMXiT/Okn5nVQIEtXShB3NmGS9gQipD1s4kZwrTG+lu5TV665gfDzMFBzezo1e7ARTGd4d4p4EB0gy/JuNJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@web/rollup-plugin-copy/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -9166,6 +9324,18 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -11033,7 +11203,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", @@ -11180,7 +11349,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, "license": "MIT" }, "node_modules/compare-version": { @@ -13098,6 +13266,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14401,6 +14570,124 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/find-cache-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -15922,7 +16209,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -17541,6 +17827,71 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/jscodeshift": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.3.0.tgz", + "integrity": "sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.7", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jscodeshift/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -17863,7 +18214,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -19697,7 +20047,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/new-github-issue-url": { @@ -20838,7 +21187,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -22784,6 +23132,22 @@ "node": ">=8.10.0" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -24293,7 +24657,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" @@ -24605,7 +24968,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -24624,7 +24986,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -25840,7 +26201,6 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -26109,7 +26469,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -28795,6 +29155,7 @@ "dependencies": { "@usebruno/schema": "^0.7.0", "js-yaml": "^4.1.0", + "jscodeshift": "^17.3.0", "lodash": "^4.17.21", "nanoid": "3.3.8" }, @@ -28805,6 +29166,7 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@web/rollup-plugin-copy": "^0.5.1", "babel-jest": "^29.7.0", "rimraf": "^5.0.7", "rollup": "3.2.5", @@ -28919,6 +29281,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@faker-js/faker": "^9.5.1", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", diff --git a/packages/bruno-app/rsbuild.config.mjs b/packages/bruno-app/rsbuild.config.mjs index 25f5b7ae7..0a2e9081f 100644 --- a/packages/bruno-app/rsbuild.config.mjs +++ b/packages/bruno-app/rsbuild.config.mjs @@ -19,7 +19,7 @@ export default defineConfig({ }) ], source: { - tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file + tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file, }, html: { title: 'Bruno' @@ -34,6 +34,16 @@ export default defineConfig({ }, }, }, + ignoreWarnings: [ + (warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser') + ], + // Add externals configuration to exclude Node.js libraries + externals: { + // List specific Node.js modules you want to exclude + // Format: 'module-name': 'commonjs module-name' + 'worker_threads': 'commonjs worker_threads', + // 'path': 'commonjs path' + } }, } }); diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index 3b3c6aeaa..0b71125c8 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -1,34 +1,43 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { IconLoader2 } from '@tabler/icons'; import importBrunoCollection from 'utils/importers/bruno-collection'; -import importPostmanCollection from 'utils/importers/postman-collection'; +import { postmanToBruno, readFile } from 'utils/importers/postman-collection'; import importInsomniaCollection from 'utils/importers/insomnia-collection'; import importOpenapiCollection from 'utils/importers/openapi-collection'; import { toastError } from 'utils/common/error'; import Modal from 'components/Modal'; +import fileDialog from 'file-dialog'; const ImportCollection = ({ onClose, handleSubmit }) => { + const [isLoading, setIsLoading] = useState(false) + const handleImportBrunoCollection = () => { importBrunoCollection() .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'Import collection failed')); + .catch((err) => toastError(err, 'Import collection failed')) }; + const handleImportPostmanCollection = () => { - importPostmanCollection() - .then(({ collection }) => { - handleSubmit({ collection }); + fileDialog({ accept: 'application/json' }) + .then((...args) => { + setIsLoading(true); + return readFile(...args); }) - .catch((err) => toastError(err, 'Postman Import collection failed')); - }; + .then((collection) => postmanToBruno(collection)) + .then((collection) => handleSubmit({ collection })) + .catch((err) => toastError(err, 'Postman Import collection failed')) + .finally(() => setIsLoading(false)); + } const handleImportInsomniaCollection = () => { importInsomniaCollection() .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'Insomnia Import collection failed')); + .catch((err) => toastError(err, 'Insomnia Import collection failed')) }; const handleImportOpenapiCollection = () => { @@ -36,8 +45,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => { .then(({ collection }) => { handleSubmit({ collection }); }) - .catch((err) => toastError(err, 'OpenAPI v3 Import collection failed')); + .catch((err) => toastError(err, 'OpenAPI v3 Import collection failed')) }; + const CollectionButton = ({ children, className, onClick }) => { return ( ); }; - return ( - -
-

Select the type of your existing collection :

-
- Bruno Collection - Postman Collection - Insomnia Collection - OpenAPI V3 Spec + + const FullscreenLoader = () => { + const [loadingMessage, setLoadingMessage] = useState(''); + + // Messages to cycle through while loading + const loadingMessages = [ + 'Processing collection...', + 'Analyzing requests...', + 'Translating scripts...', + 'Preparing collection...', + 'Almost done...' + ]; + + + // Cycle through loading messages for better UX + useEffect(() => { + if (!isLoading) return; + + let messageIndex = 0; + const interval = setInterval(() => { + messageIndex = (messageIndex + 1) % loadingMessages.length; + setLoadingMessage(loadingMessages[messageIndex]); + }, 2000); + + setLoadingMessage(loadingMessages[0]); + + return () => clearInterval(interval); + }, [isLoading]); + + return ( +
+
+ +

+ {loadingMessage} +

+

+ This may take a moment depending on the collection size +

- + ); + }; + + return ( + <> + {isLoading && } + {!isLoading && ( + +
+

Select the type of your existing collection :

+
+ Bruno Collection + Postman Collection + Insomnia Collection + OpenAPI V3 Spec +
+
+
+ )} + ); }; diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 75db9aaee..b9cceed65 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -1,6 +1,5 @@ import fileDialog from 'file-dialog'; import { BrunoError } from 'utils/common/error'; -import { postmanToBruno } from '@usebruno/converters'; import { safeParseJSON } from 'utils/common/index'; const readFile = (files) => { @@ -12,18 +11,15 @@ const readFile = (files) => { }); }; - -const importCollection = () => { +const postmanToBruno = (collection) => { return new Promise((resolve, reject) => { - fileDialog({ accept: 'application/json' }) - .then(readFile) - .then((collection) => postmanToBruno(collection)) - .then((collection) => resolve({ collection })) - .catch((err) => { - console.log(err); - reject(new BrunoError('Import collection failed')); - }) + window.ipcRenderer.invoke('renderer:convert-postman-to-bruno', collection) + .then(result => resolve(result)) + .catch(err => { + console.error('Error converting Postman to Bruno via Electron:', err); + reject(new BrunoError('Conversion failed')); + }); }); }; -export default importCollection; +export { postmanToBruno, readFile }; diff --git a/packages/bruno-converters/package.json b/packages/bruno-converters/package.json index 3c4051849..b942dea98 100644 --- a/packages/bruno-converters/package.json +++ b/packages/bruno-converters/package.json @@ -21,6 +21,7 @@ "dependencies": { "@usebruno/schema": "^0.7.0", "js-yaml": "^4.1.0", + "jscodeshift": "^17.3.0", "lodash": "^4.17.21", "nanoid": "3.3.8" }, @@ -31,6 +32,7 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^9.0.2", + "@web/rollup-plugin-copy": "^0.5.1", "babel-jest": "^29.7.0", "rimraf": "^5.0.7", "rollup": "3.2.5", diff --git a/packages/bruno-converters/rollup.config.js b/packages/bruno-converters/rollup.config.js index d0c0aad44..ec9a7a4c9 100644 --- a/packages/bruno-converters/rollup.config.js +++ b/packages/bruno-converters/rollup.config.js @@ -2,6 +2,7 @@ const { nodeResolve } = require('@rollup/plugin-node-resolve'); const commonjs = require('@rollup/plugin-commonjs'); const { terser } = require('rollup-plugin-terser'); const peerDepsExternal = require('rollup-plugin-peer-deps-external'); +const { copy } = require('@web/rollup-plugin-copy'); const packageJson = require('./package.json'); const alias = require('@rollup/plugin-alias'); @@ -12,12 +13,12 @@ module.exports = [ input: 'src/index.js', output: [ { - file: packageJson.main, + dir: path.dirname(packageJson.main), format: 'cjs', sourcemap: true }, { - file: packageJson.module, + dir: path.dirname(packageJson.module), format: 'esm', sourcemap: true } @@ -32,6 +33,10 @@ module.exports = [ terser(), alias({ entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }] + }), + copy({ + patterns: 'src/workers/scripts/**/*', + rootDir: '.' }) ] } diff --git a/packages/bruno-converters/src/common/index.js b/packages/bruno-converters/src/common/index.js index c2a3d76aa..bc8c32cb4 100644 --- a/packages/bruno-converters/src/common/index.js +++ b/packages/bruno-converters/src/common/index.js @@ -47,6 +47,7 @@ export const validateSchema = (collection = {}) => { collectionSchema.validateSync(collection); return collection; } catch (err) { + console.log("Error validating schema", err); throw new Error('The Collection has an invalid schema'); } }; diff --git a/packages/bruno-converters/src/index.js b/packages/bruno-converters/src/index.js index fa89457ed..d5b3d3a3b 100644 --- a/packages/bruno-converters/src/index.js +++ b/packages/bruno-converters/src/index.js @@ -2,4 +2,5 @@ export { default as postmanToBruno } from './postman/postman-to-bruno.js'; export { default as postmanToBrunoEnvironment } from './postman/postman-env-to-bruno-env.js'; export { default as brunoToPostman } from './postman/bruno-to-postman.js'; export { default as openApiToBruno } from './openapi/openapi-to-bruno.js'; -export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js'; \ No newline at end of file +export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js'; +export { default as postmanTranslation } from './postman/postman-translations.js'; \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 6eb832c24..c14e12172 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -93,17 +93,10 @@ const importScriptsFromEvents = (events, requestObject) => { requestObject.script = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - requestObject.script.req = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - requestObject.script.req = ''; - } - } else if (typeof event.script.exec === 'string') { - requestObject.script.req = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + requestObject.script.req = postmanTranslation(event.script.exec) } else { + requestObject.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } @@ -113,17 +106,10 @@ const importScriptsFromEvents = (events, requestObject) => { requestObject.tests = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - requestObject.tests = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - requestObject.tests = ''; - } - } else if (typeof event.script.exec === 'string') { - requestObject.tests = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + requestObject.tests = postmanTranslation(event.script.exec) } else { + requestObject.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } @@ -246,13 +232,13 @@ const processAuth = (auth, requestObject) => { } }; -const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { +const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorkers = false } = {}, scriptMap)=> { brunoParent.items = brunoParent.items || []; const folderMap = {}; const requestMap = {}; const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] - each(item, (i, index) => { + item.forEach((i, index) => { if (isItemAFolder(i)) { const baseFolderName = i.name || 'Untitled Folder'; let folderName = baseFolderName; @@ -292,6 +278,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { } }; + brunoParent.items.push(brunoFolderItem); + // Folder level auth if (i.auth) { processAuth(i.auth, brunoFolderItem.root.request); @@ -301,222 +289,221 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { } if (i.item && i.item.length) { - importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth); + importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, { useWorkers }, scriptMap); } if (i.event) { - importScriptsFromEvents(i.event, brunoFolderItem.root.request); + if(useWorkers) { + scriptMap.set(brunoFolderItem.uid, { + events: i.event, + request: brunoFolderItem.root.request + }); + } else { + importScriptsFromEvents(i.event, brunoFolderItem.root.request); + } } - brunoParent.items.push(brunoFolderItem); folderMap[folderName] = brunoFolderItem; - } else { - if (i.request) { - if (!requestMethods.includes(i?.request?.method.toUpperCase())) { - console.warn('Unexpected request.method', i?.request?.method); - return; + } else if (i.request) { + if (!requestMethods.includes(i?.request?.method.toUpperCase())) { + console.warn('Unexpected request.method', i?.request?.method); + return; + } + + const baseRequestName = i.name || 'Untitled Request'; + let requestName = baseRequestName; + let count = 1; + + while (requestMap[requestName]) { + requestName = `${baseRequestName}_${count}`; + count++; + } + + const url = constructUrl(i.request.url); + + const brunoRequestItem = { + uid: uuid(), + name: requestName, + type: 'http-request', + seq: index + 1, + request: { + url: url, + method: i?.request?.method?.toUpperCase(), + auth: { + mode: 'none', + basic: null, + bearer: null, + awsv4: null, + apikey: null, + oauth2: null, + digest: null + }, + headers: [], + params: [], + body: { + mode: 'none', + json: null, + text: null, + xml: null, + formUrlEncoded: [], + multipartForm: [] + }, + docs: i.request.description || '' } + }; - const baseRequestName = i.name || 'Untitled Request'; - let requestName = baseRequestName; - let count = 1; + brunoParent.items.push(brunoRequestItem); - while (requestMap[requestName]) { - requestName = `${baseRequestName}_${count}`; - count++; - } - - const url = constructUrl(i.request.url); - - const brunoRequestItem = { - uid: uuid(), - name: requestName, - type: 'http-request', - seq: index + 1, - request: { - url: url, - method: i?.request?.method?.toUpperCase(), - auth: { - mode: 'none', - basic: null, - bearer: null, - awsv4: null, - apikey: null, - oauth2: null, - digest: null - }, - headers: [], - params: [], - body: { - mode: 'none', - json: null, - text: null, - xml: null, - formUrlEncoded: [], - multipartForm: [] - }, - docs: i.request.description || '' - } - }; - - if (i.event) { + if (i.event) { + if(useWorkers) { + scriptMap.set(brunoRequestItem.uid, { + events: i.event, + request: brunoRequestItem.request + }); + } else { i.event.forEach((event) => { if (event.listen === 'prerequest' && event.script && event.script.exec) { - if (!brunoRequestItem.request.script) { + if (!brunoRequestItem.request?.script) { brunoRequestItem.request.script = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - brunoRequestItem.request.script.req = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - brunoRequestItem.request.script.req = ''; - } - } else if (typeof event.script.exec === 'string') { - brunoRequestItem.request.script.req = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + brunoRequestItem.request.script.req = postmanTranslation(event.script.exec) } else { + brunoRequestItem.request.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } if (event.listen === 'test' && event.script && event.script.exec) { - if (!brunoRequestItem.request.tests) { + if (!brunoRequestItem.request?.tests) { brunoRequestItem.request.tests = {}; } - if (Array.isArray(event.script.exec)) { - if (event.script.exec.length > 0) { - brunoRequestItem.request.tests = event.script.exec - .map((line) => postmanTranslation(line)) - .join('\n'); - } else { - brunoRequestItem.request.tests = ''; - } - } else if (typeof event.script.exec === 'string') { - brunoRequestItem.request.tests = postmanTranslation(event.script.exec); + if (event.script.exec && event.script.exec.length > 0) { + brunoRequestItem.request.tests = postmanTranslation(event.script.exec) } else { + brunoRequestItem.request.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } }); } - - const bodyMode = get(i, 'request.body.mode'); - if (bodyMode) { - if (bodyMode === 'formdata') { - brunoRequestItem.request.body.mode = 'multipartForm'; - - each(i.request.body.formdata, (param) => { - const isFile = param.type === 'file'; - let value; - let type; - - if (isFile) { - // If param.src is an array, keep it as it is. - // If param.src is a string, convert it into an array with a single element. - value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; - type = 'file'; - } else { - value = param.value; - type = 'text'; - } - - brunoRequestItem.request.body.multipartForm.push({ - uid: uuid(), - type: type, - name: param.key, - value: value, - description: param.description, - enabled: !param.disabled - }); - }); - } - - if (bodyMode === 'urlencoded') { - brunoRequestItem.request.body.mode = 'formUrlEncoded'; - each(i.request.body.urlencoded, (param) => { - brunoRequestItem.request.body.formUrlEncoded.push({ - uid: uuid(), - name: param.key, - value: param.value, - description: param.description, - enabled: !param.disabled - }); - }); - } - - if (bodyMode === 'raw') { - let language = get(i, 'request.body.options.raw.language'); - if (!language) { - language = searchLanguageByHeader(i.request.header); - } - if (language === 'json') { - brunoRequestItem.request.body.mode = 'json'; - brunoRequestItem.request.body.json = i.request.body.raw; - } else if (language === 'xml') { - brunoRequestItem.request.body.mode = 'xml'; - brunoRequestItem.request.body.xml = i.request.body.raw; - } else { - brunoRequestItem.request.body.mode = 'text'; - brunoRequestItem.request.body.text = i.request.body.raw; - } - } - } - - if (bodyMode === 'graphql') { - brunoRequestItem.type = 'graphql-request'; - brunoRequestItem.request.body.mode = 'graphql'; - brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql); - } - - each(i.request.header, (header) => { - brunoRequestItem.request.headers.push({ - uid: uuid(), - name: header.key, - value: header.value, - description: header.description, - enabled: !header.disabled - }); - }); - - // Handle request-level auth or inherit from parent - const auth = i.request.auth ?? parentAuth; - processAuth(auth, brunoRequestItem.request); - - each(get(i, 'request.url.query'), (param) => { - brunoRequestItem.request.params.push({ - uid: uuid(), - name: param.key, - value: param.value, - description: param.description, - type: 'query', - enabled: !param.disabled - }); - }); - - each(get(i, 'request.url.variable', []), (param) => { - if (!param.key) { - // If no key, skip this iteration and discard the param - return; - } - - brunoRequestItem.request.params.push({ - uid: uuid(), - name: param.key, - value: param.value ?? '', - description: param.description ?? '', - type: 'path', - enabled: true - }); - }); - - brunoParent.items.push(brunoRequestItem); - requestMap[requestName] = brunoRequestItem; } + + const bodyMode = get(i, 'request.body.mode'); + if (bodyMode) { + if (bodyMode === 'formdata') { + brunoRequestItem.request.body.mode = 'multipartForm'; + + each(i.request.body.formdata, (param) => { + const isFile = param.type === 'file'; + let value; + let type; + + if (isFile) { + // If param.src is an array, keep it as it is. + // If param.src is a string, convert it into an array with a single element. + value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; + type = 'file'; + } else { + value = param.value; + type = 'text'; + } + + brunoRequestItem.request.body.multipartForm.push({ + uid: uuid(), + type: type, + name: param.key, + value: value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if (bodyMode === 'urlencoded') { + brunoRequestItem.request.body.mode = 'formUrlEncoded'; + each(i.request.body.urlencoded, (param) => { + brunoRequestItem.request.body.formUrlEncoded.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if (bodyMode === 'raw') { + let language = get(i, 'request.body.options.raw.language'); + if (!language) { + language = searchLanguageByHeader(i.request.header); + } + if (language === 'json') { + brunoRequestItem.request.body.mode = 'json'; + brunoRequestItem.request.body.json = i.request.body.raw; + } else if (language === 'xml') { + brunoRequestItem.request.body.mode = 'xml'; + brunoRequestItem.request.body.xml = i.request.body.raw; + } else { + brunoRequestItem.request.body.mode = 'text'; + brunoRequestItem.request.body.text = i.request.body.raw; + } + } + } + + if (bodyMode === 'graphql') { + brunoRequestItem.type = 'graphql-request'; + brunoRequestItem.request.body.mode = 'graphql'; + brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql); + } + + each(i.request.header, (header) => { + brunoRequestItem.request.headers.push({ + uid: uuid(), + name: header.key, + value: header.value, + description: header.description, + enabled: !header.disabled + }); + }); + + // Handle request-level auth or inherit from parent + const auth = i.request.auth ?? parentAuth; + processAuth(auth, brunoRequestItem.request); + + each(get(i, 'request.url.query'), (param) => { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + type: 'query', + enabled: !param.disabled + }); + }); + + each(get(i, 'request.url.variable', []), (param) => { + if (!param.key) { + // If no key, skip this iteration and discard the param + return; + } + + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value ?? '', + description: param.description ?? '', + type: 'path', + enabled: true + }); + }); + + requestMap[requestName] = brunoRequestItem; } }); }; + const searchLanguageByHeader = (headers) => { let contentType; each(headers, (header) => { @@ -532,7 +519,7 @@ const searchLanguageByHeader = (headers) => { return contentType; }; -const importPostmanV2Collection = (collection) => { +const importPostmanV2Collection = async (collection, { useWorkers = false }) => { const brunoCollection = { name: collection.info.name || 'Untitled Collection', uid: uuid(), @@ -573,12 +560,74 @@ const importPostmanV2Collection = (collection) => { // Collection level auth processAuth(collection.auth, brunoCollection.root.request); - importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth); - + // Create a single scriptMap for all items + const scriptMap = useWorkers ? new Map() : null; + + importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, { useWorkers }, scriptMap); + + // Process all scripts in a single call at the top level + if (useWorkers && scriptMap && scriptMap.size > 0) { + try { + const { default: scriptTranslationWorker } = await import('../workers/postman-translator-worker'); + const translatedScripts = await scriptTranslationWorker(scriptMap); + + // Apply translated scripts to all items in the collection + const applyScriptsToItems = (items) => { + items.forEach(item => { + if (item.type === 'folder') { + // Apply scripts to the folder + if (translatedScripts.has(item.uid)) { + if (!item.root.request.script) { + item.root.request.script = {}; + } + if (!item.root.request.tests) { + item.root.request.tests = ''; + } + + const script = translatedScripts.get(item.uid).request?.script?.req; + const tests = translatedScripts.get(item.uid).request?.tests; + + item.root.request.script.req = script && script.length > 0 ? script : ''; + item.root.request.tests = tests && tests.length > 0 ? tests : ''; + } + + // Recursively apply to nested items + if (item.items && item.items.length > 0) { + applyScriptsToItems(item.items); + } + } else { + if (translatedScripts.has(item.uid)) { + if (!item.request.script) { + item.request.script = {}; + } + if (!item.request.tests) { + item.request.tests = ''; + } + + const script = translatedScripts.get(item.uid).request?.script?.req; + const tests = translatedScripts.get(item.uid).request?.tests; + + item.request.script.req = script && script.length > 0 ? script : ''; + item.request.tests = tests && tests.length > 0 ? tests : ''; + } + } + }); + }; + + applyScriptsToItems(brunoCollection.items); + + } catch (error) { + console.error('Error in script translation worker:', error); + } finally { + scriptMap.clear(); + } + } + return brunoCollection; }; -const parsePostmanCollection = (collection) => { + +const parsePostmanCollection = async (collection, { useWorkers = false }) => { try { let schema = get(collection, 'info.schema'); @@ -590,7 +639,7 @@ const parsePostmanCollection = (collection) => { ]; if (v2Schemas.includes(schema)) { - return importPostmanV2Collection(collection); + return await importPostmanV2Collection(collection, { useWorkers }); } throw new Error('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.'); @@ -604,9 +653,10 @@ const parsePostmanCollection = (collection) => { } }; -const postmanToBruno = (postmanCollection) => { +const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => { try { - const parsedPostmanCollection = parsePostmanCollection(postmanCollection); + + const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers }); const transformedCollection = transformItemsInCollection(parsedPostmanCollection); const hydratedCollection = hydrateSeqInCollection(transformedCollection); const validatedCollection = validateSchema(hydratedCollection); @@ -617,4 +667,5 @@ const postmanToBruno = (postmanCollection) => { } }; + export default postmanToBruno; \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index b741cd3b2..23195b5b8 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -1,3 +1,5 @@ +import translateCode from '../utils/jscode-shift-translator'; + const replacements = { 'pm\\.environment\\.get\\(': 'bru.getEnvVar(', 'pm\\.environment\\.set\\(': 'bru.setEnvVar(', @@ -20,7 +22,7 @@ const replacements = { 'pm\\.response\\.responseTime': 'res.getResponseTime()', 'pm\\.environment\\.name': 'bru.getEnvName()', 'pm\\.response\\.status': 'res.statusText', - 'pm\\.response\\.headers': 'req.getHeaders()', + 'pm\\.response\\.headers': 'res.getHeaders()', "tests\\['([^']+)'\\]\\s*=\\s*([^;]+);": 'test("$1", function() { expect(Boolean($2)).to.be.true; });', 'pm\\.request\\.url': 'req.getUrl()', 'pm\\.request\\.method': 'req.getMethod()', @@ -48,22 +50,38 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern, replacement })); -const postmanTranslation = (script) => { +const processRegexReplacement = (code) => { + for (const { regex, replacement } of compiledReplacements) { + if (regex.test(code)) { + code = code.replace(regex, replacement); + + } + } + if ((code.includes('pm.') || code.includes('postman.'))) { + code = code.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); + } + return code; +} + + +const postmanTranslation = (script, options = {}) => { + let modifiedScript = Array.isArray(script) ? script.join('\n') : script; + try { - let modifiedScript = script; - let modified = false; - for (const { regex, replacement } of compiledReplacements) { - if (regex.test(modifiedScript)) { - modifiedScript = modifiedScript.replace(regex, replacement); - modified = true; - } + let translatedCode = translateCode(modifiedScript); + if ((translatedCode.includes('pm.') || translatedCode.includes('postman.'))) { + translatedCode = translatedCode.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); } - if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) { - modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); - } - return modifiedScript; + return translatedCode; } catch (e) { - return script; + console.warn('Error in postman translation:', e); + + try { + return processRegexReplacement(modifiedScript); + } catch (e) { + console.warn('Error in postman translation:', e); + return modifiedScript; + } } }; diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js new file mode 100644 index 000000000..924f4eb1d --- /dev/null +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -0,0 +1,680 @@ +const j = require('jscodeshift'); +const cloneDeep = require('lodash/cloneDeep'); + +/** + * Efficiently builds a string representation of a member expression without using toSource() + * + * @param {Object} node - The member expression node from the AST + * @return {string} - String representation of the member expression (e.g., "pm.environment.get") + */ +function getMemberExpressionString(node) { + // Handle base case: if this is an Identifier + if (node.type === 'Identifier') { + return node.name; + } + + // Handle member expressions + if (node.type === 'MemberExpression') { + const objectStr = getMemberExpressionString(node.object); + + // For computed properties like obj[prop], we need special handling + if (node.computed) { + // For literals like obj["prop"], we can include them in the string + if (node.property.type === 'Literal' && typeof node.property.value === 'string') { + return `${objectStr}.${node.property.value}`; + } + // For other computed properties, we can't reliably represent them as a simple string + return `${objectStr}.[computed]`; + } + + // For regular property access like obj.prop + if (node.property.type === 'Identifier') { + return `${objectStr}.${node.property.name}`; + } + } + + return '[unsupported]'; +} + +// Simple 1:1 translations for straightforward replacements +const simpleTranslations = { + // Environment variables + 'pm.environment.get': 'bru.getEnvVar', + 'pm.environment.set': 'bru.setEnvVar', + 'pm.environment.name': 'bru.getEnvName()', + 'pm.environment.unset': 'bru.deleteEnvVar', + + // Variables + 'pm.variables.get': 'bru.getVar', + 'pm.variables.set': 'bru.setVar', + 'pm.variables.has': 'bru.hasVar', + + // Collection variables + 'pm.collectionVariables.get': 'bru.getVar', + 'pm.collectionVariables.set': 'bru.setVar', + 'pm.collectionVariables.has': 'bru.hasVar', + 'pm.collectionVariables.unset': 'bru.deleteVar', + + // Request flow control + 'pm.setNextRequest': 'bru.setNextRequest', + + // Testing + 'pm.test': 'test', + 'pm.expect': 'expect', + 'pm.expect.fail': 'expect.fail', + + // Request properties + 'pm.request.url': 'req.getUrl()', + 'pm.request.method': 'req.getMethod()', + 'pm.request.headers': 'req.getHeaders()', + 'pm.request.body': 'req.getBody()', + + // Response properties + 'pm.response.json': 'res.getBody', + 'pm.response.code': 'res.getStatus()', + 'pm.response.status': 'res.statusText', + 'pm.response.responseTime': 'res.getResponseTime()', + 'pm.response.statusText': 'res.statusText', + 'pm.response.headers': 'res.getHeaders()', + + // Execution control + 'pm.execution.skipRequest': 'bru.runner.skipRequest', + + // Legacy Postman API (deprecated) (we can use pm instead of postman, as we are converting all postman references to pm in the code as the part of pre-processing) + 'pm.setEnvironmentVariable': 'bru.setEnvVar', + 'pm.getEnvironmentVariable': 'bru.getEnvVar', + 'pm.clearEnvironmentVariable': 'bru.deleteEnvVar', +}; + +/* Complex transformations that need custom handling +* Note: Transform functions can return either a single node or an array of nodes. +* When returning an array of nodes, each node in the array will be inserted +* as a separate statement, which allows a single Postman expression to be +* transformed into multiple Bruno statements (e.g. for complex assertions). +*/ +const complexTransformations = [ + // pm.environment.has requires special handling + { + pattern: 'pm.environment.has', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // Create: bru.getEnvVar(arg) !== undefined && bru.getEnvVar(arg) !== null + return j.logicalExpression( + '&&', + j.binaryExpression( + '!==', + j.callExpression(j.identifier('bru.getEnvVar'), args), + j.identifier('undefined') + ), + j.binaryExpression( + '!==', + j.callExpression(j.identifier('bru.getEnvVar'), args), + j.identifier('null') + ) + ); + } + }, + + { + pattern: 'pm.response.text', + transform: (_, j) => { + return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]); + } + }, + + // Handle pm.response.to.have.status + { + pattern: 'pm.response.to.have.status', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // Create: expect(res.getStatus()).to.equal(arg) + return j.callExpression( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('res.getStatus'), + [] + ) + ] + ), + j.identifier('to.equal') + ), + args + ); + } + }, + + // handle 'pm.response.to.have.header' to expect(res.getHeaders()).to.have.property(args) + { + pattern: 'pm.response.to.have.header', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + + if (args.length > 0) { + // Apply toLowerCase() to the first argument + args[0] = j.callExpression( + j.memberExpression( + args[0], + j.identifier('toLowerCase') + ), + [] + ); + } + + // Create: expect(res.getHeaders()).to.have.property(args) + return j.callExpression( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('res.getHeaders'), + [] + ) + ] + ), + j.identifier('to.have.property') + ), + args + ); + + } + }, + + // Handle pm.execution.setNextRequest(null) + { + pattern: 'pm.execution.setNextRequest', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + // If argument is null or 'null', transform to bru.runner.stopExecution() + if ( + args[0].type === 'Literal' && (args[0].value === null || args[0].value === 'null') + ) { + return j.callExpression( + j.identifier('bru.runner.stopExecution'), + [] + ); + } + + // Otherwise, keep as bru.runner.setNextRequest with the same argument + return j.callExpression( + j.identifier('bru.runner.setNextRequest'), + args + ); + } + }, +]; + +// Create a map for complex transformations to enable O(1) lookups +const complexTransformationsMap = {}; +complexTransformations.forEach(transform => { + complexTransformationsMap[transform.pattern] = transform; +}); + +const varInitsToReplace = new Set(['pm', 'postman', 'pm.request','pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution']); + +/** + * Process all transformations (both simple and complex) in the AST in a single pass + * @param {Object} ast - jscodeshift AST + * @param {Set} transformedNodes - Set of already transformed nodes + */ +function processTransformations(ast, transformedNodes) { + ast.find(j.MemberExpression).forEach(path => { + if (transformedNodes.has(path.node)) return; + + // Get string representation using our utility function + const memberExprStr = getMemberExpressionString(path.value); + + // First check for simple transformations (O(1)) + if (simpleTranslations.hasOwnProperty(memberExprStr)) { + const replacement = simpleTranslations[memberExprStr]; + j(path).replaceWith(j.identifier(replacement)); + transformedNodes.add(path.node); + return; // Skip complex transformation check if simple transformation applied + } + + // Then check for complex transformations (O(1)) + if (complexTransformationsMap.hasOwnProperty(memberExprStr) && + path.parent.value.type === 'CallExpression') { + const transform = complexTransformationsMap[memberExprStr]; + const replacement = transform.transform(path, j); + if (Array.isArray(replacement)) { + replacement.forEach((nodePath, index) => { + if(index === 0) { + j(path.parent).replaceWith(nodePath); + } else { + j(path.parent.parent).insertAfter(nodePath); + } + transformedNodes.add(nodePath.node); + transformedNodes.add(path.parent.node); + }); + } else { + j(path.parent).replaceWith(replacement); + transformedNodes.add(path.node); + transformedNodes.add(path.parent.node); + } + } + }); +} + +/** + * Translates Postman script code to Bruno script code + * @param {string} code - The Postman script code to translate + * @returns {string} The translated Bruno script code + */ +function translateCode(code) { + // Replace 'postman' with 'pm' using regex before creating the AST + // This is more efficient than an AST traversal + code = code.replace(/\bpostman\b/g, 'pm'); + + const ast = j(code); + + // Keep track of transformed nodes to avoid double-processing + const transformedNodes = new Set(); + + // Preprocess the code to resolve all aliases + preprocessAliases(ast); + + // Process all transformations in a single pass + processTransformations(ast, transformedNodes); + + // Handle special Postman syntax patterns + handleTestsBracketNotation(ast); + + return ast.toSource(); +} + +/** + * Preprocess all variable aliases in the AST to simplify later transformations + * @param {Object} ast - jscodeshift AST + */ +function preprocessAliases(ast) { + // Create a symbol table to track what each variable references + const symbolTable = new Map(); + const MAX_ITERATIONS = 5; + let iterations = 0; + + // Keep preprocessing until no more changes can be made + let changesMade; + do { + changesMade = false; + + // First pass: Identify all variables + findVariableDefinitions(ast, symbolTable); + + // Second pass: Replace all variable references with their resolved values + changesMade = resolveVariableReferences(ast, symbolTable) || false; + + // Third pass: Clean up variable declarations that are no longer needed + changesMade = removeResolvedDeclarations(ast, symbolTable) || false; + + iterations++; + + } while (changesMade && iterations < MAX_ITERATIONS); +} + +/** + * Find all variable definitions and track what they reference + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map to track variable references + */ +function findVariableDefinitions(ast, symbolTable) { + // Use a single traversal to handle both direct assignments and object destructuring + ast.find(j.VariableDeclarator).forEach(path => { + // Only process nodes that have an initializer + if (!path.value.init) return; + + // Handle direct assignments: const response = pm.response + if (path.value.id.type === 'Identifier') { + const varName = path.value.id.name; + + // If it's a direct identifier, just map it + if (path.value.init.type === 'Identifier') { + symbolTable.set(varName, { + type: 'identifier', + value: path.value.init.name + }); + } + // If it's a member expression, store both parts + else if (path.value.init.type === 'MemberExpression') { + const sourceCode = getMemberExpressionString(path.value.init); + symbolTable.set(varName, { + type: 'memberExpression', + value: sourceCode, + node: path.value.init + }); + } + } + // Handle object destructuring: const { response } = pm + else if (path.value.id.type === 'ObjectPattern' && path.value.init.type === 'Identifier') { + const source = path.value.init.name; + + path.value.id.properties.forEach(prop => { + if (prop.key.name && prop.value.type === 'Identifier') { + const destVarName = prop.value.name; + symbolTable.set(destVarName, { + type: 'memberExpression', + value: `${source}.${prop.key.name}`, + node: j.memberExpression( + j.identifier(source), + j.identifier(prop.key.name) + ) + }); + } + }); + } + }); +} + +/** + * Resolve variable references by replacing them with their original values + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map of variable references + * @returns {boolean} Whether any changes were made + */ +function resolveVariableReferences(ast, symbolTable) { + let changesMade = false; + + /** + * Example of what this function does: + * + * Input Postman code: + * const response = pm.response; + * const jsonData = response.json(); // response is a reference to pm.response + * + * After resolution: + * const response = pm.response; + * const jsonData = pm.response.json(); // response reference is replaced with pm.response + * + * Then in the next preprocessing phase, unnecessary variables like 'response' will be removed. + */ + + // Replace all identifier references with their resolved values + ast.find(j.Identifier).forEach(path => { + const varName = path.value.name; + + /** + * Skip specific types of identifiers that shouldn't be replaced: + * + * Case 1: Variable definitions (left side of declarations) + * ----------------------------------------------------- + * In code like: + * const response = pm.response; + * ^ + * We shouldn't replace 'response' on the left side with pm.response, + * which would result in: const pm.response = pm.response; (invalid syntax) + * + * Case 2: Property names in member expressions + * ----------------------------------------------------- + * In code like: + * console.log(response.status); + * ^ + * We shouldn't replace the 'status' property name with anything, + * only the 'response' object reference should be replaced. + * + * We only want to replace identifiers that are being used as references, + * not the ones being defined or used as property names. + */ + + // Skip if this is a variable definition or property name + if (path.parent.value.type === 'VariableDeclarator' && path.parent.value.id === path.value) { + return; + } + if (path.parent.value.type === 'MemberExpression' && path.parent.value.property === path.value && !path.parent.value.computed) { + return; + } + + // Only replace if this is a known variable + if (!symbolTable.has(varName)) return; + + const symbolInfo = symbolTable.get(varName); + if(!varInitsToReplace.has(symbolInfo.value)) { + return; + } + const newNode = cloneDeep(symbolInfo.node); + j(path).replaceWith(newNode); + symbolTable.set(varName, { + type: 'memberExpression', + value: symbolInfo.value, + node: newNode + }); + changesMade = true; + + }); + + return changesMade; +} + +/** + * Remove variable declarations that have been resolved + * @param {Object} ast - jscodeshift AST + * @param {Map} symbolTable - Map of variable references + * @returns {boolean} Whether any changes were made + */ +function removeResolvedDeclarations(ast, symbolTable) { + let changesMade = false; + + /** + * Example of what this function does: + * + * Original Postman code: + * const response = pm.response; + * const jsonData = response.json(); + * console.log(jsonData.name); + * + * After variable resolution: + * const response = pm.response; // This declaration is now redundant + * const jsonData = pm.response.json(); // This value has been resolved + * console.log(jsonData.name); // This still references jsonData + * + * Final code after this cleanup step: + * const jsonData = pm.response.json(); // response variable declaration is removed + * console.log(jsonData.name); // jsonData is kept since it's still referenced + * + * We only remove declarations that: + * 1. Have been fully resolved (references to pm.* objects) + * 2. No longer provide any value (since all references were replaced with resolved values) + */ + + // Use a single traversal to handle both regular variable declarations and destructuring + ast.find(j.VariableDeclarator).forEach(path => { + // Case 1: Handle regular variable declarations + if (path.value.id.type === 'Identifier') { + const varName = path.value.id.name; + const replacement = symbolTable.get(varName); + if(!replacement || !varInitsToReplace.has(replacement.value)) return; + + /** + * This code differentiates between two types of variable declarations: + * + * Example 1: Single variable declaration + * ----------------------------------- + * Input: const response = pm.response; + * Action: The entire statement can be removed + * Output: [statement removed] + * + * Example 2: Multiple variables in one declaration + * ----------------------------------- + * Input: const response = pm.response, unrelated = 5; + * Action: Only remove the 'response' declarator, keep the others + * Output: const unrelated = 5; + * + * We need this distinction to ensure we don't accidentally remove + * unrelated variables that happen to be declared in the same statement. + */ + const declarationPath = j(path).closest(j.VariableDeclaration); + if (declarationPath.get().value.declarations.length === 1) { + declarationPath.remove(); + } else { + // Otherwise just remove this declarator + j(path).remove(); + } + + changesMade = true; + } + // Case 2: Handle destructuring of pm + else if (path.value.id.type === 'ObjectPattern' && + path.value.init && + path.value.init.type === 'Identifier' && + path.value.init.name === 'pm') { + + /** + * Example of destructuring removal: + * + * Original Postman code: + * const { response, environment } = pm; + * console.log(response.json().name); + * console.log(environment.get("variable")); + * + * After variable resolution steps: + * const { response, environment } = pm; // This destructuring is now redundant + * console.log(pm.response.json().name); // 'response' references already replaced with pm.response + * console.log(pm.environment.get("variable")); // 'environment' references replaced + * + * Final code after this cleanup step: + * console.log(pm.response.json().name); // Destructuring declaration is completely removed + * console.log(pm.environment.get("variable")); + * + * This step specifically targets the Postman pattern of destructuring the pm object, + * which is common in Postman scripts but needs to be removed in the Bruno conversion. + */ + + const declarationPath = j(path).closest(j.VariableDeclaration); + if (declarationPath.get().value.declarations.length === 1) { + declarationPath.remove(); + } else { + j(path).remove(); + } + + changesMade = true; + } + }); + + return changesMade; +} + +/** + * Handle Postman's tests["..."] = ... syntax + * @param {Object} ast - jscodeshift AST + */ +function handleTestsBracketNotation(ast) { + // Find the ExpressionStatement that contains the assignment + ast.find(j.ExpressionStatement, { + expression: { + type: 'AssignmentExpression', + left: { + type: 'MemberExpression', + object: { name: 'tests' }, + computed: true, + property: {} // Accept any property type + } + } + }).forEach(path => { + // Get the assignment expression + const assignment = path.value.expression; + const left = assignment.left; + + // Verify it's a valid tests[] expression + if (left.object.type === 'Identifier' && + left.object.name === 'tests' && + left.computed === true) { + + const property = left.property; + const rightSide = assignment.right; + + // Handle string literals + if (property.type === 'Literal' && typeof property.value === 'string') { + const testName = property.value; + + // Replace with test() function call + j(path).replaceWith( + j.expressionStatement( + j.callExpression( + j.identifier('test'), + [ + j.literal(testName), + j.functionExpression( + null, + [], + j.blockStatement([ + j.expressionStatement( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('Boolean'), + [rightSide] + ) + ] + ), + j.identifier('to.be.true') + ) + ) + ]) + ) + ] + ) + ) + ); + } + // Handle template literals + else if (property.type === 'TemplateLiteral') { + // Create a template literal with the same quasi and expressions + const templateLiteral = j.templateLiteral( + property.quasis, + property.expressions + ); + + // Replace with test() function call using template literal + j(path).replaceWith( + j.expressionStatement( + j.callExpression( + j.identifier('test'), + [ + templateLiteral, + j.functionExpression( + null, + [], + j.blockStatement([ + j.expressionStatement( + j.memberExpression( + j.callExpression( + j.identifier('expect'), + [ + j.callExpression( + j.identifier('Boolean'), + [rightSide] + ) + ] + ), + j.identifier('to.be.true') + ) + ) + ]) + ) + ] + ) + ) + ); + } + } + }); +} + +export { getMemberExpressionString }; +export default translateCode; \ No newline at end of file diff --git a/packages/bruno-converters/src/workers/postman-translator-worker.js b/packages/bruno-converters/src/workers/postman-translator-worker.js new file mode 100644 index 000000000..1c9ebea79 --- /dev/null +++ b/packages/bruno-converters/src/workers/postman-translator-worker.js @@ -0,0 +1,211 @@ +const { Worker } = require('node:worker_threads'); +const path = require('node:path'); +const os = require('node:os'); + +function getMaxWorkers() { + return Math.max(os.availableParallelism(), 1) +} + +class WorkerPool { + constructor(scriptPath, size) { + this.workers = []; + this.idle = []; + this.queue = []; + this.scriptPath = scriptPath; + this.size = size; + } + + // Initialize the worker pool + initialize() { + for (let i = 0; i < this.size; i++) { + const worker = new Worker(this.scriptPath); + this.workers.push(worker); + this.idle.push(i); + } + } + + // Run a task on a worker + runTask(data) { + return new Promise((resolve, reject) => { + const task = { data, resolve, reject }; + + if (this.idle.length > 0) { + this._runTaskOnWorker(this.idle.shift(), task); + } else { + this.queue.push(task); + } + }); + } + + // Run a task on a specific worker + _runTaskOnWorker(workerId, task) { + const worker = this.workers[workerId]; + + const messageHandler = (result) => { + // Cleanup listeners + worker.removeListener('message', messageHandler); + worker.removeListener('error', errorHandler); + + // Mark worker as idle + this.idle.push(workerId); + + // Process queue if tasks are waiting + if (this.queue.length > 0) { + this._runTaskOnWorker(workerId, this.queue.shift()); + } + + // Resolve the task + task.resolve(result); + }; + + const errorHandler = (err) => { + worker.removeListener('message', messageHandler); + worker.removeListener('error', errorHandler); + + this.idle.push(workerId); + + if (this.queue.length > 0) { + this._runTaskOnWorker(workerId, this.queue.shift()); + } + + task.reject(err); + }; + + worker.on('message', messageHandler); + worker.on('error', errorHandler); + worker.postMessage(task.data); + } + + // Terminate all workers + terminate() { + for (const worker of this.workers) { + worker.terminate(); + } + this.workers = []; + this.idle = []; + } +} + +// Helper function to count lines in a script +function countScriptLines(script) { + if (!script) return 0; + return Array.isArray(script) ? script.length : script.split('\n').length; +} + +// Calculate complexity of a script entry +function calculateScriptComplexity([uid, entry]) { + let totalLines = 0; + const { events } = entry + + if (events && Array.isArray(events)) { + events.forEach(({ script }) => { + if (script && script.exec) { + totalLines += countScriptLines(script.exec); + } + }); + } + + return { uid, entry, complexity: totalLines || 1 }; // Minimum complexity of 1 +} + +// Create balanced batches based on script complexity +function createBalancedBatches(scriptEntries, workerCount) { + // Calculate complexity for each script + const scriptsWithComplexity = scriptEntries.map(calculateScriptComplexity); + + // Sort scripts by complexity (descending) + scriptsWithComplexity.sort((a, b) => b.complexity - a.complexity); + + // Initialize batches + const batches = Array.from({ length: workerCount }, () => ({ + entries: [], + totalComplexity: 0 + })); + + // Algorithm: Greedy load balancing + // 1. Process scripts in descending order of complexity + // 2. Always assign each script to the batch with lowest current load + // 3. This minimizes the maximum workload across all workers + for (const { uid, entry, complexity } of scriptsWithComplexity) { + + const batchWithLowestComplexity = batches.reduce( + (target, current) => current.totalComplexity < target.totalComplexity ? current : target + ); + + // Add the script to this batch + batchWithLowestComplexity.entries.push({uid, entry}); + batchWithLowestComplexity.totalComplexity += complexity; + } + + return batches.map(batch => + batch.entries.map(({ uid, entry }) => [uid, entry]) + ).filter(batch => batch.length > 0); +} + +const scriptTranslationWorker = async (scriptMap) => { + // Convert the Map to an array of entries + const scriptEntries = Array.from(scriptMap.entries()); + const maxWorkers = getMaxWorkers(); + + // For very small collections, don't parallelize + if (scriptEntries.length <= 50) { + const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), 1); + workerPool.initialize(); + + try { + const translatedScripts = new Map(); + const result = await workerPool.runTask({ scripts: scriptEntries }); + + if (result.error) { + console.error('Error in script translation worker:', result.error); + throw new Error(result.error); + } + + result.forEach(([uid, { request }]) => { + translatedScripts.set(uid, { request }); + }); + + return translatedScripts; + } finally { + workerPool.terminate(); + } + } + + + const workerCount = Math.min(maxWorkers, 4); + + // Create balanced batches based on script complexity + const batches = createBalancedBatches(scriptEntries, workerCount); + + const translatedScripts = new Map(); + + // Create worker pool with optimal size + const workerPool = new WorkerPool(path.join(__dirname,'./src/workers/scripts/translate-postman-scripts.js'), workerCount); + workerPool.initialize(); + + // Process all batches in parallel using worker pool + const batchPromises = batches.map(batch => { + return workerPool.runTask({ scripts: batch }) + .then(modScripts => { + modScripts.forEach(([name, { request }]) => { + translatedScripts.set(name, { request }); + }); + }) + .catch(err => { + console.error('Error in script translation worker:', err); + throw new Error(err); + }); + }); + + // Wait for all batches to complete + try { + await Promise.allSettled(batchPromises); + } finally { + // Clean up worker pool + workerPool.terminate(); + } + + return translatedScripts; +}; + +export default scriptTranslationWorker \ No newline at end of file diff --git a/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js new file mode 100644 index 000000000..31b7d9008 --- /dev/null +++ b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js @@ -0,0 +1,44 @@ +const { parentPort } = require('node:worker_threads'); +const { postmanTranslation } = require('@usebruno/converters'); + +parentPort.on('message', (workerData) => { + try { + const { scripts } = workerData; + const modScripts = scripts.map(([uid, { events }]) => { + const requestObject = { + script: {}, + tests: {} + } + + if (events && Array.isArray(events)) { + events.forEach((event) => { + if(event?.script && event.script.exec) { + if(event.listen === 'prerequest') { + if(event.script.exec && event.script.exec.length > 0) { + requestObject.script.req = postmanTranslation(event.script.exec); + } else { + requestObject.script.req = ''; + } + } + + if(event.listen === 'test') { + if(event.script.exec && event.script.exec.length > 0) { + requestObject.tests = postmanTranslation(event.script.exec); + } else { + requestObject.tests = ''; + } + } + } + }); + } + + return [uid, { request: requestObject }]; + }); + + parentPort.postMessage(modScripts); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js index ef98774de..d1a5caa7a 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Collection Authentication', () => { - it('should handle basic auth at collection level', () => { + it('should handle basic auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level basic auth', @@ -44,7 +44,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); // console.log('result', JSON.stringify(result, null, 2)); expect(result.root.request.auth).toEqual({ @@ -61,7 +61,7 @@ describe('Collection Authentication', () => { }); }); - it('should handle bearer token auth at collection level', () => { + it('should handle bearer token auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level bearer token', @@ -98,7 +98,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); // console.log('result', JSON.stringify(result, null, 2)); expect(result.root.request.auth).toEqual({ @@ -112,9 +112,9 @@ describe('Collection Authentication', () => { oauth2: null, digest: null }); - }); + }); - it('should handle API key auth at collection level', () => { + it('should handle API key auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level api key', @@ -156,7 +156,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.root.request.auth).toEqual({ mode: 'apikey', @@ -173,7 +173,7 @@ describe('Collection Authentication', () => { }); }); - it('should handle digest auth at collection level', () => { + it('should handle digest auth at collection level', async() => { const postmanCollection = { info: { name: 'Collection level digest auth', @@ -220,7 +220,7 @@ describe('Collection Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.root.request.auth).toEqual({ mode: 'digest', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js index b403d22d8..ba6f86596 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Folder Authentication', () => { - it('should handle basic auth at folder level', () => { + it('should handle basic auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level basic auth', @@ -49,7 +49,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'basic', @@ -65,7 +65,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle bearer token auth at folder level', () => { + it('should handle bearer token auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level bearer token', @@ -107,7 +107,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'bearer', @@ -120,7 +120,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle API key auth at folder level', () => { + it('should handle API key auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level API key', @@ -167,7 +167,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'apikey', @@ -180,7 +180,7 @@ describe('Folder Authentication', () => { }); }); - it('should handle digest auth at folder level', () => { + it('should handle digest auth at folder level', async() => { const postmanCollection = { info: { name: 'Folder level digest auth', @@ -232,7 +232,7 @@ describe('Folder Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].root.request.auth).toEqual({ mode: 'digest', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js index f8f52538e..3ac79476c 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js @@ -3,7 +3,7 @@ import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('postman-collection', () => { it('should correctly import a valid Postman collection file', async () => { - const brunoCollection = postmanToBruno(postmanCollection); + const brunoCollection = await postmanToBruno(postmanCollection); expect(brunoCollection).toMatchObject(expectedOutput); }); }); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js index 4f365589c..a57b8435a 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js @@ -20,7 +20,7 @@ describe('postmanTranslations - response commands', () => { const responseText = JSON.stringify(res.getBody()); const responseJson = res.getBody(); const responseStatus = res.statusText; - const responseHeaders = req.getHeaders(); + const responseHeaders = res.getHeaders(); test('Status code is 200', function() { expect(res.getStatus()).to.equal(200); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js index a542a6b61..2a71301d3 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js @@ -2,7 +2,7 @@ import { describe, it, expect } from '@jest/globals'; import postmanToBruno from '../../../src/postman/postman-to-bruno'; describe('Request Authentication', () => { - it('should handle basic auth at request level', () => { + it('should handle basic auth at request level', async() => { const postmanCollection = { info: { name: 'Request Auth Collection', @@ -26,7 +26,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].request.auth).toEqual({ mode: 'basic', @@ -42,7 +42,7 @@ describe('Request Authentication', () => { }); }); - it('should inherit folder auth when request has no auth', () => { + it('should inherit folder auth when request has no auth', async() => { const postmanCollection = { info: { name: 'Inherit Request Auth Collection', @@ -68,7 +68,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].items[0].request.auth).toEqual({ mode: 'bearer', @@ -83,7 +83,7 @@ describe('Request Authentication', () => { }); }); - it('should override folder auth with request auth', () => { + it('should override folder auth with request auth', async() => { const postmanCollection = { info: { name: 'Override Request Auth Collection', @@ -116,7 +116,7 @@ describe('Request Authentication', () => { ] }; - const result = postmanToBruno(postmanCollection); + const result = await postmanToBruno(postmanCollection); expect(result.items[0].items[0].request.auth).toEqual({ mode: 'bearer', diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js index 6d98ce6e8..1c1686bf2 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js @@ -17,7 +17,7 @@ describe('postmanTranslations - comment handling', () => { test('should comment non-translated pm commands', () => { const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; - const expectedOutput = "// test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; + const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js new file mode 100644 index 000000000..8d3508e05 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js @@ -0,0 +1,418 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Combined API Features Translation', () => { + // Basic translation test + it('should translate code', () => { + const code = 'console.log("Hello, world!");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(code); + }); + + // Preserving comments + it('should preserve comments', () => { + const code = '// This is a comment'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('// This is a comment'); + }); + + it('should preserve comments inside functions', () => { + const code = ` + function getUserDetails() { + // Get user details from API + const response = pm.response.json(); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function getUserDetails() { + // Get user details from API + const response = res.getBody(); + } + `); + }); + + it('should preserve comments inside if statements', () => { + const code = ` + if (pm.response.code === 200) { + // Success + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + // Success + console.log("Success"); + } + `); + }); + + it('should preserve multiline comments', () => { + const code = ` + /* + This is a multiline comment + */ + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + /* + This is a multiline comment + */ + `); + }); + + it('should preserve comments inside for loops', () => { + const code = ` + for (let i = 0; i < 10; i++) { + // Loop iteration + console.log(pm.response.json()[i]); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + for (let i = 0; i < 10; i++) { + // Loop iteration + console.log(res.getBody()[i]); + } + `); + }); + + // Multiple transformations in the same code block + it('should handle multiple translations in the same code block', () => { + const code = ` + const token = pm.environment.get("authToken"); + pm.test("Auth flow works", function() { + const response = pm.response.json(); + pm.expect(response.authenticated).to.be.true; + pm.environment.set("userId", response.user.id); + pm.collectionVariables.set("sessionId", response.session.id); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('pm.test("Auth flow works", function() {'); + expect(translatedCode).not.toContain('pm.expect(response.authenticated).to.be.true;'); + expect(translatedCode).not.toContain('pm.environment.set("userId", response.user.id);'); + expect(translatedCode).not.toContain('pm.collectionVariables.set("sessionId", response.session.id);'); + expect(translatedCode).toContain('const token = bru.getEnvVar("authToken");'); + expect(translatedCode).toContain('test("Auth flow works", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(response.authenticated).to.be.true;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);'); + expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);'); + }); + + // Nested expressions + it('should handle nested Postman API calls', () => { + const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");'); + }); + + it('should handle more complex nested expressions', () => { + const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));'); + }); + + // Unrelated code + it('should leave unrelated code untouched', () => { + const code = ` + function calculateTotal(items) { + return items.reduce((sum, item) => sum + item.price, 0); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(code); + }); + + it('should handle Postman API calls within JavaScript methods', () => { + const code = ` + const helpers = { + getAuthHeader: function() { + return "Bearer " + pm.environment.get("token"); + } + }; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('return "Bearer " + bru.getEnvVar("token");'); + }); + + + it('should handle aliases with object destructuring', () => { + const code = ` + const { environment, variables } = pm; + environment.set("token", "abc123"); + variables.get("userId"); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + bru.setEnvVar("token", "abc123"); + bru.getVar("userId"); + `); + }); + + // Code context tests + it('should translate pm commands inside functions', () => { + const code = ` + function getAuthHeader() { + return "Bearer " + pm.environment.get("token"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function getAuthHeader() { + return "Bearer " + bru.getEnvVar("token"); + } + `); + }); + + it('should translate pm commands inside if statements', () => { + const code = ` + if (pm.response.code === 200) { + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + console.log("Success"); + } + `); + }); + + + it('should translate pm commands inside if statements', () => { + const code = ` + const json = pm.response.json(); + if (json.code === 200) { + console.log("Success"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const json = res.getBody(); + if (json.code === 200) { + console.log("Success"); + } + `); + }); + + it('should translate pm commands inside else statements', () => { + const code = ` + if (pm.response.code === 200) { + console.log("Success"); + pm.response.to.have.status(200); + } else { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (res.getStatus() === 200) { + console.log("Success"); + expect(res.getStatus()).to.equal(200); + } else { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `); + }); + + it('should translate pm commands inside for loops', () => { + const code = ` + for (let i = 0; i < pm.response.json().length; i++) { + console.log(pm.response.json()[i]); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + for (let i = 0; i < res.getBody().length; i++) { + console.log(res.getBody()[i]); + } + `); + }); + + it('should translate pm commands inside while loops', () => { + const code = ` + while (pm.response.code === 200) { + console.log("Success"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + while (res.getStatus() === 200) { + console.log("Success"); + } + `); + }); + + it('should translate pm commands inside switch statements', () => { + const code = ` + switch (pm.response.code) { + case 200: + console.log("Success"); + break; + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + switch (res.getStatus()) { + case 200: + console.log("Success"); + break; + } + `); + }); + + it('should translate pm commands inside try catch statements', () => { + const code = ` + try { + pm.response.to.have.status(200); + } catch (error) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + try { + expect(res.getStatus()).to.equal(200); + } catch (error) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } + `); + }); + + it('should translate aliases within if statements block', () => { + const code = ` + const env = pm.environment; + const vars = pm.variables; + const collVars = pm.collectionVariables; + const test = pm.test; + const expect = pm.expect; + const response = pm.response; + + function processResponse() { + if(response.code === 200) { + console.log("Success"); + } else if(response.code === 400) { + console.log("Failure"); + expect(response.code).to.equal(400); + } else { + console.log("Unknown status code"); + expect(response.code).to.equal(500); + } + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + function processResponse() { + if(res.getStatus() === 200) { + console.log("Success"); + } else if(res.getStatus() === 400) { + console.log("Failure"); + expect(res.getStatus()).to.equal(400); + } else { + console.log("Unknown status code"); + expect(res.getStatus()).to.equal(500); + } + } + `); + }); + + it('should handle pm aliases inside functions', () => { + const code = ` + const tempRes = pm.response; + const tempTest = pm.test; + const tempExpect = pm.expect; + const tempEnv = pm.environment; + const tempVars = pm.variables; + const tempCollVars = pm.collectionVariables; + + function processResponse() { + tempTest("Status code is 200", function() { expect(tempRes.code).to.equal(200); }); + tempEnv.set("userId", tempRes.json().userId); + tempVars.set("token", tempRes.json().token); + tempCollVars.set("sessionId", tempRes.json().sessionId); + } + `; + + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + function processResponse() { + test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); }); + bru.setEnvVar("userId", res.getBody().userId); + bru.setVar("token", res.getBody().token); + bru.setVar("sessionId", res.getBody().sessionId); + } + `); + }); + + it('should nested pm commands', () => { + const code = ` + pm.collectionVariables.get(pm.environment.get('key')) + pm.test("Status code is 200", function() { + pm.response.to.have.status(200); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.getVar(bru.getEnvVar('key')) + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); + `); + }); + + it('should handle pm objects in template literals', () => { + const code = ` + const baseUrl = pm.environment.get("baseUrl"); + const endpoint = pm.variables.get("endpoint"); + const url = \`\${baseUrl}/api/\${endpoint}\`; + console.log(\`Response status: \${pm.response.code}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + expect(translatedCode).toContain('const endpoint = bru.getVar("endpoint");'); + expect(translatedCode).toContain('const url = `${baseUrl}/api/${endpoint}`;'); + expect(translatedCode).toContain('console.log(`Response status: ${res.getStatus()}`);'); + }); + + it('should handle pm objects in arrow functions', () => { + const code = ` + const getAuthHeader = () => "Bearer " + pm.environment.get("token"); + const processItems = items => items.forEach(item => { + pm.variables.set(item.key, item.value); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const getAuthHeader = () => "Bearer " + bru.getEnvVar("token");'); + expect(translatedCode).toContain('const processItems = items => items.forEach(item => {'); + expect(translatedCode).toContain('bru.setVar(item.key, item.value);'); + }); + + it('test', () => { + const code = ` + const globals = pm.globals; + const key = globals.get("key"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const globals = pm.globals; + const key = globals.get("key"); + `); + }) +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js new file mode 100644 index 000000000..c3461f6de --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js @@ -0,0 +1,242 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Environment Variable Translation', () => { + it('should translate pm.environment.get', () => { + const code = 'pm.environment.get("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvVar("test");'); + }); + + it('should translate pm.environment.set', () => { + const code = 'pm.environment.set("test", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("test", "value");'); + }); + + it('should translate pm.environment.has', () => { + const code = 'pm.environment.has("test")'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null'); + }); + + it('should translate pm.environment.unset', () => { + const code = 'pm.environment.unset("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteEnvVar("test");'); + }); + + it('should translate pm.environment.name', () => { + const code = 'pm.environment.name;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getEnvName();'); + }); + + it('should handle nested Postman API calls with environment', () => { + const code = 'pm.environment.set("computed", pm.variables.get("base") + "-suffix");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("computed", bru.getVar("base") + "-suffix");'); + }); + + it('should handle JSON operations with environment variables', () => { + const code = 'pm.environment.set("user", JSON.stringify({ id: 123, name: "John" }));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("user", JSON.stringify({ id: 123, name: "John" }));'); + }); + + it('should handle JSON.parse with environment variables', () => { + const code = 'const userData = JSON.parse(pm.environment.get("user"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const userData = JSON.parse(bru.getEnvVar("user"));'); + }); + + it('should translate pm.environment.name with different access patterns', () => { + const code = ` + const envName1 = pm.environment.name; + const env = pm.environment; + const envName2 = env.name; + console.log(pm.environment.name); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const envName1 = bru.getEnvName(); + const envName2 = bru.getEnvName(); + console.log(bru.getEnvName()); + `); + }); + + it('should handle environment aliases', () => { + const code = ` + const env = pm.environment; + const name = env.name; + const has = env.has("test"); + const set = env.set("test", "value"); + const get = env.get("test"); + const unset = env.unset("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const name = bru.getEnvName(); + const has = bru.getEnvVar("test") !== undefined && bru.getEnvVar("test") !== null; + const set = bru.setEnvVar("test", "value"); + const get = bru.getEnvVar("test"); + const unset = bru.deleteEnvVar("test"); + `); + }); + + // Legacy API (postman.) tests related to environment + it('should translate postman.setEnvironmentVariable', () => { + const code = 'postman.setEnvironmentVariable("apiKey", "abc123");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("apiKey", "abc123");'); + }); + + it('should translate postman.getEnvironmentVariable', () => { + const code = 'const baseUrl = postman.getEnvironmentVariable("baseUrl");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const baseUrl = bru.getEnvVar("baseUrl");'); + }); + + it('should translate postman.clearEnvironmentVariable', () => { + const code = 'postman.clearEnvironmentVariable("tempToken");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteEnvVar("tempToken");'); + }); + + it('should handle all environment variable methods together', () => { + const code = ` + // All environment variable methods + const envName = pm.environment.name; + const hasToken = pm.environment.has("token"); + const token = pm.environment.get("token"); + pm.environment.set("timestamp", new Date().toISOString()); + + console.log(\`Environment: \${envName}, Has token: \${hasToken}, Token: \${token}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const envName = bru.getEnvName();'); + expect(translatedCode).toContain('const hasToken = bru.getEnvVar("token") !== undefined && bru.getEnvVar("token") !== null;'); + expect(translatedCode).toContain('const token = bru.getEnvVar("token");'); + expect(translatedCode).toContain('bru.setEnvVar("timestamp", new Date().toISOString());'); + }); + + // Additional robust tests for environment variables + it('should handle environment variables with computed property names', () => { + const code = ` + const prefix = "api"; + const suffix = "Key"; + pm.environment.set(prefix + "_" + suffix, "abc123"); + const computedValue = pm.environment.get(prefix + "_" + suffix); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar(prefix + "_" + suffix, "abc123");'); + expect(translatedCode).toContain('const computedValue = bru.getEnvVar(prefix + "_" + suffix);'); + }); + + it('should handle environment variables in complex object structures', () => { + const code = ` + const config = { + baseUrl: pm.environment.get("apiUrl"), + headers: { + "Authorization": "Bearer " + pm.environment.get("token"), + "X-Api-Key": pm.environment.get("apiKey") || "default-key" + }, + timeout: parseInt(pm.environment.get("timeout") || "5000"), + validate: pm.environment.has("validateResponses") + }; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('baseUrl: bru.getEnvVar("apiUrl"),'); + expect(translatedCode).toContain('"Authorization": "Bearer " + bru.getEnvVar("token"),'); + expect(translatedCode).toContain('"X-Api-Key": bru.getEnvVar("apiKey") || "default-key"'); + expect(translatedCode).toContain('timeout: parseInt(bru.getEnvVar("timeout") || "5000"),'); + expect(translatedCode).toContain('validate: bru.getEnvVar("validateResponses") !== undefined && bru.getEnvVar("validateResponses") !== null'); + }); + + it('should handle environment variables in conditionals correctly', () => { + const code = ` + if (pm.environment.has("apiKey")) { + if (pm.environment.get("apiKey").length > 0) { + console.log("Valid API key exists"); + } else { + console.log("API key is empty"); + } + } else { + console.log("No API key defined"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) {'); + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey").length > 0) {'); + }); + + it('should handle multiple levels of environment variable aliasing', () => { + const code = ` + const env = pm.environment; + + env.set("key", "value"); + const value = env.get("key"); + const exists = env.has("key"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.setEnvVar("key", "value"); + const value = bru.getEnvVar("key"); + const exists = bru.getEnvVar("key") !== undefined && bru.getEnvVar("key") !== null; + `); + }); + + it('should handle environment variables with dynamic values', () => { + const code = ` + // Generate a timestamp for this request + const timestamp = new Date().toISOString(); + pm.environment.set("requestTimestamp", timestamp); + + // Generate a unique ID + const uniqueId = "req_" + Math.random().toString(36).substring(2, 15); + pm.environment.set("requestId", uniqueId); + + // Calculate an expiry time (30 minutes from now) + const expiryTime = new Date(); + expiryTime.setMinutes(expiryTime.getMinutes() + 30); + pm.environment.set("tokenExpiry", expiryTime.getTime()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("requestTimestamp", timestamp);'); + expect(translatedCode).toContain('bru.setEnvVar("requestId", uniqueId);'); + expect(translatedCode).toContain('bru.setEnvVar("tokenExpiry", expiryTime.getTime());'); + }); + + it('should handle environment variables in try-catch blocks', () => { + const code = ` + try { + const configStr = pm.environment.get("config"); + const config = JSON.parse(configStr); + console.log("Config loaded:", config.version); + } catch (error) { + console.error("Failed to parse config"); + pm.environment.set("configError", error.message); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const configStr = bru.getEnvVar("config");'); + expect(translatedCode).toContain('bru.setEnvVar("configError", error.message);'); + }); + + it('should handle legacy environment and pm.setEnvironmentVariable together', () => { + const code = ` + // Legacy style + postman.setEnvironmentVariable("legacyKey", "legacyValue"); + + // Mixed with newer style + const value = pm.environment.get("anotherKey"); + + // Another legacy form + pm.setEnvironmentVariable("thirdKey", "thirdValue"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("legacyKey", "legacyValue");'); + expect(translatedCode).toContain('const value = bru.getEnvVar("anotherKey");'); + expect(translatedCode).toContain('bru.setEnvVar("thirdKey", "thirdValue");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js new file mode 100644 index 000000000..053e99685 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js @@ -0,0 +1,64 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Execution Flow Translation', () => { + // Request flow control + it('should translate pm.setNextRequest', () => { + const code = 'pm.setNextRequest("Get User Details");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setNextRequest("Get User Details");'); + }); + + it('should translate pm.execution.skipRequest', () => { + const code = 'if (condition) pm.execution.skipRequest();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (condition) bru.runner.skipRequest();'); + }); + + it('should translate pm.execution.setNextRequest(null)', () => { + const code = 'pm.execution.setNextRequest(null);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.runner.stopExecution();'); + }); + + it('should translate pm.execution.setNextRequest("null")', () => { + const code = 'pm.execution.setNextRequest("null");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.runner.stopExecution();'); + }); + + it('should handle pm.execution.setNextRequest with non-null parameters', () => { + const code = ` + // Continue normal flow + pm.execution.setNextRequest("Get user details"); + + // With variable + const nextReq = "Update profile"; + pm.execution.setNextRequest(nextReq); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('bru.runner.setNextRequest("Get user details");'); + expect(translatedCode).toContain('bru.runner.setNextRequest(nextReq);'); + }); + + it('should handle all execution control methods together', () => { + const code = ` + // All execution control methods + if (pm.response.code === 401) { + pm.execution.skipRequest(); + } else if (pm.response.code === 500) { + pm.execution.setNextRequest(null); + } else { + pm.setNextRequest("Get User Details"); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('if (res.getStatus() === 401) {'); + expect(translatedCode).toContain('bru.runner.skipRequest();'); + expect(translatedCode).toContain('} else if (res.getStatus() === 500) {'); + expect(translatedCode).toContain('bru.runner.stopExecution();'); + expect(translatedCode).toContain('} else {'); + expect(translatedCode).toContain('bru.setNextRequest("Get User Details");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js new file mode 100644 index 000000000..e548aa03c --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js @@ -0,0 +1,283 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Legacy Tests[] Syntax Translation', () => { + it('should handle tests[] commands', () => { + const code = ` + tests["Status code is 200"] = pm.response.code === 200;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Status code is 200", function() { + expect(Boolean(res.getStatus() === 200)).to.be.true; + });`); + }); + + it('should handle tests[] with complex expressions', () => { + const code = ` + tests["Response has valid data"] = pm.response.json().data && pm.response.json().data.length > 0;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response has valid data", function() { + expect(Boolean(res.getBody().data && res.getBody().data.length > 0)).to.be.true; + });`); + }); + + it('should handle tests[] with string equality', () => { + const code = ` + tests["Content-Type is application/json"] = pm.response.headers.get("Content-Type") === "application/json";`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Content-Type is application/json", function() { + expect(Boolean(res.getHeaders().get("Content-Type") === "application/json")).to.be.true; + });`); + }); + + it('should handle tests[] with function calls', () => { + const code = ` + tests["Response time is acceptable"] = pm.response.responseTime < 500;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response time is acceptable", function() { + expect(Boolean(res.getResponseTime() < 500)).to.be.true; + });`); + }); + + it('should handle tests[] with variable references', () => { + const code = ` + const expectedStatus = 201; + tests["Status code is correct"] = pm.response.code === expectedStatus;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const expectedStatus = 201; + test("Status code is correct", function() { + expect(Boolean(res.getStatus() === expectedStatus)).to.be.true; + });`); + }); + + it('should handle multiple tests[] statements', () => { + const code = ` + tests["Status code is 200"] = pm.response.code === 200; + tests["Response has data"] = pm.response.json().hasOwnProperty("data");`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Status code is 200", function() { + expect(Boolean(res.getStatus() === 200)).to.be.true; + }); + test("Response has data", function() { + expect(Boolean(res.getBody().hasOwnProperty("data"))).to.be.true; + });`); + }); + + it('should handle tests[] with special characters in name', () => { + const code = ` + tests["Special characters: !@#$%^&*()"] = true;`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Special characters: !@#$%^&*()", function() { + expect(Boolean(true)).to.be.true; + });`); + }); + + it('should handle tests[] with pm.environment variables', () => { + const code = ` + tests["Response matches environment variable"] = pm.response.json().id === pm.environment.get("expectedId");`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Response matches environment variable", function() { + expect(Boolean(res.getBody().id === bru.getEnvVar("expectedId"))).to.be.true; + });`); + }); + + it('should handle nested pm objects in tests[] assignments', () => { + const code = ` + tests["Authentication header is present"] = pm.request.headers.has("Authorization"); + tests["Data count is correct"] = pm.response.json().items.length === pm.variables.get("expectedCount"); + `; + const translatedCode = translateCode(code); + + // The exact translation might vary depending on implementation details, + // but we can check for key transformations + expect(translatedCode).toContain('test("Authentication header is present"'); + expect(translatedCode).toContain('test("Data count is correct"'); + expect(translatedCode).toContain('res.getBody().items.length === bru.getVar("expectedCount")'); + }); + + // Additional robust tests for legacy tests[] syntax + it('should handle tests[] with complex boolean expressions', () => { + const code = ` + tests["Complex validation"] = (pm.response.code >= 200 && pm.response.code < 300) || + (pm.response.json().success === true && pm.response.json().data !== null);`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Complex validation", function() {'); + expect(translatedCode).toContain('expect(Boolean((res.getStatus() >= 200 && res.getStatus() < 300) ||'); + expect(translatedCode).toContain('(res.getBody().success === true && res.getBody().data !== null))).to.be.true;'); + }); + + it('should handle tests[] with array methods', () => { + const code = ` + tests["All items have an ID"] = pm.response.json().items.every(item => item.hasOwnProperty('id')); + tests["Has premium item"] = pm.response.json().items.some(item => item.type === 'premium');`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("All items have an ID", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().items.every(item => item.hasOwnProperty(\'id\')))).to.be.true;'); + expect(translatedCode).toContain('test("Has premium item", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().items.some(item => item.type === \'premium\'))).to.be.true;'); + }); + + it('should handle tests[] with template literals in the name', () => { + const code = ` + const endpoint = "users"; + tests[\`Endpoint \${endpoint} returns valid response\`] = pm.response.code === 200;`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const endpoint = "users";'); + expect(translatedCode).toContain('test(`Endpoint ${endpoint} returns valid response`, function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + }); + + it('should handle tests[] with deep property access', () => { + const code = ` + tests["User has admin role"] = pm.response.json().user && + pm.response.json().user.roles && + pm.response.json().user.roles.includes('admin');`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("User has admin role", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getBody().user &&'); + expect(translatedCode).toContain('res.getBody().user.roles &&'); + expect(translatedCode).toContain('res.getBody().user.roles.includes(\'admin\'))).to.be.true;'); + }); + + it('should handle tests[] with JSON schema validation patterns', () => { + const code = ` + const schema = { + type: "object", + required: ["id", "name"], + properties: { + id: { type: "string" }, + name: { type: "string" } + } + }; + + const data = pm.response.json(); + + // Basic schema validation patterns + tests["Has required fields"] = data.hasOwnProperty('id') && data.hasOwnProperty('name'); + tests["ID is string"] = typeof data.id === 'string'; + tests["Name is string"] = typeof data.name === 'string';`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const schema = {'); + expect(translatedCode).toContain('type: "object",'); + expect(translatedCode).toContain('required: ["id", "name"],'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('test("Has required fields", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'id\') && data.hasOwnProperty(\'name\'))).to.be.true;'); + expect(translatedCode).toContain('test("ID is string", function() {'); + expect(translatedCode).toContain('expect(Boolean(typeof data.id === \'string\')).to.be.true;'); + }); + + it('should handle tests[] within conditional blocks', () => { + const code = ` + const data = pm.response.json(); + + if (pm.response.code === 200) { + tests["Success response has data"] = data.hasOwnProperty('items'); + + if (data.items.length > 0) { + tests["First item has ID"] = data.items[0].hasOwnProperty('id'); + } + } else { + tests["Error response has message"] = data.hasOwnProperty('message'); + }`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('if (res.getStatus() === 200) {'); + expect(translatedCode).toContain('test("Success response has data", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'items\'))).to.be.true;'); + expect(translatedCode).toContain('if (data.items.length > 0) {'); + expect(translatedCode).toContain('test("First item has ID", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.items[0].hasOwnProperty(\'id\'))).to.be.true;'); + expect(translatedCode).toContain('} else {'); + expect(translatedCode).toContain('test("Error response has message", function() {'); + expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\'message\'))).to.be.true;'); + }); + + it('should handle tests[] with combination of legacy and modern styles', () => { + const code = ` + // Legacy style + tests["Status code is 200"] = pm.response.code === 200; + + // Modern style + pm.test("Response has valid data", function() { + const json = pm.response.json(); + pm.expect(json).to.be.an('object'); + pm.expect(json.items).to.be.an('array'); + + // Mix by using tests[] inside pm.test + tests["All items have price"] = json.items.every(item => item.hasOwnProperty('price')); + });`; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + expect(translatedCode).toContain('test("Response has valid data", function() {'); + expect(translatedCode).toContain('const json = res.getBody();'); + expect(translatedCode).toContain('expect(json).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(json.items).to.be.an(\'array\');'); + expect(translatedCode).toContain('test("All items have price", function() {'); + expect(translatedCode).toContain('expect(Boolean(json.items.every(item => item.hasOwnProperty(\'price\')))).to.be.true;'); + }); + + it('should handle complex real-world tests[] example', () => { + const code = ` + // Parse response + const response = pm.response.json(); + + // Basic response validation + tests["Status code is 200"] = pm.response.code === 200; + tests["Response is valid JSON"] = response !== null && typeof response === 'object'; + + // Check headers + tests["Has content-type header"] = pm.response.headers.has("Content-Type"); + tests["Content-Type is JSON"] = pm.response.headers.get("Content-Type").includes("application/json"); + + // Validate against expected values + const expectedItems = parseInt(pm.environment.get("expectedItemCount")); + tests["Has correct number of items"] = response.items.length === expectedItems; + + // Check for required fields on all items + const requiredFields = ["id", "name", "price", "category"]; + tests["All items have required fields"] = response.items.every(item => { + return requiredFields.every(field => item.hasOwnProperty(field)); + }); + + // Validate specific business rules + tests["No items with zero price"] = response.items.every(item => parseFloat(item.price) > 0); + tests["Has at least one featured item"] = response.items.some(item => item.featured === true); + + // If we find a specific item we're looking for, save its ID for later + const targetItem = response.items.find(item => item.name === pm.variables.get("targetItemName")); + if (targetItem) { + pm.environment.set("targetItemId", targetItem.id); + tests["Found target item"] = true; + }`; + const translatedCode = translateCode(code); + + // Check key transformations + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;'); + expect(translatedCode).toContain('test("Has content-type header", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getHeaders().has("Content-Type"))).to.be.true;'); + expect(translatedCode).toContain('test("Content-Type is JSON", function() {'); + expect(translatedCode).toContain('expect(Boolean(res.getHeaders().get("Content-Type").includes("application/json"))).to.be.true;'); + expect(translatedCode).toContain('const expectedItems = parseInt(bru.getEnvVar("expectedItemCount"));'); + expect(translatedCode).toContain('test("Has correct number of items", function() {'); + expect(translatedCode).toContain('expect(Boolean(response.items.length === expectedItems)).to.be.true;'); + expect(translatedCode).toContain('const targetItem = response.items.find(item => item.name === bru.getVar("targetItemName"));'); + expect(translatedCode).toContain('bru.setEnvVar("targetItemId", targetItem.id);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js new file mode 100644 index 000000000..a9be82130 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js @@ -0,0 +1,283 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Multiline Syntax Handling', () => { + it('should handle basic multiline variable syntax with indentation', () => { + const code = ` + const userId = pm.variables + .get("userId"); + pm.variables + .set("timestamp", new Date().toISOString()); + const hasToken = pm.variables + .has("token"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const userId = bru.getVar("userId"); + bru.setVar("timestamp", new Date().toISOString()); + const hasToken = bru.hasVar("token"); + `); + }); + + it('should handle multiline environment variable syntax', () => { + const code = ` + const baseUrl = pm + .environment + .get("baseUrl"); + pm + .environment + .set("requestTime", Date.now()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const baseUrl = bru.getEnvVar("baseUrl"); + bru.setEnvVar("requestTime", Date.now()); + `); + }); + + it('should handle multiline collection variable syntax', () => { + const code = ` + const apiKey = pm.collectionVariables + .get("apiKey"); + pm.collectionVariables + .set("lastRun", new Date().toISOString()); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const apiKey = bru.getVar("apiKey"); + bru.setVar("lastRun", new Date().toISOString()); + `); + }); + + it('should handle complex environment.has transformation with multiline syntax', () => { + const code = ` + if (pm.environment + .has("apiKey")) { + console.log("API Key exists"); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null) { + console.log("API Key exists"); + } + `); + }); + + it('should handle response.to.have.status with multiline formatting', () => { + const code = ` + pm.test("Status code is correct", function() { + pm + .response + .to + .have + .status(200); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + }); + + it('should handle response.to.have.header with multiline formatting', () => { + const code = ` + pm.test("Content type is present", function() { + pm + .response + .to + .have + .header("content-type"); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())'); + }); + + it('should handle response properties with multiline syntax', () => { + const code = ` + const responseBody = pm + .response + .json(); + const responseText = pm + .response + .text; + const responseTime = pm + .response + .responseTime; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const responseBody = res.getBody()'); + expect(translatedCode).toContain('const responseText = '); + expect(translatedCode).toContain('const responseTime = res.getResponseTime()'); + }); + + it('should handle execution flow control with multiline syntax', () => { + const code = ` + // Stop execution + pm + .execution + .setNextRequest(null); + + // Continue to next request + pm + .execution + .setNextRequest("Next API Call"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('// Stop execution'); + expect(translatedCode).toContain('// Continue to next request'); + expect(translatedCode).toContain('bru.runner.stopExecution()'); + expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")'); + }); + + it('should handle mixed normal and multiline syntax in the same code', () => { + const code = ` + // Normal syntax + const normalVar = pm.variables.get("normal"); + + // Multiline syntax + const multilineVar = pm.variables + .get("multiline"); + + // Normal syntax again + pm.variables.set("normalSet", "value"); + + // Multiline syntax again + pm.variables + .set("multilineSet", "value"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + // Normal syntax + const normalVar = bru.getVar("normal"); + + // Multiline syntax + const multilineVar = bru.getVar("multiline"); + + // Normal syntax again + bru.setVar("normalSet", "value"); + + // Multiline syntax again + bru.setVar("multilineSet", "value"); + `); + }); + + it('should handle complex multiline method chaining', () => { + const code = ` + pm + .test("Test with chaining", function() { + pm + .response + .to + .have + .status(200); + + const body = pm + .response + .json(); + + pm + .expect(body) + .to + .have + .property('success') + .equal(true); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('test("Test with chaining", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + expect(translatedCode).toContain('const body = res.getBody()'); + expect(translatedCode).toContain('.property(\'success\')'); + expect(translatedCode).toContain('.equal(true)'); + }); + + it('should handle a comprehensive script with various multiline formats', () => { + const code = ` + // This comprehensive script tests different multiline styles and whitespace variations + + // Environment variables with different formatting styles + const baseUrl = pm.environment.get("baseUrl"); + const apiKey = pm + .environment + .get("apiKey"); + const userId = pm.environment + .get("userId"); + + // Mix of variable styles + pm.variables.set("testId", "test-" + Date.now()); + pm + .variables + .set("timestamp", new Date().toISOString()); + + // Collection variables with inconsistent spacing + pm.collectionVariables + .set("lastRun", new Date()); + + // Complex conditionals with multiline expressions + if (pm + .environment + .has("apiKey") && + pm.variables.has("testId")) { + + // Testing response with mixed syntax styles + pm.test("Response validation", function() { + // Normal style + pm.response.to.have.status(200); + + // Multiline with different indentation + pm + .response + .to + .have + .header("content-type"); + + pm.response + .to.have + .jsonBody("success", true); + + // Extreme indentation + pm + .response + .to + .not + .have + .jsonBody("error"); + }); + + // Flow control with mixed styles + if (pm.response.code === 401) { + pm.execution.setNextRequest(null); + } else { + pm + .execution + .setNextRequest("Next API Call"); + } + } + `; + + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl")'); + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey")'); + expect(translatedCode).toContain('const userId = bru.getEnvVar("userId")'); + + // Check variables translations + expect(translatedCode).toContain('bru.setVar("testId", "test-" + Date.now())'); + expect(translatedCode).toContain('bru.setVar("timestamp", new Date().toISOString())'); + + // Check collection variables + expect(translatedCode).toContain('bru.setVar("lastRun", new Date())'); + + // Check complex conditionals + expect(translatedCode).toContain('if (bru.getEnvVar("apiKey") !== undefined && bru.getEnvVar("apiKey") !== null &&'); + expect(translatedCode).toContain('bru.hasVar("testId"))'); + + // Check response testing + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("content-type".toLowerCase())'); + + // Check flow control + expect(translatedCode).toContain('if (res.getStatus() === 401)'); + expect(translatedCode).toContain('bru.runner.stopExecution()'); + expect(translatedCode).toContain('bru.runner.setNextRequest("Next API Call")'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js new file mode 100644 index 000000000..20e7890a7 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js @@ -0,0 +1,132 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Postman to PM References Conversion', () => { + // Basic conversions + it('should convert basic postman references to pm', () => { + const code = 'postman.setEnvironmentVariable("key", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setEnvVar("key", "value");'); + // The key part is that it should convert postman.* to pm.* internally before + // translating to bru.* APIs + }); + + it('should convert postman variable access to pm', () => { + const code = 'const value = postman.getEnvironmentVariable("key");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const value = bru.getEnvVar("key");'); + }); + + it('should handle postman variable assignments', () => { + const code = ` + const envVar = postman.environment.get("apiKey"); + const baseUrl = postman.environment.get("baseUrl"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const envVar = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + }); + + // More complex patterns + it('should handle mixed postman and pm references in the same code', () => { + const code = ` + // Using both postman and pm APIs + const apiKey = postman.environment.get("apiKey"); + const baseUrl = pm.environment.get("baseUrl"); + + // Using both formats in a test + postman.test("Status code is 200", function() { + pm.expect(pm.response.code).to.equal(200); + }); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + it('should handle postman references in object destructuring', () => { + const code = ` + const { environment } = postman; + environment.set("key", "value"); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('bru.setEnvVar("key", "value");'); + }); + + // Complex control flows + it('should handle postman references in control flow statements', () => { + const code = ` + if (postman.environment.get("isProduction") === "true") { + const apiUrl = postman.environment.get("prodUrl"); + postman.setNextRequest("Production Flow"); + } else { + const apiUrl = postman.environment.get("devUrl"); + postman.setNextRequest("Development Flow"); + } + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (bru.getEnvVar("isProduction") === "true") {'); + expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("prodUrl");'); + expect(translatedCode).toContain('bru.setNextRequest("Production Flow");'); + expect(translatedCode).toContain('const apiUrl = bru.getEnvVar("devUrl");'); + expect(translatedCode).toContain('bru.setNextRequest("Development Flow");'); + }); + + // Legacy response handling + it('should handle legacy postman response methods', () => { + const code = ` + // Using legacy response handling + const responseCode = postman.response.code; + const responseBody = postman.response.json(); + + // Set environment variables with response data + postman.setEnvironmentVariable("lastResponseCode", responseCode); + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const responseCode = res.getStatus();'); + expect(translatedCode).toContain('const responseBody = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("lastResponseCode", responseCode);'); + }); + + // Postman in string literals should be untouched + it('should not convert postman references in string literals', () => { + const code = ` + console.log("This is a pm script"); + const message = "We're using pm to test our API"; + `; + + const translatedCode = translateCode(code); + expect(translatedCode).toContain('console.log("This is a pm script");'); + expect(translatedCode).toContain('const message = "We\'re using pm to test our API";'); + }); + + // Complex example with aliasing + it('should handle complex postman reference patterns with aliasing', () => { + const code = ` + // Aliasing the postman object + const env = postman.environment; + const code = postman.code; + + // Using the alias + const apiKey = env.get("apiKey"); + const userId = env.get("userId"); + + // Using alias in tests + postman.test("Response is valid", function() { + postman.expect(code).to.equal(200); + }); + `; + + const translatedCode = translateCode(code); + // Should handle the aliases properly + expect(translatedCode).toContain('const apiKey = bru.getEnvVar("apiKey");'); + expect(translatedCode).toContain('const userId = bru.getEnvVar("userId");'); + expect(translatedCode).toContain('test("Response is valid", function() {'); + expect(translatedCode).toContain('expect(code).to.equal(200);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js new file mode 100644 index 000000000..a81bc6c72 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js @@ -0,0 +1,108 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Request Translation', () => { + it('should translate pm.request.url', () => { + const code = 'const requestUrl = pm.request.url;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const requestUrl = req.getUrl();'); + }); + + it('should translate pm.request.method', () => { + const code = 'const method = pm.request.method;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const method = req.getMethod();'); + }); + + it('should translate pm.request.headers', () => { + const code = 'const headers = pm.request.headers;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const headers = req.getHeaders();'); + }); + + it('should translate pm.request.body', () => { + const code = 'const body = pm.request.body;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const body = req.getBody();'); + }); + + it('should translate pm.response.statusText', () => { + const code = 'const statusText = pm.response.statusText;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const statusText = res.statusText;'); + }); + + it('should translate multiple request methods in one block', () => { + const code = ` + const url = pm.request.url; + const method = pm.request.method; + const headers = pm.request.headers; + const body = pm.request.body; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const url = req.getUrl(); + const method = req.getMethod(); + const headers = req.getHeaders(); + const body = req.getBody(); + `); + }); + + it('should handle request and response properties together', () => { + const code = ` + // Get request data + const url = pm.request.url; + const method = pm.request.method; + + // Get response data + const statusCode = pm.response.code; + const statusText = pm.response.statusText; + + // Verify expectations + pm.test("Request was made correctly", function() { + pm.expect(method).to.equal("POST"); + pm.expect(url).to.include("/api/items"); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const url = req.getUrl();'); + expect(translatedCode).toContain('const method = req.getMethod();'); + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const statusText = res.statusText;'); + expect(translatedCode).toContain('test("Request was made correctly", function() {'); + expect(translatedCode).toContain('expect(method).to.equal("POST");'); + expect(translatedCode).toContain('expect(url).to.include("/api/items");'); + }); + + it('should handle request properties in conditional blocks', () => { + const code = ` + if (pm.request.method === "POST") { + console.log("This is a POST request to " + pm.request.url); + pm.test("Request has correct content-type", function() { + pm.expect(pm.request.headers.has("Content-Type")).to.be.true; + }); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('if (req.getMethod() === "POST") {'); + expect(translatedCode).toContain('console.log("This is a POST request to " + req.getUrl());'); + expect(translatedCode).toContain('test("Request has correct content-type", function() {'); + // Note: The expectation for headers.has might be transformed differently + // depending on how complex transformations are handled + }); + + it('should handle request data extraction and variable setting', () => { + const code = ` + // Extract request data + const requestData = pm.request.body; + const contentType = pm.request.headers.get("Content-Type"); + + // Save for later use + pm.variables.set("lastRequestBody", JSON.stringify(requestData)); + pm.environment.set("lastContentType", contentType); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const requestData = req.getBody();'); + expect(translatedCode).toContain('bru.setVar("lastRequestBody", JSON.stringify(requestData));'); + expect(translatedCode).toContain('bru.setEnvVar("lastContentType", contentType);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js new file mode 100644 index 000000000..7fd4d902b --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js @@ -0,0 +1,489 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Response Translation', () => { + // Basic response property tests + it('should translate pm.response.json', () => { + const code = 'const jsonData = pm.response.json();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const jsonData = res.getBody();'); + }); + + it('should translate pm.response.code', () => { + const code = 'if (pm.response.code === 200) { console.log("Success"); }'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (res.getStatus() === 200) { console.log("Success"); }'); + }); + + it('should translate pm.response.text', () => { + const code = 'const responseText = pm.response.text();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const responseText = JSON.stringify(res.getBody());'); + }); + + it('should translate pm.response.responseTime', () => { + const code = 'console.log("Response time:", pm.response.responseTime);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('console.log("Response time:", res.getResponseTime());'); + }); + + it('should translate pm.response.statusText', () => { + const code = 'console.log("Status text:", pm.response.statusText);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('console.log("Status text:", res.statusText);'); + }); + + // Complex response transformations + it('should transform pm.response.to.have.status', () => { + const code = 'pm.response.to.have.status(201);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getStatus()).to.equal(201);'); + }); + + it('should transform pm.response.to.have.header with single argument', () => { + const code = 'pm.response.to.have.header("Content-Type");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase());'); + }); + + it('should transform multiple pm.response.to.have.header statements', () => { + const code = ` + pm.response.to.have.header("Content-Type", "application/json"); + pm.response.to.have.header("Cache-Control", "no-cache"); + `; + const translatedCode = translateCode(code); + + // Check for the existence of all four assertions (two pairs) + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase(), "no-cache");'); + }); + + it('should transform pm.response.to.have.header inside control structures', () => { + const code = ` + if (pm.response.code === 200) { + pm.response.to.have.header("Content-Type", "application/json"); + } + `; + const translatedCode = translateCode(code); + + // The assertions should be inside the if block + expect(translatedCode).toContain('if (res.getStatus() === 200) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + }); + + it('should transform pm.response.to.have.header with variable parameters', () => { + const code = ` + const headerName = "Content-Type"; + const expectedValue = "application/json"; + pm.response.to.have.header(headerName, expectedValue); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const headerName = "Content-Type";'); + expect(translatedCode).toContain('const expectedValue = "application/json";'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);'); + }); + + // Response aliases tests + it('should handle response aliases', () => { + const code = ` + const response = pm.response; + const status = response.status; + const body = response.json(); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const status = res.statusText; + const body = res.getBody(); + `); + }); + + // Response to.have.status with different formats + it('should handle pm.response.to.have.status with different status codes', () => { + const code = ` + // Test different status codes + pm.response.to.have.status(200); // OK + pm.response.to.have.status(201); // Created + pm.response.to.have.status(400); // Bad Request + pm.response.to.have.status(404); // Not Found + pm.response.to.have.status(500); // Server Error + + // With variables + const expectedStatus = 200; + pm.response.to.have.status(expectedStatus); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(201);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(400);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(404);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(500);'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(expectedStatus);'); + }); + + // Alias for pm.response.to.have.status + it('should handle pm.response.to.have.status alias', () => { + const code = ` + const resp = pm.response; + resp.to.have.status(200); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + expect(res.getStatus()).to.equal(200); + `); + }); + + it('should handle pm.response.to.have.header alias', () => { + const code = ` + const resp = pm.response; + resp.to.have.header("Content-Type"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase()); + `); + }); + + it('should handle pm.response.to.have.header alias with value check', () => { + const code = ` + const resp = pm.response; + resp.to.have.header("Content-Type", "application/json"); + `; + const translatedCode = translateCode(code); + + // Check for both assertions when using an alias + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + }); + + + it('should translate response.status', () => { + const code = ` + const resp = pm.response; + const statusCode = resp.status; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusCode = res.statusText; + `); + }); + + it('should translate response.body', () => { + const code = ` + const resp = pm.response; + const responseBody = resp.json(); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const responseBody = res.getBody(); + `); + }); + + it('should translate pm.response.statusText', () => { + const code = ` + const resp = pm.response; + const statusText = resp.statusText; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusText = res.statusText; + `); + }); + + it('should translate multiple response methods in one block', () => { + const code = ` + const resp = pm.response; + const statusCode = resp.code; + const statusText = resp.statusText; + const jsonData = resp.json(); + const responseText = resp.text(); + const time = resp.responseTime; + resp.to.have.status(200); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const statusCode = res.getStatus(); + const statusText = res.statusText; + const jsonData = res.getBody(); + const responseText = JSON.stringify(res.getBody()); + const time = res.getResponseTime(); + expect(res.getStatus()).to.equal(200); + `); + }); + + it('should handle accessing nested properties on response objects', () => { + const code = ` + const resp = pm.response; + const data = resp.json(); + if (data && data.user && data.user.id) { + pm.environment.set("userId", data.user.id); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const resp = pm.response;'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("userId", data.user.id);'); + }); + + it('should handle all response property methods together', () => { + const code = ` + // All response property methods + const statusCode = pm.response.code; + const responseBody = pm.response.json(); + const responseText = pm.response.text(); + const statusText = pm.response.statusText; + const responseTime = pm.response.responseTime; + + pm.test("Response is valid", function() { + pm.response.to.have.status(200); + pm.expect(responseBody).to.be.an('object'); + pm.expect(responseTime).to.be.below(1000); + pm.expect(statusText).to.equal('OK'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const responseBody = res.getBody();'); + expect(translatedCode).toContain('const responseText = JSON.stringify(res.getBody());'); + expect(translatedCode).toContain('const responseTime = res.getResponseTime();'); + expect(translatedCode).toContain('const statusText = res.statusText;'); + expect(translatedCode).toContain('test("Response is valid", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('expect(responseBody).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);'); + expect(translatedCode).toContain('expect(statusText).to.equal(\'OK\');'); + }); + + it('should handle pm objects with array access on response', () => { + const code = ` + const items = pm.response.json().items; + for (let i = 0; i < items.length; i++) { + pm.collectionVariables.set("item_" + i, items[i].id); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('bru.setVar("item_" + i, items[i].id);'); + }); + + it('should handle response JSON with optional chaining and nullish coalescing', () => { + const code = ` + const userId = pm.response.json()?.user?.id ?? "anonymous"; + const items = pm.response.json()?.data?.items || []; + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const userId = res.getBody()?.user?.id ?? "anonymous";'); + expect(translatedCode).toContain('const items = res.getBody()?.data?.items || [];'); + }); + + it('should handle response headers with different access patterns', () => { + // will need to handle get, set methods, bruno does not support this yet + const code = ` + const contentType = pm.response.headers.get('Content-Type'); + const contentLength = pm.response.headers.get('Content-Length'); + console.log("contentType", contentType); + console.log("contentLength", contentLength); + + pm.test("Headers are correct", function() { + pm.response.to.have.header('Content-Type'); + pm.response.to.have.header('Content-Length'); + pm.expect(contentType).to.include('application/json'); + }); + `; + const translatedCode = translateCode(code); + + // Check how header access is translated + expect(translatedCode).toContain('const contentType = res.getHeaders().get(\'Content-Type\');'); + expect(translatedCode).toContain('const contentLength = res.getHeaders().get(\'Content-Length\');'); + expect(translatedCode).toContain('console.log("contentType", contentType);'); + expect(translatedCode).toContain('console.log("contentLength", contentLength);'); + expect(translatedCode).not.toContain('pm.test') + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Type\'.toLowerCase())'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\'Content-Length\'.toLowerCase())'); + expect(translatedCode).toContain('expect(contentType).to.include(\'application/json\')'); + }); + + it('should transform response data with array destructuring', () => { + const code = ` + const { id, name, items } = pm.response.json(); + const [first, second] = items; + pm.environment.set("userId", id); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const { id, name, items } = res.getBody();'); + expect(translatedCode).toContain('const [first, second] = items;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", id);'); + }); + + it('should handle response in complex conditionals', () => { + const code = ` + if (pm.response.code >= 200 && pm.response.code < 300) { + if (pm.response.headers.get('Content-Type').includes('application/json')) { + const data = pm.response.json(); + + if (data.success === true && data.token) { + pm.environment.set("authToken", data.token); + } else if (data.error) { + console.error("API error:", data.error); + } + } + } else if (pm.response.code === 404) { + console.log("Resource not found"); + } else { + console.error("Request failed with status:", pm.response.code); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('if (res.getStatus() >= 200 && res.getStatus() < 300) {'); + expect(translatedCode).toContain('if (res.getHeaders().get(\'Content-Type\').includes(\'application/json\')) {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("authToken", data.token);'); + expect(translatedCode).toContain('} else if (res.getStatus() === 404) {'); + expect(translatedCode).toContain('console.error("Request failed with status:", res.getStatus());'); + }); + + it('should handle response processing with try-catch', () => { + const code = ` + try { + const data = pm.response.json(); + pm.environment.set("userData", JSON.stringify(data.user)); + } catch (error) { + console.error("Failed to parse response:", error); + const text = pm.response.text(); + pm.environment.set("rawResponse", text); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('bru.setEnvVar("userData", JSON.stringify(data.user));'); + expect(translatedCode).toContain('const text = JSON.stringify(res.getBody());'); + expect(translatedCode).toContain('bru.setEnvVar("rawResponse", text);'); + }); + + it('should handle JSON path style access to response data', () => { + const code = ` + const data = pm.response.json(); + const userId = data.user.id; + const userEmail = data.user.contact.email; + const firstItem = data.items[0]; + + pm.environment.set("userId", userId); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('const userId = data.user.id;'); + expect(translatedCode).toContain('const userEmail = data.user.contact.email;'); + expect(translatedCode).toContain('const firstItem = data.items[0];'); + expect(translatedCode).toContain('bru.setEnvVar("userId", userId);'); + }); + + it('should handle template literals with response data', () => { + const code = ` + const data = pm.response.json(); + const welcomeMessage = \`Hello, \${data.user.name}! Your ID is \${data.user.id}.\`; + + pm.environment.set("welcomeMessage", welcomeMessage); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('const welcomeMessage = `Hello, ${data.user.name}! Your ID is ${data.user.id}.`;'); + expect(translatedCode).toContain('bru.setEnvVar("welcomeMessage", welcomeMessage);'); + }); + + it('should handle response processing in arrow functions', () => { + const code = ` + const processItems = () => { + const items = pm.response.json().items; + return items.map(item => item.id); + }; + + const itemIds = processItems(); + pm.environment.set("itemIds", JSON.stringify(itemIds)); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('return items.map(item => item.id);'); + expect(translatedCode).toContain('const itemIds = processItems();'); + expect(translatedCode).toContain('bru.setEnvVar("itemIds", JSON.stringify(itemIds));'); + }); + + it('should handle complex inline operations with response data', () => { + const code = ` + const items = pm.response.json().items; + const totalValue = items.reduce((sum, item) => sum + item.price, 0); + const highValueItems = items.filter(item => item.price > 100); + const itemNames = items.map(item => item.name); + + pm.environment.set("totalValue", totalValue); + pm.environment.set("highValueItemCount", highValueItems.length); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const items = res.getBody().items;'); + expect(translatedCode).toContain('const totalValue = items.reduce((sum, item) => sum + item.price, 0);'); + expect(translatedCode).toContain('const highValueItems = items.filter(item => item.price > 100);'); + expect(translatedCode).toContain('const itemNames = items.map(item => item.name);'); + expect(translatedCode).toContain('bru.setEnvVar("totalValue", totalValue);'); + expect(translatedCode).toContain('bru.setEnvVar("highValueItemCount", highValueItems.length);'); + }); + + it('should handle complex test structure with pm.response.to.have.header', () => { + const code = ` + pm.test("Response headers validation", function() { + pm.response.to.have.header("Content-Type", "application/json"); + pm.response.to.have.header("Cache-Control"); + + const responseTime = pm.response.responseTime; + pm.expect(responseTime).to.be.below(1000); + }); + `; + const translatedCode = translateCode(code); + + // Check for test function conversion + expect(translatedCode).toContain('test("Response headers validation", function() {'); + + // Check for header assertions inside the test callback + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json");'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property("Cache-Control".toLowerCase())'); + + // Check that other test assertions are preserved + expect(translatedCode).toContain('const responseTime = res.getResponseTime();'); + expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);'); + }); + + it('should handle dynamic header names in pm.response.to.have.header', () => { + const code = ` + function checkHeaderPresent(headerName) { + pm.response.to.have.header(headerName); + } + + function validateHeader(headerName, expectedValue) { + pm.response.to.have.header(headerName, expectedValue); + } + + checkHeaderPresent("Authorization"); + validateHeader("Content-Type", "application/json"); + `; + const translatedCode = translateCode(code); + + // Check function transformations + expect(translatedCode).toContain('function checkHeaderPresent(headerName) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase())'); + + expect(translatedCode).toContain('function validateHeader(headerName, expectedValue) {'); + expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);'); + + // Check function calls + expect(translatedCode).toContain('checkHeaderPresent("Authorization");'); + expect(translatedCode).toContain('validateHeader("Content-Type", "application/json");'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js new file mode 100644 index 000000000..9ed5ed700 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js @@ -0,0 +1,51 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Scoped Variables', () => { + it.skip('should handle scoped variables correctly', () => { + const code = ` + const response = pm.response; + const status = response.status; + + function test() { + const response = delta.response; + const status = response.status; + console.log(status); + } + ` + const result = translateCode(code); + console.log(result); + expect(result).toBe(` + const status = res.statusText; + + function test() { + const response = delta.response; + const status = response.status; + console.log(status); + } + `) + }) + + it.skip('should handle scoped variables correctly', () => { + const code = ` + const response = delta.response; + const status = response.status; + + function test() { + const response = pm.response; + const status = response.status; + console.log(status); + } + ` + const result = translateCode(code); + console.log(result); + expect(result).toBe(` + const response = delta.response; + const status = response.status; + + function test() { + const status = res.statusText; + console.log(status); + } + `) + }) +}) \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js new file mode 100644 index 000000000..fc3988f1f --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js @@ -0,0 +1,399 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Testing Framework Translation', () => { + // Basic testing framework translations + it('should translate pm.test', () => { + const code = 'pm.test("Status code is 200", function() { pm.response.to.have.status(200); });'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('test("Status code is 200", function() { expect(res.getStatus()).to.equal(200); });'); + }); + + it('should translate pm.expect', () => { + const code = 'pm.expect(jsonData.success).to.be.true;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(jsonData.success).to.be.true;'); + }); + + it('should translate pm.expect.fail', () => { + const code = 'if (!isValid) pm.expect.fail("Data is invalid");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('if (!isValid) expect.fail("Data is invalid");'); + }); + + // Tests with response assertions + it('should translate pm.response.to.have.status in tests', () => { + const code = ` + pm.test("Check environment and call successful", function () { + pm.expect(pm.environment.name).to.equal("ENVIRONMENT_NAME"); + pm.response.to.have.status(200); + });`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + test("Check environment and call successful", function () { + expect(bru.getEnvName()).to.equal("ENVIRONMENT_NAME"); + expect(res.getStatus()).to.equal(200); + });`); + }); + + // Test aliases + it('should handle test aliases', () => { + const code = ` + const { test, expect } = pm; + + test("Status code is 200", function () { + expect(pm.response.code).to.equal(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const { test, expect } = pm'); + expect(translatedCode).toContain('test("Status code is 200", function () {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + // Tests inside different code structures + it('should translate pm commands inside tests with nested functions', () => { + const code = ` + pm.test("Auth flow works", function() { + const response = pm.response.json(); + pm.expect(response.authenticated).to.be.true; + pm.environment.set("userId", response.user.id); + pm.collectionVariables.set("sessionId", response.session.id); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Auth flow works", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(response.authenticated).to.be.true;'); + expect(translatedCode).toContain('bru.setEnvVar("userId", response.user.id);'); + expect(translatedCode).toContain('bru.setVar("sessionId", response.session.id);'); + }); + + it('should translate pm.test with arrow functions', () => { + const code = ` + pm.test("Status code is 200", () => { + pm.expect(pm.response.code).to.eql(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Status code is 200", () => {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.eql(200);'); + }); + + it('should handle multiple test assertions in one function', () => { + const code = ` + pm.test("The response has all properties", () => { + const responseJson = pm.response.json(); + pm.expect(responseJson.type).to.eql('vip'); + pm.expect(responseJson.name).to.be.a('string'); + pm.expect(responseJson.id).to.have.lengthOf(1); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("The response has all properties", () => {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('expect(responseJson.type).to.eql(\'vip\');'); + expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');'); + expect(translatedCode).toContain('expect(responseJson.id).to.have.lengthOf(1);'); + }); + + // Test with aliased variables + it('should translate aliases within test functions', () => { + const code = ` + const tempRes = pm.response; + const tempTest = pm.test; + const tempExpect = pm.expect; + + tempTest("Status code is 200", function() { + tempExpect(tempRes.code).to.equal(200); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).not.toContain('const tempRes = pm.response;'); + expect(translatedCode).not.toContain('const tempTest = pm.test;'); + expect(translatedCode).not.toContain('const tempExpect = pm.expect;'); + expect(translatedCode).toContain('test("Status code is 200", function() {'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + }); + + // Additional robust tests for testing framework + it('should handle nested test functions', () => { + const code = ` + pm.test("Main test group", function() { + const responseJson = pm.response.json(); + + pm.test("User data validation", function() { + pm.expect(responseJson.user).to.be.an('object'); + pm.expect(responseJson.user.id).to.be.a('string'); + }); + + pm.test("Settings validation", function() { + pm.expect(responseJson.settings).to.be.an('object'); + pm.expect(responseJson.settings.notifications).to.be.a('boolean'); + }); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Main test group", function() {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('test("User data validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.user).to.be.an(\'object\');'); + expect(translatedCode).toContain('test("Settings validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.settings.notifications).to.be.a(\'boolean\');'); + }); + + it('should handle test with dynamic test names', () => { + const code = ` + const endpoint = pm.variables.get("currentEndpoint"); + + pm.test(\`\${endpoint} returns correct data\`, function() { + const responseJson = pm.response.json(); + pm.expect(responseJson).to.be.an('object'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const endpoint = bru.getVar("currentEndpoint");'); + expect(translatedCode).toContain('test(`${endpoint} returns correct data`, function() {'); + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('expect(responseJson).to.be.an(\'object\');'); + }); + + it('should handle test with conditional execution', () => { + const code = ` + const responseJson = pm.response.json(); + + if (responseJson.type === 'user') { + pm.test("User validation", function() { + pm.expect(responseJson.name).to.be.a('string'); + pm.expect(responseJson.email).to.be.a('string'); + }); + } else if (responseJson.type === 'admin') { + pm.test("Admin validation", function() { + pm.expect(responseJson.accessLevel).to.be.above(5); + pm.expect(responseJson.permissions).to.be.an('array'); + }); + } + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const responseJson = res.getBody();'); + expect(translatedCode).toContain('if (responseJson.type === \'user\') {'); + expect(translatedCode).toContain('test("User validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\'string\');'); + expect(translatedCode).toContain('} else if (responseJson.type === \'admin\') {'); + expect(translatedCode).toContain('test("Admin validation", function() {'); + expect(translatedCode).toContain('expect(responseJson.accessLevel).to.be.above(5);'); + }); + + it('should handle assertions with logical operators', () => { + const code = ` + pm.test("Response has valid structure", function() { + const data = pm.response.json(); + + pm.expect(data.id && data.name).to.be.ok; + pm.expect(data.active || data.pending).to.be.true; + pm.expect(!data.deleted).to.be.true; + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Response has valid structure", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.id && data.name).to.be.ok;'); + expect(translatedCode).toContain('expect(data.active || data.pending).to.be.true;'); + expect(translatedCode).toContain('expect(!data.deleted).to.be.true;'); + }); + + it('should handle array and object assertions', () => { + const code = ` + pm.test("Array and object validations", function() { + const data = pm.response.json(); + + // Array validations + pm.expect(data.items).to.be.an('array'); + pm.expect(data.items).to.have.lengthOf.at.least(1); + pm.expect(data.items[0]).to.have.property('id'); + + // Object validations + pm.expect(data.user).to.be.an('object'); + pm.expect(data.user).to.have.all.keys('id', 'name', 'email'); + pm.expect(data.user).to.include({active: true}); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Array and object validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.items).to.be.an(\'array\');'); + expect(translatedCode).toContain('expect(data.items).to.have.lengthOf.at.least(1);'); + expect(translatedCode).toContain('expect(data.items[0]).to.have.property(\'id\');'); + expect(translatedCode).toContain('expect(data.user).to.be.an(\'object\');'); + expect(translatedCode).toContain('expect(data.user).to.have.all.keys(\'id\', \'name\', \'email\');'); + expect(translatedCode).toContain('expect(data.user).to.include({active: true});'); + }); + + it('should handle chai assertions with deep equality', () => { + const code = ` + pm.test("Deep equality checks", function() { + const data = pm.response.json(); + + pm.expect(data.config).to.deep.equal({ + version: "1.0", + active: true, + features: ["search", "export"] + }); + + pm.expect(data.tags).to.have.members(['api', 'test']); + pm.expect(data.meta).to.deep.include({format: 'json'}); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Deep equality checks", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.config).to.deep.equal({'); + expect(translatedCode).toContain('version: "1.0",'); + expect(translatedCode).toContain('active: true,'); + expect(translatedCode).toContain('features: ["search", "export"]'); + expect(translatedCode).toContain('expect(data.tags).to.have.members([\'api\', \'test\']);'); + expect(translatedCode).toContain('expect(data.meta).to.deep.include({format: \'json\'});'); + }); + + it('should handle chai assertions with string comparisons', () => { + const code = ` + pm.test("String validations", function() { + const data = pm.response.json(); + + pm.expect(data.id).to.be.a('string'); + pm.expect(data.name).to.match(/^[A-Za-z\\s]+$/); + pm.expect(data.description).to.include('API'); + pm.expect(data.url).to.have.string('api/v1'); + pm.expect(data.code).to.have.lengthOf(8); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("String validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.id).to.be.a(\'string\');'); + expect(translatedCode).toContain('expect(data.name).to.match(/^[A-Za-z\\s]+$/);'); + expect(translatedCode).toContain('expect(data.description).to.include(\'API\');'); + expect(translatedCode).toContain('expect(data.url).to.have.string(\'api/v1\');'); + expect(translatedCode).toContain('expect(data.code).to.have.lengthOf(8);'); + }); + + it('should handle assertions with numeric comparisons', () => { + const code = ` + pm.test("Numeric validations", function() { + const data = pm.response.json(); + + pm.expect(data.count).to.be.a('number'); + pm.expect(data.count).to.be.above(0); + pm.expect(data.price).to.be.within(10, 100); + pm.expect(data.discount).to.be.at.most(25); + pm.expect(data.quantity * data.price).to.equal(data.total); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Numeric validations", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('expect(data.count).to.be.a(\'number\');'); + expect(translatedCode).toContain('expect(data.count).to.be.above(0);'); + expect(translatedCode).toContain('expect(data.price).to.be.within(10, 100);'); + expect(translatedCode).toContain('expect(data.discount).to.be.at.most(25);'); + expect(translatedCode).toContain('expect(data.quantity * data.price).to.equal(data.total);'); + }); + + it('should handle pm.expect.fail with conditions', () => { + const code = ` + pm.test("Validate critical fields", function() { + const data = pm.response.json(); + + if (!data.id) { + pm.expect.fail("Missing ID field"); + } + + if (data.status !== 'active' && data.status !== 'pending') { + pm.expect.fail("Invalid status: " + data.status); + } + + // Continue with normal assertions + pm.expect(data.name).to.be.a('string'); + }); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('test("Validate critical fields", function() {'); + expect(translatedCode).toContain('const data = res.getBody();'); + expect(translatedCode).toContain('if (!data.id) {'); + expect(translatedCode).toContain('expect.fail("Missing ID field");'); + expect(translatedCode).toContain('if (data.status !== \'active\' && data.status !== \'pending\') {'); + expect(translatedCode).toContain('expect.fail("Invalid status: " + data.status);'); + expect(translatedCode).toContain('expect(data.name).to.be.a(\'string\');'); + }); + + it('should handle complex test compositions', () => { + const code = ` + // Helper function + function validateUserObject(user) { + pm.expect(user).to.be.an('object'); + pm.expect(user.id).to.be.a('string'); + pm.expect(user.name).to.be.a('string'); + return user.id && user.name; + } + + pm.test("Response validation", function() { + const response = pm.response.json(); + const validUsers = []; + + // Test status code + pm.response.to.have.status(200); + + // Test main user + if (response.user) { + const isValid = validateUserObject(response.user); + if (isValid) { + validUsers.push(response.user); + } + } + + // Test related users + if (response.relatedUsers && Array.isArray(response.relatedUsers)) { + pm.test("Related users validation", function() { + response.relatedUsers.forEach((user, index) => { + pm.test(\`User at index \${index}\`, function() { + const isValid = validateUserObject(user); + if (isValid) { + validUsers.push(user); + } + }); + }); + }); + } + + // Set the valid users for later use + if (validUsers.length > 0) { + pm.environment.set("validUsers", JSON.stringify(validUsers)); + } + }); + `; + const translatedCode = translateCode(code); + + // Test key transformations + expect(translatedCode).toContain('function validateUserObject(user) {'); + expect(translatedCode).toContain('expect(user).to.be.an(\'object\');'); + expect(translatedCode).toContain('test("Response validation", function() {'); + expect(translatedCode).toContain('const response = res.getBody();'); + expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);'); + expect(translatedCode).toContain('test("Related users validation", function() {'); + expect(translatedCode).toContain('test(`User at index ${index}`, function() {'); + expect(translatedCode).toContain('bru.setEnvVar("validUsers", JSON.stringify(validUsers));'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js new file mode 100644 index 000000000..3c700000e --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js @@ -0,0 +1,91 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Variable Chaining Resolution', () => { + test('should resolve a simple variable chain (variable pointing to another variable)', () => { + const code = ` + const original = pm.response; + const alias = original; + const data = alias.json(); + `; + + const translatedCode = translateCode(code); + + // Check that alias.json() was properly resolved to res.getBody() + expect(translatedCode).toContain('const data = res.getBody();'); + // The original variable declarations should be removed + expect(translatedCode).not.toContain('const original ='); + expect(translatedCode).not.toContain('const alias ='); + }); + + test('should handle mixed variable references correctly', () => { + const code = ` + const respVar = pm.response; + const envVar = pm.environment; + const respAlias = respVar; + + // These should be replaced + const statusCode = respAlias.code; + const envValue = envVar.get("key"); + + // This should not be replaced + const unrelatedVar = "some value"; + `; + + const translatedCode = translateCode(code); + + // Check correct replacements + expect(translatedCode).not.toContain('const respVar'); + expect(translatedCode).not.toContain('const envVar'); + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const envValue = bru.getEnvVar("key");'); + + // Check that unrelated variables are preserved + expect(translatedCode).toContain('const unrelatedVar = "some value";'); + }); + + /** + * This test verifies that when multiple variables are declared in a single statement, + * only the ones referencing Postman objects are removed and the others are preserved. + * + * For example, in a statement like: + * const response = pm.response, counter = 5, helper = "test"; + * + * Only 'response' should be removed, resulting in: + * const counter = 5, helper = "test"; + */ + test('should handle multiple variables in one declaration statement', () => { + const code = ` + // Multiple variables in one declaration, with a mix of Postman objects and regular variables + const response = pm.response, counter = 5, helper = "test"; + + // Using both the Postman reference (should be replaced) and regular values (should be preserved) + const statusCode = response.code; + console.log("Counter value:", counter); + console.log("Helper string:", helper); + + // Another example with different Postman object + let env = pm.environment, timeout = 1000, isValid = true; + const baseUrl = env.get("baseUrl"); + `; + + const translatedCode = translateCode(code); + + // Postman references should be replaced + expect(translatedCode).not.toContain('response = pm.response'); + expect(translatedCode).not.toContain('env = pm.environment'); + + // Regular variables should be preserved + expect(translatedCode).toContain('const counter = 5'); + expect(translatedCode).toContain('helper = "test"'); + expect(translatedCode).toContain('timeout = 1000'); + expect(translatedCode).toContain('isValid = true'); + + // References to Postman objects should be properly translated + expect(translatedCode).toContain('const statusCode = res.getStatus();'); + expect(translatedCode).toContain('const baseUrl = bru.getEnvVar("baseUrl");'); + + // Console logs with regular variables should be preserved + expect(translatedCode).toContain('console.log("Counter value:", counter);'); + expect(translatedCode).toContain('console.log("Helper string:", helper);'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js new file mode 100644 index 000000000..b704e4a7e --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js @@ -0,0 +1,128 @@ +import translateCode from '../../../../src/utils/jscode-shift-translator'; + +describe('Variables Translation', () => { + // Regular variables tests + it('should translate pm.variables.get', () => { + const code = 'pm.variables.get("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("test");'); + }); + + it('should translate pm.variables.set', () => { + const code = 'pm.variables.set("test", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("test", "value");'); + }); + + it('should translate pm.variables.has', () => { + const code = 'pm.variables.has("userId");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("userId");'); + }); + + // Collection variables tests + it('should translate pm.collectionVariables.get', () => { + const code = 'pm.collectionVariables.get("apiUrl");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("apiUrl");'); + }); + + it('should translate pm.collectionVariables.set', () => { + const code = 'pm.collectionVariables.set("token", jsonData.token);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("token", jsonData.token);'); + }); + + it('should translate pm.collectionVariables.has', () => { + const code = 'pm.collectionVariables.has("authToken");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("authToken");'); + }); + + it('should translate pm.collectionVariables.unset', () => { + const code = 'pm.collectionVariables.unset("tempVar");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteVar("tempVar");'); + }); + + // Alias tests for variables + it('should handle variables aliases', () => { + const code = ` + const vars = pm.variables; + const has = vars.has("test"); + const set = vars.set("test", "value"); + const get = vars.get("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const has = bru.hasVar("test"); + const set = bru.setVar("test", "value"); + const get = bru.getVar("test"); + `); + }); + + // Alias tests for collection variables + it('should handle collection variables aliases', () => { + const code = ` + const collVars = pm.collectionVariables; + const has = collVars.has("test"); + const set = collVars.set("test", "value"); + const get = collVars.get("test"); + const unset = collVars.unset("test"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const has = bru.hasVar("test"); + const set = bru.setVar("test", "value"); + const get = bru.getVar("test"); + const unset = bru.deleteVar("test"); + `); + }); + + // Combined tests + it('should handle conditional expressions with variable calls', () => { + const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";'); + }); + + it('should handle all variable methods together', () => { + const code = ` + // All variable methods + const hasUserId = pm.variables.has("userId"); + const userId = pm.variables.get("userId"); + pm.variables.set("requestTime", new Date().toISOString()); + + console.log(\`Has userId: \${hasUserId}, User ID: \${userId}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const hasUserId = bru.hasVar("userId");'); + expect(translatedCode).toContain('const userId = bru.getVar("userId");'); + expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());'); + }); + + it('should handle all collection variable methods together', () => { + const code = ` + // All collection variable methods + const hasApiUrl = pm.collectionVariables.has("apiUrl"); + const apiUrl = pm.collectionVariables.get("apiUrl"); + pm.collectionVariables.set("requestTime", new Date().toISOString()); + pm.collectionVariables.unset("tempVar"); + + console.log(\`Has API URL: \${hasApiUrl}, API URL: \${apiUrl}\`); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toContain('const hasApiUrl = bru.hasVar("apiUrl");'); + expect(translatedCode).toContain('const apiUrl = bru.getVar("apiUrl");'); + expect(translatedCode).toContain('bru.setVar("requestTime", new Date().toISOString());'); + expect(translatedCode).toContain('bru.deleteVar("tempVar");'); + }); + + it('should handle more complex nested expressions with variables', () => { + const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js b/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js new file mode 100644 index 000000000..f5dd69f09 --- /dev/null +++ b/packages/bruno-converters/tests/utils/getMemberExpressionString.test.js @@ -0,0 +1,53 @@ +import { describe, it, expect } from '@jest/globals'; +import { getMemberExpressionString } from '../../src/utils/jscode-shift-translator'; +const j = require('jscodeshift'); + +describe('getMemberExpressionString', () => { + it('should correctly convert simple member expressions to strings', () => { + // Create a simple member expression: pm.environment.get + const memberExpr = j.memberExpression( + j.memberExpression( + j.identifier('pm'), + j.identifier('environment') + ), + j.identifier('get') + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('pm.environment.get'); + }); + + it('should handle computed properties with string literals', () => { + // Create a computed member expression: pm["environment"]["get"] + const memberExpr = j.memberExpression( + j.memberExpression( + j.identifier('pm'), + j.literal('environment'), + true // computed + ), + j.literal('get'), + true // computed + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('pm.environment.get'); + }); + + it('should mark non-string computed properties as [computed]', () => { + // Create a computed member expression with variable: obj[varName] + const memberExpr = j.memberExpression( + j.identifier('obj'), + j.identifier('varName'), + true // computed + ); + + const result = getMemberExpressionString(memberExpr); + expect(result).toBe('obj.[computed]'); + }); + + it('should handle basic identifiers', () => { + const identifier = j.identifier('pm'); + const result = getMemberExpressionString(identifier); + expect(result).toBe('pm'); + }); +}); \ No newline at end of file diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index dbc921211..26f327b73 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -28,6 +28,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@faker-js/faker": "^9.5.1", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 443c5e956..b0924bb6b 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -6,6 +6,8 @@ const os = require('os'); const path = require('path'); const { ipcMain, shell, dialog, app } = require('electron'); const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); +const brunoConverters = require('@usebruno/converters'); +const { postmanToBruno } = brunoConverters; const { writeFile, @@ -1151,6 +1153,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw error; } }); + + // Implement the Postman to Bruno conversion handler + ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => { + try { + // Convert Postman collection to Bruno format + const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true}); + + return brunoCollection; + } catch (error) { + console.error('Error converting Postman to Bruno:', error); + return Promise.reject(error); + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json index ada36145a..d2aa0a97a 100644 --- a/packages/bruno-tests/collection/bruno.json +++ b/packages/bruno-tests/collection/bruno.json @@ -28,4 +28,4 @@ "requestType": "http", "requestUrl": "http://localhost:6000" } -} +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo headers.bru b/packages/bruno-tests/collection/echo/echo headers.bru new file mode 100644 index 000000000..9f6571109 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo headers.bru @@ -0,0 +1,22 @@ +meta { + name: echo headers + type: http + seq: 13 +} + +post { + url: {{echo-host}} + body: none + auth: inherit +} + +headers { + Custom-Header-String: bruno +} + +tests { + test("test headers",function() { + expect(res.getHeaders()).to.have.property("Custom-Header-String".toLowerCase()) + expect(res.getHeaders()).to.have.property("Custom-Header-String".toLowerCase(), "bruno") + }) +}