diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 31ddd34f5..6db1fd370 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -13,6 +13,9 @@ const STATIC_API_HINTS = { 'req.timeout', 'req.getUrl()', 'req.setUrl(url)', + 'req.getHost()', + 'req.getPath()', + 'req.getQueryString()', 'req.getMethod()', 'req.getAuthMode()', 'req.setMethod(method)', @@ -27,6 +30,7 @@ const STATIC_API_HINTS = { 'req.setTimeout(timeout)', 'req.getExecutionMode()', 'req.getName()', + 'req.getPathParams()', 'req.getTags()', 'req.disableParsingResponseJson()', 'req.onFail(function(err) {})' diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 29bad3715..5ca8a9c7b 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -38,10 +38,18 @@ const replacements = { // Supported Postman request translations: // - pm.request.url / request.url -> req.getUrl() + // - pm.request.url.getHost() -> req.getHost() + // - pm.request.url.getPath() -> req.getPath() + // - pm.request.url.getQueryString() -> req.getQueryString() + // - pm.request.url.variables -> req.getPathParams() // - pm.request.method / request.method -> req.getMethod() // - pm.request.headers / request.headers -> req.getHeaders() // - pm.request.body / request.body -> req.getBody() // - pm.info.requestName / request.name -> req.getName() + 'pm\\.request\\.url\\.getHost\\(\\)': 'req.getHost()', + 'pm\\.request\\.url\\.getPath\\(\\)': 'req.getPath()', + 'pm\\.request\\.url\\.getQueryString\\(\\)': 'req.getQueryString()', + 'pm\\.request\\.url\\.variables': 'req.getPathParams()', 'pm\\.request\\.url': 'req.getUrl()', 'pm\\.request\\.method': 'req.getMethod()', 'pm\\.request\\.headers': 'req.getHeaders()', diff --git a/packages/bruno-converters/src/utils/postman-to-bruno-translator.js b/packages/bruno-converters/src/utils/postman-to-bruno-translator.js index 4912f1980..7a21d07e1 100644 --- a/packages/bruno-converters/src/utils/postman-to-bruno-translator.js +++ b/packages/bruno-converters/src/utils/postman-to-bruno-translator.js @@ -38,6 +38,10 @@ const simpleTranslations = { 'pm.info.requestName': 'req.getName()', // Request properties (pm.request.*) + 'pm.request.url.getHost': 'req.getHost', + 'pm.request.url.getPath': 'req.getPath', + 'pm.request.url.getQueryString': 'req.getQueryString', + 'pm.request.url.variables': 'req.getPathParams()', 'pm.request.url': 'req.getUrl()', 'pm.request.method': 'req.getMethod()', 'pm.request.headers': 'req.getHeaders()', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js index 333cedcbc..f50632a4d 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js @@ -42,4 +42,20 @@ describe('postmanTranslations - request commands', () => { `; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); + + test('should handle pm.request.url helper methods', () => { + const inputScript = ` + const host = pm.request.url.getHost(); + const path = pm.request.url.getPath(); + const queryString = pm.request.url.getQueryString(); + const pathVariables = pm.request.url.variables; + `; + const expectedOutput = ` + const host = req.getHost(); + const path = req.getPath(); + const queryString = req.getQueryString(); + const pathVariables = req.getPathParams(); + `; + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index d48a85d30..7aea48f39 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -332,9 +332,6 @@ const configureRequest = async ( request.url = urlObj.toString(); } - // Remove pathParams, already in URL (Issue #2439) - delete request.pathParams; - // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL delete request.apiKeyAuthValueForQueryParams; diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index 12275fd53..02f8e6c19 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -18,6 +18,7 @@ class BrunoRequest { this.headers = req.headers; this.timeout = req.timeout; this.name = req.name; + this.pathParams = req.pathParams; this.tags = req.tags || []; /** * We automatically parse the JSON body if the content type is JSON @@ -41,6 +42,53 @@ class BrunoRequest { this.req.url = url; } + getHost() { + try { + const url = new URL(this.req.url); + return url.host; + } catch (e) { + return ''; + } + } + + getPath() { + try { + const url = new URL(this.req.url); + let pathname = url.pathname; + + // If path params exist, interpolate them into the pathname + if (this.req.pathParams && Array.isArray(this.req.pathParams)) { + pathname = pathname + .split('/') + .map((segment) => { + if (segment.startsWith(':')) { + const paramName = segment.slice(1); + const pathParam = this.req.pathParams.find((param) => param.name === paramName); + if (pathParam && pathParam.value) { + return pathParam.value; + } + } + return segment; + }) + .join('/'); + } + + return pathname; + } catch (e) { + return ''; + } + } + + getQueryString() { + try { + const url = new URL(this.req.url); + // Return query string without the leading '?' + return url.search ? url.search.substring(1) : ''; + } catch (e) { + return ''; + } + } + getMethod() { return this.req.method; } @@ -191,6 +239,16 @@ class BrunoRequest { return this.req.name; } + getPathParams() { + const params = Array.isArray(this.req.pathParams) ? this.req.pathParams : []; + + return params.map((param) => ({ + name: param.name, + value: param.value, + type: param.type + })); + } + /** * Get the tags associated with this request * @returns {Array} Array of tag strings diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js index df014b9c6..b65acffca 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js @@ -9,6 +9,7 @@ const addBrunoRequestShimToContext = (vm, req) => { const body = marshallToVm(req.getBody(), vm); const timeout = marshallToVm(req.getTimeout(), vm); const name = marshallToVm(req.getName(), vm); + const pathParams = marshallToVm(req.getPathParams(), vm); const tags = marshallToVm(req.getTags(), vm); vm.setProp(reqObject, 'url', url); @@ -17,6 +18,7 @@ const addBrunoRequestShimToContext = (vm, req) => { vm.setProp(reqObject, 'body', body); vm.setProp(reqObject, 'timeout', timeout); vm.setProp(reqObject, 'name', name); + vm.setProp(reqObject, 'pathParams', pathParams); vm.setProp(reqObject, 'tags', tags); url.dispose(); @@ -25,6 +27,7 @@ const addBrunoRequestShimToContext = (vm, req) => { body.dispose(); timeout.dispose(); name.dispose(); + pathParams.dispose(); tags.dispose(); let getUrl = vm.newFunction('getUrl', function () { @@ -39,6 +42,24 @@ const addBrunoRequestShimToContext = (vm, req) => { vm.setProp(reqObject, 'setUrl', setUrl); setUrl.dispose(); + let getHost = vm.newFunction('getHost', function () { + return marshallToVm(req.getHost(), vm); + }); + vm.setProp(reqObject, 'getHost', getHost); + getHost.dispose(); + + let getPath = vm.newFunction('getPath', function () { + return marshallToVm(req.getPath(), vm); + }); + vm.setProp(reqObject, 'getPath', getPath); + getPath.dispose(); + + let getQueryString = vm.newFunction('getQueryString', function () { + return marshallToVm(req.getQueryString(), vm); + }); + vm.setProp(reqObject, 'getQueryString', getQueryString); + getQueryString.dispose(); + let getMethod = vm.newFunction('getMethod', function () { return marshallToVm(req.getMethod(), vm); }); @@ -57,6 +78,12 @@ const addBrunoRequestShimToContext = (vm, req) => { vm.setProp(reqObject, 'getName', getName); getName.dispose(); + let getPathParams = vm.newFunction('getPathParams', function () { + return marshallToVm(req.getPathParams(), vm); + }); + vm.setProp(reqObject, 'getPathParams', getPathParams); + getPathParams.dispose(); + let setMethod = vm.newFunction('setMethod', function (method) { req.setMethod(vm.dump(method)); }); diff --git a/packages/bruno-tests/collection/scripting/api/req/getHost.bru b/packages/bruno-tests/collection/scripting/api/req/getHost.bru new file mode 100644 index 000000000..52c40bb5e --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/req/getHost.bru @@ -0,0 +1,23 @@ +meta { + name: getHost + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +assert { + res.status: eq 200 + res.body: eq pong +} + +tests { + test("req.getHost()", function() { + const host = req.getHost(); + expect(host).to.equal("testbench-sanity.usebruno.com"); + }); +} diff --git a/packages/bruno-tests/collection/scripting/api/req/getPath.bru b/packages/bruno-tests/collection/scripting/api/req/getPath.bru new file mode 100644 index 000000000..038c48d7c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/req/getPath.bru @@ -0,0 +1,18 @@ +meta { + name: getPath + type: http + seq: 1 +} + +get { + url: {{host}}/api/users/123 + body: none + auth: none +} + +tests { + test("req.getPath()", function() { + const path = req.getPath(); + expect(path).to.equal("/api/users/123"); + }); +} diff --git a/packages/bruno-tests/collection/scripting/api/req/getPathParams.bru b/packages/bruno-tests/collection/scripting/api/req/getPathParams.bru new file mode 100644 index 000000000..71fa92ae2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/req/getPathParams.bru @@ -0,0 +1,23 @@ +meta { + name: getPathParam + type: http + seq: 1 +} + +get { + url: {{host}}/:pathParam + body: none + auth: none +} + +params:path { + pathParam: ping +} + +tests { + test("req.getPathParams()", function() { + const pathParams = req.getPathParams(); + expect(pathParams[0].name).to.equal('pathParam'); + expect(pathParams[0].value).to.equal('ping'); + }); +} diff --git a/packages/bruno-tests/collection/scripting/api/req/getQueryString.bru b/packages/bruno-tests/collection/scripting/api/req/getQueryString.bru new file mode 100644 index 000000000..e63345a85 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/req/getQueryString.bru @@ -0,0 +1,29 @@ +meta { + name: getQueryString + type: http + seq: 1 +} + +get { + url: {{host}}/ping?page=1&limit=10&sort=desc + body: none + auth: none +} + +params:query { + page: 1 + limit: 10 + sort: desc +} + +assert { + res.status: eq 200 + res.body: eq pong +} + +tests { + test("req.getQueryString()", function() { + const queryString = req.getQueryString(); + expect(queryString).to.equal("page=1&limit=10&sort=desc"); + }); +} diff --git a/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/bruno.json b/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/bruno.json new file mode 100644 index 000000000..9037456e1 --- /dev/null +++ b/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "url_helpers_test", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/url-helpers-test.bru b/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/url-helpers-test.bru new file mode 100644 index 000000000..9c1e50791 --- /dev/null +++ b/tests/scripting/url-helpers/fixtures/collections/url_helpers_test/url-helpers-test.bru @@ -0,0 +1,84 @@ +meta { + name: url-helpers-test + type: http + seq: 1 +} + +get { + url: https://echo.usebruno.com/api/users/:userId?name=john&age=30 + body: none + auth: none +} + +params:path { + userId: 123 +} + +script:pre-request { + // Test URL helper methods in pre-request script + const host = req.getHost(); + const path = req.getPath(); + const queryString = req.getQueryString(); + const pathParams = req.getPathParams(); + + // Store values for verification in tests + bru.setVar('preReqHost', host); + bru.setVar('preReqPath', path); + bru.setVar('preReqQueryString', queryString); + bru.setVar('preReqPathParams', JSON.stringify(pathParams)); +} + +script:post-response { + // Test URL helper methods in post-response script + const host = req.getHost(); + const path = req.getPath(); + const queryString = req.getQueryString(); + const pathParams = req.getPathParams(); + + // Store values for verification in tests + bru.setVar('postResHost', host); + bru.setVar('postResPath', path); + bru.setVar('postResQueryString', queryString); + bru.setVar('postResPathParams', JSON.stringify(pathParams)); +} + +tests { + test("getHost() returns correct host", function() { + const preReqHost = bru.getVar('preReqHost'); + const postResHost = bru.getVar('postResHost'); + + expect(preReqHost).to.equal('echo.usebruno.com'); + expect(postResHost).to.equal('echo.usebruno.com'); + }); + + test("getPath() returns correct path", function() { + const preReqPath = bru.getVar('preReqPath'); + const postResPath = bru.getVar('postResPath'); + + expect(preReqPath).to.equal('/api/users/123'); + expect(postResPath).to.equal('/api/users/123'); + }); + + test("getQueryString() returns correct query string", function() { + const preReqQueryString = bru.getVar('preReqQueryString'); + const postResQueryString = bru.getVar('postResQueryString'); + + expect(preReqQueryString).to.equal('name=john&age=30'); + expect(postResQueryString).to.equal('name=john&age=30'); + }); + + test("getPathParams() returns correct path parameters", function() { + const preReqPathParams = JSON.parse(bru.getVar('preReqPathParams')); + const postResPathParams = JSON.parse(bru.getVar('postResPathParams')); + + expect(preReqPathParams).to.be.an('array'); + expect(preReqPathParams).to.have.lengthOf(1); + expect(preReqPathParams[0].name).to.equal('userId'); + expect(preReqPathParams[0].value).to.equal('123'); + + expect(postResPathParams).to.be.an('array'); + expect(postResPathParams).to.have.lengthOf(1); + expect(postResPathParams[0].name).to.equal('userId'); + expect(postResPathParams[0].value).to.equal('123'); + }); +} diff --git a/tests/scripting/url-helpers/init-user-data/preferences.json b/tests/scripting/url-helpers/init-user-data/preferences.json new file mode 100644 index 000000000..05eef945c --- /dev/null +++ b/tests/scripting/url-helpers/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/scripting/url-helpers/fixtures/collections/url_helpers_test" + ] +} diff --git a/tests/scripting/url-helpers/url-helpers.spec.ts b/tests/scripting/url-helpers/url-helpers.spec.ts new file mode 100644 index 000000000..3587b4edd --- /dev/null +++ b/tests/scripting/url-helpers/url-helpers.spec.ts @@ -0,0 +1,38 @@ +import { test } from '../../../playwright'; +import { setSandboxMode, runCollection, validateRunnerResults } from '../../utils/page'; + +test.describe.serial('URL helper methods', () => { + test.describe('req.getHost(), req.getPath(), req.getQueryString(), req.getPathParams()', () => { + test('should work in developer mode', async ({ pageWithUserData: page }) => { + // Set up developer mode + await setSandboxMode(page, 'url_helpers_test', 'developer'); + + // Run the collection + await runCollection(page, 'url_helpers_test'); + + // Validate test results - 1 request should pass (with 4 assertions inside) + await validateRunnerResults(page, { + totalRequests: 1, + passed: 1, + failed: 0, + skipped: 0 + }); + }); + + test('should work in safe mode', async ({ pageWithUserData: page }) => { + // Set up safe mode + await setSandboxMode(page, 'url_helpers_test', 'safe'); + + // Run the collection + await runCollection(page, 'url_helpers_test'); + + // Validate test results - 1 request should pass in safe mode too (with 4 assertions inside) + await validateRunnerResults(page, { + totalRequests: 1, + passed: 1, + failed: 0, + skipped: 0 + }); + }); + }); +});