From 40b44de294ab3117a29bf54d00851150fd16fb42 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 23 Sep 2025 09:01:00 +0200 Subject: [PATCH 1/4] Support for Odata style path params (#5048) * Support for Odata style path params * Remove test statement * Update packages/bruno-app/src/utils/url/index.spec.js Add more testcases Co-authored-by: sid-bruno * Performance improvements * Add testcases for odata style url params --------- Co-authored-by: sid-bruno --- packages/bruno-app/src/utils/url/index.js | 45 +++++-- .../bruno-app/src/utils/url/index.spec.js | 126 ++++++++++++++++++ .../bruno-cli/src/runner/interpolate-vars.js | 15 ++- .../src/ipc/network/interpolate-vars.js | 23 ++-- 4 files changed, 181 insertions(+), 28 deletions(-) diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index a8ac1b812..ae8d10c11 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -32,19 +32,24 @@ export const parsePathParams = (url) => { paths = uri.split('/'); } - paths = paths.reduce((acc, path) => { - if (path !== '' && path[0] === ':') { - let name = path.slice(1, path.length); - if (name) { - let isExist = find(acc, (path) => path.name === name); - if (!isExist) { - acc.push({ name: path.slice(1, path.length), value: '' }); + // Enhanced: also match :param inside parentheses and/or quotes + const paramRegex = /[:](\w+)/g; + const foundParams = new Set(); + paths.forEach((segment) => { + let match; + while ((match = paramRegex.exec(segment))) { + if (match[1]) { + // Clean up: remove trailing quotes/parentheses if present + let name = match[1].replace(/[')"`]+$/, ''); + // Remove leading quotes/parentheses if present + name = name.replace(/^[('"`]+/, ''); + if (name && !foundParams.has(name)) { + foundParams.add(name); } } } - return acc; - }, []); - return paths; + }); + return Array.from(foundParams).map((name) => ({ name, value: '' })); }; export const splitOnFirst = (str, char) => { @@ -79,13 +84,25 @@ export const interpolateUrl = ({ url, variables }) => { export const interpolateUrlPathParams = (url, params) => { const getInterpolatedBasePath = (pathname, params) => { + const regex = /[:](\w+)/g; return pathname .split('/') .map((segment) => { - if (segment.startsWith(':')) { - const pathParamName = segment.slice(1); - const pathParam = params.find((p) => p?.name === pathParamName && p?.type === 'path'); - return pathParam ? pathParam.value : segment; + + if(!segment.startsWith(":")) return segment + + let match; + while ((match = regex.exec(segment))) { + if (match[1]) { + // Clean up: remove trailing quotes/parentheses if present + let name = match[1].replace(/[')"`]+$/, ''); + // Remove leading quotes/parentheses if present + name = name.replace(/^[('"`]+/, ''); + if (name) { + const pathParam = params.find((p) => p?.name === name && p?.type === 'path'); + return pathParam ? pathParam.value : segment; + } + } } return segment; }) diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index bbcc919c8..c75ce63c8 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -43,6 +43,132 @@ describe('Url Utils - parsePathParams', () => { { name: 'postId', value: '' } ]); }); + + it('should parse path param inside parentheses and quotes', () => { + const params = parsePathParams("https://example.com/ExchangeRates(':ExchangeRateOID')"); + expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse path param inside parentheses and no quotes', () => { + const params = parsePathParams("https://example.com/ExchangeRates(:ExchangeRateOID)"); + expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse multiple path params inside parentheses', () => { + const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)"); + expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse mix and match of normal and param inside parentheses', () => { + const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/:key"); + expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'key', value: '' }]); + }); + + // OData-specific test cases for enhanced path parameter parsing + it('should parse OData entity key with single quotes', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData entity key with double quotes', () => { + const params = parsePathParams('https://example.com/odata/Products(":productId")'); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData entity key with backticks', () => { + const params = parsePathParams('https://example.com/odata/Products(`:productId`)'); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData entity key with parentheses only', () => { + const params = parsePathParams("https://example.com/odata/Products(:productId)"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData composite key with multiple parameters', () => { + const params = parsePathParams("https://example.com/odata/Orders(:orderId,ProductId=':productId')"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); + }); + + it('should parse OData navigation property with key', () => { + const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]); + }); + + it('should parse OData function with parameters', () => { + const params = parsePathParams("https://example.com/odata/GetProductsByCategory(categoryId=':categoryId')"); + expect(params).toEqual([{ name: 'categoryId', value: '' }]); + }); + + it('should parse OData action with complex parameters', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')/Rate(rating=:rating,comment=':comment')"); + expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]); + }); + + it('should handle OData parameters with special characters in names', () => { + const params = parsePathParams("https://example.com/odata/Products(':product-id')"); + expect(params).toEqual([{ name: 'product', value: '' }]); + }); + + it('should handle OData parameters with underscores in names', () => { + const params = parsePathParams("https://example.com/odata/Products(':product_id')"); + expect(params).toEqual([{ name: 'product_id', value: '' }]); + }); + + it('should handle OData parameters with mixed quote types', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')/Categories(\":categoryId\")"); + expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]); + }); + + it('should handle OData parameters with nested parentheses', () => { + const params = parsePathParams("https://example.com/odata/Products((':productId'))"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should handle OData parameters with complex nested structures', () => { + const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(':propName')"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]); + }); + + it('should handle OData parameters with query options in path', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')?$expand=Category"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should handle OData parameters with multiple segments and mixed syntax', () => { + const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(:propName)"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]); + }); + + it('should handle OData parameters with empty string values', () => { + const params = parsePathParams("https://example.com/odata/Products('')"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with function calls in parentheses', () => { + const params = parsePathParams("https://example.com/odata/Products(GetId(':productId'))"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should handle OData parameters with escaped quotes', () => { + const params = parsePathParams("https://example.com/odata/Products('ABC''123')"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with spaces in quotes', () => { + const params = parsePathParams("https://example.com/odata/Products('Product Name With Spaces')"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with numeric keys', () => { + const params = parsePathParams("https://example.com/odata/Products(12345)"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with GUID keys', () => { + const params = parsePathParams("https://example.com/odata/Products('123e4567-e89b-12d3-a456-426614174000')"); + expect(params).toEqual([]); + }); }); describe('Url Utils - splitOnFirst', () => { diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 7ec7041b5..83b9e0881 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -115,16 +115,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc throw { message: 'Invalid URL format', originalError: e.message }; } + const paramRegex = /[:](\w+)/g; const interpolatedUrlPath = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { - if (path[0] !== ':') { - return '/' + path; + const matches = path.match(paramRegex); + if (matches) { + const paramName = matches[0].slice(1); // Remove the : prefix + const existingPathParam = request.pathParams.find(param => param.name === paramName); + if (!existingPathParam) { + return '/' + path; + } + return '/' + path.replace(':' + paramName, existingPathParam.value); } else { - const name = path.slice(1); - const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name); - return existingPathParam ? '/' + existingPathParam.value : ''; + return '/' + path; } }) .join(''); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 0aff18f19..7ab2dea61 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -91,15 +91,15 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc if (typeof request.data === 'string') { if (request.data.length) { request.data = _interpolate(request.data, { - escapeJSONStrings: true - }); + escapeJSONStrings: true + }); } } else if (typeof request.data === 'object') { try { const jsonDoc = JSON.stringify(request.data); const parsed = _interpolate(jsonDoc, { - escapeJSONStrings: true - }); + escapeJSONStrings: true + }); request.data = JSON.parse(parsed); } catch (err) {} } @@ -142,16 +142,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc throw { message: 'Invalid URL format', originalError: e.message }; } + const paramRegex = /[:](\w+)/g; const urlPathnameInterpolatedWithPathParams = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { - if (path[0] !== ':') { - return '/' + path; + const matches = path.match(paramRegex); + if (matches) { + const paramName = matches[0].slice(1); // Remove the : prefix + const existingPathParam = request.pathParams.find(param => param.name === paramName); + if (!existingPathParam) { + return '/' + path; + } + return '/' + path.replace(':' + paramName, existingPathParam.value); } else { - const name = path.slice(1); - const existingPathParam = request.pathParams.find((param) => param.type === 'path' && param.name === name); - return existingPathParam ? '/' + existingPathParam.value : ''; + return '/' + path; } }) .join(''); From 6f8c543ee3662c7599f6798311c24d4d40cfc6ab Mon Sep 17 00:00:00 2001 From: sid-bruno Date: Tue, 23 Sep 2025 15:55:04 +0530 Subject: [PATCH 2/4] tests: additional tests for path params and odata (#5610) * Support for Odata style path params (#5048) * Support for Odata style path params * Remove test statement * Update packages/bruno-app/src/utils/url/index.spec.js Add more testcases Co-authored-by: sid-bruno * Performance improvements * Add testcases for odata style url params --------- Co-authored-by: sid-bruno * tests: additional tests for odata and path params tests(electron): add in odata smoke for interpolation chore: code format chore: ESLint atomic diff based formatting (#5592) * chore: atomic diff based formatting chore(format): fix formatting tests(playwright): interpolation tests Support for Odata style path params (#5048) * Support for Odata style path params * Remove test statement * Update packages/bruno-app/src/utils/url/index.spec.js Add more testcases Co-authored-by: sid-bruno * Performance improvements * Add testcases for odata style url params --------- Co-authored-by: sid-bruno --------- Co-authored-by: Anton Co-authored-by: Siddharth Gelera --- eslint.config.js | 1 + .../bruno-app/src/utils/url/index.spec.js | 42 +++++++++---------- .../tests/network/interpolate-vars.spec.js | 28 +++++++++++++ packages/bruno-tests/src/echo/index.js | 4 ++ tests/interpolation/collection/bruno.json | 9 ++++ .../collection/echo-request-odata.bru | 21 ++++++++++ .../collection/echo-request-url.bru | 18 ++++++++ .../init-user-data/collection-security.json | 10 +++++ .../init-user-data/preferences.json | 6 +++ .../interpolate-request-url.spec.ts | 26 ++++++++++++ 10 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 tests/interpolation/collection/bruno.json create mode 100644 tests/interpolation/collection/echo-request-odata.bru create mode 100644 tests/interpolation/collection/echo-request-url.bru create mode 100644 tests/interpolation/init-user-data/collection-security.json create mode 100644 tests/interpolation/init-user-data/preferences.json create mode 100644 tests/interpolation/interpolate-request-url.spec.ts diff --git a/eslint.config.js b/eslint.config.js index 861eeda1c..4eaaf583b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,6 +18,7 @@ module.exports = runESMImports().then(() => defineConfig([ }, files: [ './eslint.config.js', + 'tests/**/*.spec.{ts,js}', 'packages/bruno-app/**/*.{js,jsx,ts}', 'packages/bruno-app/src/test-utils/mocks/codemirror.js', 'packages/bruno-cli/**/*.js', diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index c75ce63c8..8a3e021b5 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -48,7 +48,7 @@ describe('Url Utils - parsePathParams', () => { const params = parsePathParams("https://example.com/ExchangeRates(':ExchangeRateOID')"); expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); }); - + it('should parse path param inside parentheses and no quotes', () => { const params = parsePathParams("https://example.com/ExchangeRates(:ExchangeRateOID)"); expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); @@ -56,12 +56,12 @@ describe('Url Utils - parsePathParams', () => { it('should parse multiple path params inside parentheses', () => { const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)"); - expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'ExchangeRateOID', value: '' }]); + expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'ExchangeRateOID', value: '' }]); }); it('should parse mix and match of normal and param inside parentheses', () => { const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/:key"); - expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'key', value: '' }]); + expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'key', value: '' }]); }); // OData-specific test cases for enhanced path parameter parsing @@ -81,92 +81,92 @@ describe('Url Utils - parsePathParams', () => { }); it('should parse OData entity key with parentheses only', () => { - const params = parsePathParams("https://example.com/odata/Products(:productId)"); + const params = parsePathParams('https://example.com/odata/Products(:productId)'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); it('should parse OData composite key with multiple parameters', () => { - const params = parsePathParams("https://example.com/odata/Orders(:orderId,ProductId=':productId')"); + const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=\':productId\')'); expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); }); it('should parse OData navigation property with key', () => { - const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')"); + const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')'); expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]); }); it('should parse OData function with parameters', () => { - const params = parsePathParams("https://example.com/odata/GetProductsByCategory(categoryId=':categoryId')"); + const params = parsePathParams('https://example.com/odata/GetProductsByCategory(categoryId=\':categoryId\')'); expect(params).toEqual([{ name: 'categoryId', value: '' }]); }); it('should parse OData action with complex parameters', () => { - const params = parsePathParams("https://example.com/odata/Products(':productId')/Rate(rating=:rating,comment=':comment')"); + const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Rate(rating=:rating,comment=\':comment\')'); expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]); }); it('should handle OData parameters with special characters in names', () => { - const params = parsePathParams("https://example.com/odata/Products(':product-id')"); + const params = parsePathParams('https://example.com/odata/Products(\':product-id\')'); expect(params).toEqual([{ name: 'product', value: '' }]); }); it('should handle OData parameters with underscores in names', () => { - const params = parsePathParams("https://example.com/odata/Products(':product_id')"); + const params = parsePathParams('https://example.com/odata/Products(\':product_id\')'); expect(params).toEqual([{ name: 'product_id', value: '' }]); }); it('should handle OData parameters with mixed quote types', () => { - const params = parsePathParams("https://example.com/odata/Products(':productId')/Categories(\":categoryId\")"); + const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")'); expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]); }); it('should handle OData parameters with nested parentheses', () => { - const params = parsePathParams("https://example.com/odata/Products((':productId'))"); + const params = parsePathParams('https://example.com/odata/Products((\':productId\'))'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); it('should handle OData parameters with complex nested structures', () => { - const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(':propName')"); + const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(\':propName\')'); expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]); }); it('should handle OData parameters with query options in path', () => { - const params = parsePathParams("https://example.com/odata/Products(':productId')?$expand=Category"); + const params = parsePathParams('https://example.com/odata/Products(\':productId\')?$expand=Category'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); it('should handle OData parameters with multiple segments and mixed syntax', () => { - const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(:propName)"); + const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(:propName)'); expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]); }); it('should handle OData parameters with empty string values', () => { - const params = parsePathParams("https://example.com/odata/Products('')"); + const params = parsePathParams('https://example.com/odata/Products(\'\')'); expect(params).toEqual([]); }); it('should handle OData parameters with function calls in parentheses', () => { - const params = parsePathParams("https://example.com/odata/Products(GetId(':productId'))"); + const params = parsePathParams('https://example.com/odata/Products(GetId(\':productId\'))'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); it('should handle OData parameters with escaped quotes', () => { - const params = parsePathParams("https://example.com/odata/Products('ABC''123')"); + const params = parsePathParams('https://example.com/odata/Products(\'ABC\'\'123\')'); expect(params).toEqual([]); }); it('should handle OData parameters with spaces in quotes', () => { - const params = parsePathParams("https://example.com/odata/Products('Product Name With Spaces')"); + const params = parsePathParams('https://example.com/odata/Products(\'Product Name With Spaces\')'); expect(params).toEqual([]); }); it('should handle OData parameters with numeric keys', () => { - const params = parsePathParams("https://example.com/odata/Products(12345)"); + const params = parsePathParams('https://example.com/odata/Products(12345)'); expect(params).toEqual([]); }); it('should handle OData parameters with GUID keys', () => { - const params = parsePathParams("https://example.com/odata/Products('123e4567-e89b-12d3-a456-426614174000')"); + const params = parsePathParams('https://example.com/odata/Products(\'123e4567-e89b-12d3-a456-426614174000\')'); expect(params).toEqual([]); }); }); diff --git a/packages/bruno-electron/tests/network/interpolate-vars.spec.js b/packages/bruno-electron/tests/network/interpolate-vars.spec.js index 88abdd811..f0b800806 100644 --- a/packages/bruno-electron/tests/network/interpolate-vars.spec.js +++ b/packages/bruno-electron/tests/network/interpolate-vars.spec.js @@ -154,6 +154,34 @@ describe('interpolate-vars: interpolateVars', () => { const result = interpolateVars(request, null, null, null); expect(result.url).toBe('http://example.com/foobar'); }); + + it('updates the path with odata style params | smoke', async () => { + const request = { + method: 'GET', + url: 'http://example.com/Category(\':CategoryID\')/Item(:ItemId)/:xpath/Tags("tag test")', + pathParams: [ + { + type: 'path', + name: 'CategoryID', + value: 'foobar' + }, + { + type: 'path', + name: 'ItemId', + value: 1 + }, + { + type: 'path', + name: 'xpath', + value: 'foobar' + } + ] + }; + + const result = interpolateVars(request, null, null, null); + expect(result.url).toBe('http://example.com/Category(\'foobar\')/Item(1)/foobar/Tags(%22tag%20test%22)'); + }); + }); describe('With process environment variables', () => { diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index 00b50bd36..a9425b305 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -1,6 +1,10 @@ const express = require('express'); const router = express.Router(); +router.get('/path/*', (req, res) => { + return res.json({ url: req.url }); +}); + router.post('/json', (req, res) => { return res.json(req.body); }); diff --git a/tests/interpolation/collection/bruno.json b/tests/interpolation/collection/bruno.json new file mode 100644 index 000000000..f29e04549 --- /dev/null +++ b/tests/interpolation/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "interpolation", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/tests/interpolation/collection/echo-request-odata.bru b/tests/interpolation/collection/echo-request-odata.bru new file mode 100644 index 000000000..f3cb79320 --- /dev/null +++ b/tests/interpolation/collection/echo-request-odata.bru @@ -0,0 +1,21 @@ +meta { + name: echo-request-odata + type: http + seq: 2 +} + +get { + url: http://localhost:8081/api/echo/path/Category(':CategoryID')/Item(:ItemId)/:xpath/Tags("tag test") + body: none + auth: inherit +} + +params:path { + CategoryID: category123 + ItemId: item456 + xpath: foobar +} + +settings { + encodeUrl: true +} diff --git a/tests/interpolation/collection/echo-request-url.bru b/tests/interpolation/collection/echo-request-url.bru new file mode 100644 index 000000000..3b3b00754 --- /dev/null +++ b/tests/interpolation/collection/echo-request-url.bru @@ -0,0 +1,18 @@ +meta { + name: echo-request-url + type: http + seq: 1 +} + +get { + url: http://localhost:8081/api/echo/path/:path + auth: inherit +} + +params:path { + path: some-data +} + +settings { + encodeUrl: true +} diff --git a/tests/interpolation/init-user-data/collection-security.json b/tests/interpolation/init-user-data/collection-security.json new file mode 100644 index 000000000..9369c1b91 --- /dev/null +++ b/tests/interpolation/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{projectRoot}}/tests/interpolation/collection", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} \ No newline at end of file diff --git a/tests/interpolation/init-user-data/preferences.json b/tests/interpolation/init-user-data/preferences.json new file mode 100644 index 000000000..38f64eadb --- /dev/null +++ b/tests/interpolation/init-user-data/preferences.json @@ -0,0 +1,6 @@ +{ + "maximized": true, + "lastOpenedCollections": [ + "{{projectRoot}}/tests/interpolation/collection" + ] +} \ No newline at end of file diff --git a/tests/interpolation/interpolate-request-url.spec.ts b/tests/interpolation/interpolate-request-url.spec.ts new file mode 100644 index 000000000..6ac0b6f41 --- /dev/null +++ b/tests/interpolation/interpolate-request-url.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '../../playwright'; + +test.describe.serial('URL Interpolation', () => { + test.setTimeout(2 * 10 * 1000); + + test('Interpolate basic path params', async ({ pageWithUserData: page }) => { + await page.locator('#sidebar-collection-name').click(); + await page.getByRole('complementary').getByText('echo-request-url').click(); + await page.getByTestId('send-arrow-icon').click(); + + expect(page.getByTestId('response-status-code')).toHaveText(/200/); + + const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts(); + expect(texts.some(d => d.includes(`"url": "/path/some-data"`))).toBe(true); + }); + + test('Interpolate oData path params', async ({ pageWithUserData: page }) => { + await page.getByRole('complementary').getByText('echo-request-odata').click(); + await page.getByTestId('send-arrow-icon').click(); + + expect(page.getByTestId('response-status-code')).toHaveText(/200/); + + const texts = await page.locator('div:nth-child(2) > .CodeMirror-scroll').allInnerTexts(); + expect(texts.some(d => d.includes(`"url": "/path/Category('category123')/Item(item456)/foobar/Tags(%22tag%20test%22)"`))).toBe(true); + }); +}); From c15d47c0dc1a06a78347900bc9176a0edb9b1bc1 Mon Sep 17 00:00:00 2001 From: "Siddharth Gelera (reaper)" Date: Wed, 24 Sep 2025 13:00:54 +0530 Subject: [PATCH 3/4] chore: base format (#5624) --- packages/bruno-app/src/utils/url/index.js | 8 ++++---- packages/bruno-app/src/utils/url/index.spec.js | 10 +++++----- .../src/ipc/network/interpolate-vars.js | 4 ++-- .../tests/network/interpolate-vars.spec.js | 11 +++++------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index ae8d10c11..bff1ac895 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -35,7 +35,7 @@ export const parsePathParams = (url) => { // Enhanced: also match :param inside parentheses and/or quotes const paramRegex = /[:](\w+)/g; const foundParams = new Set(); - paths.forEach((segment) => { + paths.forEach(segment => { let match; while ((match = paramRegex.exec(segment))) { if (match[1]) { @@ -49,7 +49,7 @@ export const parsePathParams = (url) => { } } }); - return Array.from(foundParams).map((name) => ({ name, value: '' })); + return Array.from(foundParams).map(name => ({ name, value: '' })); }; export const splitOnFirst = (str, char) => { @@ -89,7 +89,7 @@ export const interpolateUrlPathParams = (url, params) => { .split('/') .map((segment) => { - if(!segment.startsWith(":")) return segment + if (!segment.startsWith(':')) return segment; let match; while ((match = regex.exec(segment))) { @@ -99,7 +99,7 @@ export const interpolateUrlPathParams = (url, params) => { // Remove leading quotes/parentheses if present name = name.replace(/^[('"`]+/, ''); if (name) { - const pathParam = params.find((p) => p?.name === name && p?.type === 'path'); + const pathParam = params.find(p => p?.name === name && p?.type === 'path'); return pathParam ? pathParam.value : segment; } } diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index 8a3e021b5..ddab88a99 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -45,28 +45,28 @@ describe('Url Utils - parsePathParams', () => { }); it('should parse path param inside parentheses and quotes', () => { - const params = parsePathParams("https://example.com/ExchangeRates(':ExchangeRateOID')"); + const params = parsePathParams('https://example.com/ExchangeRates(\':ExchangeRateOID\')'); expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); }); it('should parse path param inside parentheses and no quotes', () => { - const params = parsePathParams("https://example.com/ExchangeRates(:ExchangeRateOID)"); + const params = parsePathParams('https://example.com/ExchangeRates(:ExchangeRateOID)'); expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); }); it('should parse multiple path params inside parentheses', () => { - const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)"); + const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)'); expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'ExchangeRateOID', value: '' }]); }); it('should parse mix and match of normal and param inside parentheses', () => { - const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/:key"); + const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/:key'); expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'key', value: '' }]); }); // OData-specific test cases for enhanced path parameter parsing it('should parse OData entity key with single quotes', () => { - const params = parsePathParams("https://example.com/odata/Products(':productId')"); + const params = parsePathParams('https://example.com/odata/Products(\':productId\')'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 7ab2dea61..a3b869156 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -91,14 +91,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc if (typeof request.data === 'string') { if (request.data.length) { request.data = _interpolate(request.data, { - escapeJSONStrings: true + escapeJSONStrings: true, }); } } else if (typeof request.data === 'object') { try { const jsonDoc = JSON.stringify(request.data); const parsed = _interpolate(jsonDoc, { - escapeJSONStrings: true + escapeJSONStrings: true, }); request.data = JSON.parse(parsed); } catch (err) {} diff --git a/packages/bruno-electron/tests/network/interpolate-vars.spec.js b/packages/bruno-electron/tests/network/interpolate-vars.spec.js index f0b800806..0c1e4f178 100644 --- a/packages/bruno-electron/tests/network/interpolate-vars.spec.js +++ b/packages/bruno-electron/tests/network/interpolate-vars.spec.js @@ -163,25 +163,24 @@ describe('interpolate-vars: interpolateVars', () => { { type: 'path', name: 'CategoryID', - value: 'foobar' + value: 'foobar', }, { type: 'path', name: 'ItemId', - value: 1 + value: 1, }, { type: 'path', name: 'xpath', - value: 'foobar' - } - ] + value: 'foobar', + }, + ], }; const result = interpolateVars(request, null, null, null); expect(result.url).toBe('http://example.com/Category(\'foobar\')/Item(1)/foobar/Tags(%22tag%20test%22)'); }); - }); describe('With process environment variables', () => { From a1a7c9a136c3cc3250e3ec0d5a05d8baa1984cb9 Mon Sep 17 00:00:00 2001 From: Bijin A B Date: Wed, 24 Sep 2025 13:56:41 +0530 Subject: [PATCH 4/4] remove the custom test timeout as default would be enough --- tests/interpolation/interpolate-request-url.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/interpolation/interpolate-request-url.spec.ts b/tests/interpolation/interpolate-request-url.spec.ts index 6ac0b6f41..061558a5c 100644 --- a/tests/interpolation/interpolate-request-url.spec.ts +++ b/tests/interpolation/interpolate-request-url.spec.ts @@ -1,8 +1,6 @@ import { test, expect } from '../../playwright'; test.describe.serial('URL Interpolation', () => { - test.setTimeout(2 * 10 * 1000); - test('Interpolate basic path params', async ({ pageWithUserData: page }) => { await page.locator('#sidebar-collection-name').click(); await page.getByRole('complementary').getByText('echo-request-url').click();