From dce14811854b40201b35eeb51b8cd17f54cf739d Mon Sep 17 00:00:00 2001 From: Nelu Platonov Date: Thu, 23 Nov 2023 23:38:43 +0100 Subject: [PATCH 001/904] fix(#521): Allow "context" as the name of a key/var in a JS expression --- packages/bruno-js/src/utils.js | 10 +++++---- packages/bruno-js/tests/utils.spec.js | 30 ++++++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 7d6b2db30..92f3a2c08 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -18,8 +18,8 @@ const JS_KEYWORDS = ` * ```js * res.data.pets.map(pet => pet.name.toUpperCase()) * - * function(context) { - * const { res, pet } = context; + * function(_BrunoNewFunctionInnerContext) { + * const { res, pet } = _BrunoNewFunctionInnerContext; * return res.data.pets.map(pet => pet.name.toUpperCase()) * } * ``` @@ -45,9 +45,11 @@ const compileJsExpression = (expr) => { globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('') }; - const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`; + // param name that is unlikely to show up as a var in an expression + const param = `_BrunoNewFunctionInnerContext`; + const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`; - return new Function('context', body); + return new Function(param, body); }; const internalExpressionCache = new Map(); diff --git a/packages/bruno-js/tests/utils.spec.js b/packages/bruno-js/tests/utils.spec.js index 6ac687f07..6a454fde7 100644 --- a/packages/bruno-js/tests/utils.spec.js +++ b/packages/bruno-js/tests/utils.spec.js @@ -5,7 +5,9 @@ describe('utils', () => { describe('expression evaluation', () => { const context = { res: { - data: { pets: ['bruno', 'max'] } + data: { pets: ['bruno', 'max'] }, + context: 'testContext', + _BrunoNewFunctionInnerContext: 0 } }; @@ -45,32 +47,32 @@ describe('utils', () => { it('should identify top level variables', () => { const expr = 'res.data.pets[0].toUpperCase()'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should not duplicate variables', () => { const expr = 'res.data.pets[0] + res.data.pets[1]'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should exclude js keywords like true false from vars', () => { const expr = 'res.data.pets.length > 0 ? true : false'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should exclude numbers from vars', () => { const expr = 'res.data.pets.length + 10'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = _BrunoNewFunctionInnerContext;'); }); it('should pick variables from complex expressions', () => { const expr = 'res.data.pets.map(pet => pet.length)'; const result = evaluateJsExpression(expr, context); expect(result).toEqual([5, 3]); - expect(cache.get(expr).toString()).toContain('let { res, pet } = context;'); + expect(cache.get(expr).toString()).toContain('let { res, pet } = _BrunoNewFunctionInnerContext;'); }); it('should be ok picking extra vars from strings', () => { @@ -78,7 +80,7 @@ describe('utils', () => { const result = evaluateJsExpression(expr, context); expect(result).toBe('hello bruno'); // extra var hello is harmless - expect(cache.get(expr).toString()).toContain('let { hello, res } = context;'); + expect(cache.get(expr).toString()).toContain('let { hello, res } = _BrunoNewFunctionInnerContext;'); }); it('should evaluate expressions referencing globals', () => { @@ -112,6 +114,20 @@ describe('utils', () => { expect(result).toBe(startTime); }); + + it('should allow "context" as a var name', () => { + const expr = 'res["context"].toUpperCase()'; + evaluateJsExpression(expr, context); + expect(cache.get(expr).toString()).toContain('let { res, context } = _BrunoNewFunctionInnerContext;'); + }); + + it('should throw an error when we use "_BrunoNewFunctionInnerContext" as a var name', () => { + const expr = 'res["_BrunoNewFunctionInnerContext"].toUpperCase()'; + expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError); + expect(() => evaluateJsExpression(expr, context)).toThrow( + "Identifier '_BrunoNewFunctionInnerContext' has already been declared" + ); + }); }); describe('response parser', () => { From 39f60daca70708127e1019e87fd85f54ac62f625 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Thu, 18 Apr 2024 15:43:09 +0200 Subject: [PATCH 002/904] feature: Multi-part requests: user should be able to set content-type for each part in a multi-part request. #1602 --- .../MultipartFormParams/StyledWrapper.js | 2 +- .../RequestPane/MultipartFormParams/index.js | 25 ++++++++ .../ReduxStore/slices/collections/index.js | 2 + .../bruno-cli/src/runner/prepare-request.js | 51 +++++++++++---- .../src/ipc/network/prepare-request.js | 10 ++- packages/bruno-lang/v2/src/bruToJson.js | 17 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 6 +- .../bruno-lang/v2/tests/fixtures/request.json | 3 + .../bruno-schema/src/collections/index.js | 1 + .../multipart/mixed-content-types.bru | 39 ++++++++++++ .../collection/multipart/small.png | Bin 0 -> 132 bytes packages/bruno-tests/src/index.js | 10 ++- .../src/multipart/form-data-parser.js | 58 ++++++++++++++++++ packages/bruno-tests/src/multipart/index.js | 10 +++ 14 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 packages/bruno-tests/collection/multipart/mixed-content-types.bru create mode 100644 packages/bruno-tests/collection/multipart/small.png create mode 100644 packages/bruno-tests/src/multipart/form-data-parser.js create mode 100644 packages/bruno-tests/src/multipart/index.js diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index f04a30be0..80a146a5c 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -24,7 +24,7 @@ const Wrapper = styled.div` width: 30%; } - &:nth-child(3) { + &:nth-child(4) { width: 70px; } } diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 1f1c9977e..794e35add 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -52,6 +52,10 @@ const MultipartFormParams = ({ item, collection }) => { param.value = e.target.value; break; } + case 'contentType': { + param.contentType = e.target.value; + break; + } case 'enabled': { param.enabled = e.target.checked; break; @@ -83,6 +87,7 @@ const MultipartFormParams = ({ item, collection }) => { Key Value + Content-Type @@ -142,6 +147,26 @@ const MultipartFormParams = ({ item, collection }) => { /> )} + + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> +
{ + // make axios work in node using form data + // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 + const form = new FormData(); + datas.forEach((item) => { + const value = item.value; + const name = item.name; + let options = {}; + if (item.contentType) { + options.contentType = item.contentType; + } + if (item.type === 'file') { + const filePaths = value || []; + filePaths.forEach((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); + }); + } else { + form.append(name, value, options); + } + }); + return form; +}; const prepareRequest = (request, collectionRoot) => { const headers = {}; @@ -124,17 +155,11 @@ const prepareRequest = (request, collectionRoot) => { } if (request.body.mode === 'multipartForm') { - const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - each(enabledParams, (p) => { - if (p.type === 'file') { - params[p.name] = p.value.map((path) => fs.createReadStream(path)); - } else { - params[p.name] = p.value; - } - }); - axiosRequest.headers['content-type'] = 'multipart/form-data'; - axiosRequest.data = params; + const collectionPath = process.cwd(); + const form = parseFormData(enabledParams, collectionPath); + extend(axiosRequest.headers, form.getHeaders()); + axiosRequest.data = form; } if (request.body.mode === 'graphql') { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 37196589a..249b1754f 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -12,6 +12,10 @@ const parseFormData = (datas, collectionPath) => { datas.forEach((item) => { const value = item.value; const name = item.name; + let options = {}; + if (item.contentType) { + options.contentType = item.contentType; + } if (item.type === 'file') { const filePaths = value || []; filePaths.forEach((filePath) => { @@ -20,11 +24,11 @@ const parseFormData = (datas, collectionPath) => { if (!path.isAbsolute(trimmedFilePath)) { trimmedFilePath = path.join(collectionPath, trimmedFilePath); } - - form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); }); } else { - form.append(name, value); + form.append(name, value, options); } }); return form; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 6f12a6ce5..32ac3a467 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -45,7 +45,7 @@ const grammar = ohm.grammar(`Bru { pair = st* key st* ":" st* value st* key = keychar* value = multilinetextblock | valuechar* - + // Dictionary for Assert Block assertdictionary = st* "{" assertpairlist? tagend assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)* @@ -133,16 +133,31 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { }); }; +const multipartExtractContentType = (pair) => { + if (_.isString(pair.value)) { + const match = pair.value.match(/^(.*?)\s*\(Content-Type=(.*?)\)\s*$/); + if (match != null && match.length > 2) { + pair.value = match[1]; + pair.contentType = match[2]; + } else { + pair.contentType = ''; + } + } +}; + const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); return pairs.map((pair) => { pair.type = 'text'; + multipartExtractContentType(pair); + if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); pair.type = 'file'; pair.value = filestr.split('|'); } + return pair; }); }; diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 3357e5d09..658d7cbd2 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -247,16 +247,18 @@ ${indentString(body.sparql)} multipartForms .map((item) => { const enabled = item.enabled ? '' : '~'; + const contentType = + item.contentType && item.contentType !== '' ? ' (Content-Type=' + item.contentType + ')' : ''; if (item.type === 'text') { - return `${enabled}${item.name}: ${item.value}`; + return `${enabled}${item.name}: ${item.value}${contentType}`; } if (item.type === 'file') { let filepaths = item.value || []; let filestr = filepaths.join('|'); const value = `@file(${filestr})`; - return `${enabled}${item.name}: ${value}`; + return `${enabled}${item.name}: ${value}${contentType}`; } }) .join('\n') diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index afb7ca3f9..594d02e97 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -103,18 +103,21 @@ ], "multipartForm": [ { + "contentType": "", "name": "apikey", "value": "secret", "enabled": true, "type": "text" }, { + "contentType": "", "name": "numbers", "value": "+91998877665", "enabled": true, "type": "text" }, { + "contentType": "", "name": "message", "value": "hello", "enabled": false, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 033e68277..c0cacc1c4 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -68,6 +68,7 @@ const multipartFormSchema = Yup.object({ otherwise: Yup.string().nullable() }), description: Yup.string().nullable(), + contentType: Yup.string().nullable(), enabled: Yup.boolean() }) .noUnknown(true) diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru new file mode 100644 index 000000000..7fc421100 --- /dev/null +++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru @@ -0,0 +1,39 @@ +meta { + name: mixed-content-types + type: http + seq: 1 +} + +post { + url: {{host}}/api/multipart/mixed-content-types + body: multipartForm + auth: none +} + +body:multipart-form { + param1: test + param2: {"test":"i am json"} (Content-Type=application/json) + param3: @file(multipart/small.png) +} + +tests { + test("Status code is 200", function () { + expect(res.getStatus()).to.equal(200); + }); + test("param1 has no content-type", function () { + var param1 = res.body.find(p=>p.name === 'param1') + expect(param1).to.be.an('object'); + expect(param1.contentType).to.be.undefined; + }); + test("param2 has content-type application/json", function () { + var param2 = res.body.find(p=>p.name === 'param2') + expect(param2).to.be.an('object'); + expect(param2.contentType).to.equals('application/json'); + }); + test("param3 has content-type image/png", function () { + var param3 = res.body.find(p=>p.name === 'param3') + expect(param3).to.be.an('object'); + expect(param3.contentType).to.equals('image/png'); + }); + +} diff --git a/packages/bruno-tests/collection/multipart/small.png b/packages/bruno-tests/collection/multipart/small.png new file mode 100644 index 0000000000000000000000000000000000000000..2b584adf0512bb703f04eab2f012893496a03684 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp!2~4lMxL|Nn{1`ISV`@iy0Uc%|V#aIAVSkP*Bp-#WBRfKlz8hlEhU99x!f{`uCrm;pK~% Vb1~=hq=1STJYD@<);T3K0RTE@AvOR2 literal 0 HcmV?d00001 diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index 9ba6e3170..d9b921951 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -2,23 +2,25 @@ const express = require('express'); const bodyParser = require('body-parser'); const xmlparser = require('express-xml-bodyparser'); const cors = require('cors'); -const multer = require('multer'); +const formDataParser = require('./multipart/form-data-parser'); const app = new express(); const port = process.env.PORT || 8080; -const upload = multer(); app.use(cors()); app.use(xmlparser()); app.use(bodyParser.text()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); +formDataParser.init(app, express); const authRouter = require('./auth'); const echoRouter = require('./echo'); +const multipartRouter = require('./multipart'); app.use('/api/auth', authRouter); app.use('/api/echo', echoRouter); +app.use('/api/multipart', multipartRouter); app.get('/ping', function (req, res) { return res.send('pong'); @@ -32,10 +34,6 @@ app.get('/query', function (req, res) { return res.json(req.query); }); -app.post('/echo/multipartForm', upload.none(), function (req, res) { - return res.json(req.body); -}); - app.get('/redirect-to-ping', function (req, res) { return res.redirect('/ping'); }); diff --git a/packages/bruno-tests/src/multipart/form-data-parser.js b/packages/bruno-tests/src/multipart/form-data-parser.js new file mode 100644 index 000000000..8b4aa500a --- /dev/null +++ b/packages/bruno-tests/src/multipart/form-data-parser.js @@ -0,0 +1,58 @@ +/** + * Instead of using multer for example to parse the multipart form data, we build our own parser + * so that we can verify the content type are set correctly by bruno (for example application/json for json content) + */ + +const extractParam = function (param, str, delimiter, quote, endDelimiter) { + let regex = new RegExp(`${param}${delimiter}\\s*${quote}(.*?)${quote}${endDelimiter}`); + const found = str.match(regex); + if (found != null && found.length > 1) { + return found[1]; + } else { + return null; + } +}; + +const init = function (app, express) { + app.use(express.raw({ type: 'multipart/form-data' })); +}; + +const parsePart = function (part) { + let result = {}; + const name = extractParam('name', part, '=', '"', ''); + if (name) { + result.name = name; + } + const filename = extractParam('filename', part, '=', '"', ''); + if (filename) { + result.filename = filename; + } + const contentType = extractParam('Content-Type', part, ':', '', ';'); + if (contentType) { + result.contentType = contentType; + } + if (!filename) { + result.value = part.substring(part.indexOf('value=') + 'value='.length); + } + if (contentType === 'application/json') { + result.value = JSON.parse(result.value); + } + return result; +}; + +const parse = function (req) { + const BOUNDARY = 'boundary='; + const contentType = req.headers['content-type']; + const boundary = '--' + contentType.substring(contentType.indexOf(BOUNDARY) + BOUNDARY.length); + const rawBody = req.body.toString(); + let parts = rawBody.split(boundary).filter((part) => part.length > 0); + parts = parts.map((part) => part.trim('\r\n')); + parts = parts.filter((part) => part != '--'); + parts = parts.map((part) => part.replace('\r\n\r\n', ';value=')); + parts = parts.map((part) => part.replace('\r\n', ';')); + parts = parts.map((part) => parsePart(part)); + return parts; +}; + +module.exports.parse = parse; +module.exports.init = init; diff --git a/packages/bruno-tests/src/multipart/index.js b/packages/bruno-tests/src/multipart/index.js new file mode 100644 index 000000000..a98837c54 --- /dev/null +++ b/packages/bruno-tests/src/multipart/index.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const formDataParser = require('./form-data-parser'); + +router.post('/mixed-content-types', (req, res) => { + const parts = formDataParser.parse(req); + return res.json(parts); +}); + +module.exports = router; From c3c91d61c818074c351397c41ce088bfb8073bf6 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 15:01:41 +0200 Subject: [PATCH 003/904] added placeholder support to MultiLineEditor component --- .../src/components/MultiLineEditor/StyledWrapper.js | 6 ++++++ .../bruno-app/src/components/MultiLineEditor/index.js | 1 + .../RequestPane/MultipartFormParams/StyledWrapper.js | 8 ++++++++ .../components/RequestPane/MultipartFormParams/index.js | 1 + packages/bruno-app/src/pages/Bruno/index.js | 1 + 5 files changed, 17 insertions(+) diff --git a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js index 6b3a8d568..e8ef24de7 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js @@ -13,6 +13,12 @@ const StyledWrapper = styled.div` line-height: 30px; overflow: hidden; + pre.CodeMirror-placeholder { + color: ${(props) => props.theme.text}; + padding-left: 0; + opacity: 0.5; + } + .CodeMirror-scroll { overflow: hidden !important; ${'' /* padding-bottom: 50px !important; */} diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index efcd89f45..359dbc2ce 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -28,6 +28,7 @@ class MultiLineEditor extends Component { lineWrapping: false, lineNumbers: false, theme: this.props.theme === 'dark' ? 'monokai' : 'default', + placeholder: this.props.placeholder, mode: 'brunovariables', brunoVarInfo: { variables: getAllVariables(this.props.collection) diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index 80a146a5c..49c45f21d 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -24,6 +24,14 @@ const Wrapper = styled.div` width: 30%; } + &:nth-child(2) { + width: 45%; + } + + &:nth-child(3) { + width: 25%; + } + &:nth-child(4) { width: 70px; } diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 794e35add..4ddd64218 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -151,6 +151,7 @@ const MultipartFormParams = ({ item, collection }) => { handleParamChange( diff --git a/packages/bruno-app/src/pages/Bruno/index.js b/packages/bruno-app/src/pages/Bruno/index.js index 71e24dcfa..d0b9c3485 100644 --- a/packages/bruno-app/src/pages/Bruno/index.js +++ b/packages/bruno-app/src/pages/Bruno/index.js @@ -31,6 +31,7 @@ if (!SERVER_RENDERED) { require('codemirror/addon/search/jump-to-line'); require('codemirror/addon/search/search'); require('codemirror/addon/search/searchcursor'); + require('codemirror/addon/display/placeholder'); require('codemirror/keymap/sublime'); require('codemirror-graphql/hint'); From 8e99ed32582ae423bbcbf0b692c375f927c17826 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:06:49 +0200 Subject: [PATCH 004/904] moved assertions from Tests panel to Assert panel and --- .../multipart/mixed-content-types.bru | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru index 7fc421100..45a1cdd18 100644 --- a/packages/bruno-tests/collection/multipart/mixed-content-types.bru +++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru @@ -16,24 +16,9 @@ body:multipart-form { param3: @file(multipart/small.png) } -tests { - test("Status code is 200", function () { - expect(res.getStatus()).to.equal(200); - }); - test("param1 has no content-type", function () { - var param1 = res.body.find(p=>p.name === 'param1') - expect(param1).to.be.an('object'); - expect(param1.contentType).to.be.undefined; - }); - test("param2 has content-type application/json", function () { - var param2 = res.body.find(p=>p.name === 'param2') - expect(param2).to.be.an('object'); - expect(param2.contentType).to.equals('application/json'); - }); - test("param3 has content-type image/png", function () { - var param3 = res.body.find(p=>p.name === 'param3') - expect(param3).to.be.an('object'); - expect(param3.contentType).to.equals('image/png'); - }); - +assert { + res.status: eq 200 + res.body.find(p=>p.name === 'param1').contentType: isUndefined + res.body.find(p=>p.name === 'param2').contentType: eq application/json + res.body.find(p=>p.name === 'param3').contentType: eq image/png } From b7f4edac249e0551cca1fdad15ebbd0adb11b1a1 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:08:38 +0200 Subject: [PATCH 005/904] Reduced the width of the Operator column in tab Assert --- .../components/RequestPane/Assertions/StyledWrapper.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js index db0a36e58..eef02664e 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js @@ -21,7 +21,15 @@ const Wrapper = styled.div` padding: 6px 10px; &:nth-child(1) { - width: 30%; + width: 60%; + } + + &:nth-child(2) { + width: 130px; + } + + &:nth-child(3) { + width: 40%; } &:nth-child(4) { From 40872f6e9e2ecbd3e4851a349a659e933806ec53 Mon Sep 17 00:00:00 2001 From: busy-panda Date: Fri, 19 Apr 2024 17:14:44 +0200 Subject: [PATCH 006/904] Reduced the width of the Operator column in tab Assert --- .../components/RequestPane/Assertions/StyledWrapper.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js index eef02664e..8e658c263 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js @@ -20,18 +20,10 @@ const Wrapper = styled.div` td { padding: 6px 10px; - &:nth-child(1) { - width: 60%; - } - &:nth-child(2) { width: 130px; } - &:nth-child(3) { - width: 40%; - } - &:nth-child(4) { width: 70px; } From 77b1e6d738187f6b5bec6e00d66a5f4728152832 Mon Sep 17 00:00:00 2001 From: Jaideep Ghosh <3909648+jaideepghosh@users.noreply.github.com> Date: Thu, 23 May 2024 01:08:50 +0530 Subject: [PATCH 007/904] docs:Fix markdown syntax in the contributing_pl (#2354) * docs:Added hindi translation for contribution guides. * feat:Included the hindi translation link to other language files. * docs:Resolved the markdown formatting in contributing_pl. --------- Co-authored-by: Anoop M D --- docs/contributing/contributing_pl.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/contributing_pl.md b/docs/contributing/contributing_pl.md index 0d67045c4..3c10af8de 100644 --- a/docs/contributing/contributing_pl.md +++ b/docs/contributing/contributing_pl.md @@ -33,7 +33,7 @@ Bruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, u ### Lokalny Rozwój -````bash +```bash # użyj wersji nodejs 18 nvm use @@ -66,7 +66,7 @@ done # Usuń package-lock w podkatalogach find . -type f -name "package-lock.json" -delete -```` +``` ### Testowanie From abfd14a3068331651ebe29a09746b2599b00eca0 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Thu, 30 May 2024 15:49:14 +0530 Subject: [PATCH 008/904] Feat/improved path params (#2357) * feat: path parameters (#484) * add path parameters on bruno-app * add path parameters on bruno-cli * fix bruno-schema testing * fix generate request code not replace path parameter value --------- Co-authored-by: game5413 Co-authored-by: Anoop M D * feat: Refactor request parameter handling - Update prepare-request.js to filter and rename 'paths' to 'params' with type 'path' - Remove 'paths' from export.js and interpolate-vars.js - Update bru.js to use 'params' instead of 'path' - Update requestSchema in index.js to use 'keyValueWithTypeSchema' for 'params' Co-authored-by: game5413 Co-authored-by: Anoop M D * feat: Refactor request parameter handling * refactor: changes form the review * refactor: Refactor transformItemsInCollection handling * refactor: Refactor improved export/import functionalities * refactor: Remove console.log statement in bruToJson.js --------- Co-authored-by: game5413 <37659721+game5413@users.noreply.github.com> Co-authored-by: game5413 Co-authored-by: Anoop M D --- contributing.md | 2 +- docs/contributing/contributing_ja.md | 10 +- docs/contributing/contributing_pl.md | 4 +- docs/publishing/publishing_ja.md | 8 +- .../RequestPane/HttpRequestPane/index.js | 13 +- .../RequestPane/QueryParams/StyledWrapper.js | 3 + .../RequestPane/QueryParams/index.js | 242 ++++++++++++------ .../CollectionItem/GenerateCodeItem/index.js | 47 +++- .../ReduxStore/slices/collections/index.js | 125 +++++++-- .../bruno-app/src/utils/codegenerator/har.js | 2 +- .../bruno-app/src/utils/collections/export.js | 3 - .../bruno-app/src/utils/collections/index.js | 6 +- .../src/utils/exporters/postman-collection.js | 34 ++- .../bruno-app/src/utils/importers/common.js | 8 +- .../utils/importers/insomnia-collection.js | 12 + .../src/utils/importers/openapi-collection.js | 12 +- .../src/utils/importers/postman-collection.js | 12 + packages/bruno-app/src/utils/url/index.js | 36 +++ .../bruno-app/src/utils/url/index.spec.js | 47 +++- .../bruno-cli/src/runner/interpolate-vars.js | 32 ++- .../bruno-cli/src/runner/prepare-request.js | 3 +- packages/bruno-cli/src/utils/bru.js | 4 +- packages/bruno-electron/src/bru/index.js | 8 +- .../src/ipc/network/interpolate-vars.js | 32 ++- .../src/ipc/network/prepare-request.js | 1 + packages/bruno-lang/v2/src/bruToJson.js | 39 ++- packages/bruno-lang/v2/src/jsonToBru.js | 47 ++-- .../bruno-schema/src/collections/index.js | 13 +- .../src/collections/index.spec.js | 2 + .../src/collections/requestSchema.spec.js | 2 + packages/bruno-toml/tests/index.spec.js | 45 ++++ 31 files changed, 691 insertions(+), 163 deletions(-) diff --git a/contributing.md b/contributing.md index c6b3b632c..81fc27a22 100644 --- a/contributing.md +++ b/contributing.md @@ -1,5 +1,5 @@ **English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) -| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md) +| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md) ## Let's make Bruno better, together !! diff --git a/docs/contributing/contributing_ja.md b/docs/contributing/contributing_ja.md index 7a4f195f3..c40e01681 100644 --- a/docs/contributing/contributing_ja.md +++ b/docs/contributing/contributing_ja.md @@ -1,13 +1,13 @@ [English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | **日本語** -## 一緒にBrunoをよりよいものにしていきましょう!! +## 一緒に Bruno をよりよいものにしていきましょう!! -Brunoを改善していただけるのは歓迎です。以下はあなたの環境でBrunoを起動するためのガイドラインです。 +Bruno を改善していただけるのは歓迎です。以下はあなたの環境で Bruno を起動するためのガイドラインです。 ### 技術スタック -BrunoはNext.jsとReactで作られています。デスクトップアプリ(ローカルのコレクションに対応しています)にはelectronも使用しています。 +Bruno は Next.js と React で作られています。デスクトップアプリ(ローカルのコレクションに対応しています)には electron も使用しています。 使用ライブラリ @@ -22,11 +22,11 @@ BrunoはNext.jsとReactで作られています。デスクトップアプリ( ### 依存関係 -[Node v18.x もしくは最新のLTSバージョン](https://nodejs.org/en/)とnpm 8.xが必要です。プロジェクトにnpmワークスペースを使用しています。 +[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。 ## 開発 -Brunoはデスクトップアプリとして開発されています。一つのターミナルでNext.jsアプリを立ち上げ、もう一つのターミナルでelectronアプリを立ち上げてアプリを読み込む必要があります。 +Bruno はデスクトップアプリとして開発されています。一つのターミナルで Next.js アプリを立ち上げ、もう一つのターミナルで electron アプリを立ち上げてアプリを読み込む必要があります。 ### ローカル環境での開発 diff --git a/docs/contributing/contributing_pl.md b/docs/contributing/contributing_pl.md index 3c10af8de..0d67045c4 100644 --- a/docs/contributing/contributing_pl.md +++ b/docs/contributing/contributing_pl.md @@ -33,7 +33,7 @@ Bruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, u ### Lokalny Rozwój -```bash +````bash # użyj wersji nodejs 18 nvm use @@ -66,7 +66,7 @@ done # Usuń package-lock w podkatalogach find . -type f -name "package-lock.json" -delete -``` +```` ### Testowanie diff --git a/docs/publishing/publishing_ja.md b/docs/publishing/publishing_ja.md index 681a806b1..f5d60b00e 100644 --- a/docs/publishing/publishing_ja.md +++ b/docs/publishing/publishing_ja.md @@ -1,8 +1,8 @@ [English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [正體中文](docs/publishing/publishing_zhtw.md) | **日本語** -### Brunoを新しいパッケージマネージャに公開する場合の注意 +### Bruno を新しいパッケージマネージャに公開する場合の注意 -私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私はBrunoの製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャでBrunoを使いたい場合は、GitHubのissueを立ててください。 +私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私は Bruno の製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャで Bruno を使いたい場合は、GitHub の issue を立ててください。 -私たちの機能の大部分が無料でオープンソース(RESTやGraphQLのAPIも含む)ですが、 -私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269 \ No newline at end of file +私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、 +私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269 diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 834751848..df90082c6 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -1,5 +1,4 @@ import React from 'react'; -import find from 'lodash/find'; import classnames from 'classnames'; import { useSelector, useDispatch } from 'react-redux'; import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs'; @@ -14,7 +13,7 @@ import Assertions from 'components/RequestPane/Assertions'; import Script from 'components/RequestPane/Script'; import Tests from 'components/RequestPane/Tests'; import StyledWrapper from './StyledWrapper'; -import { get } from 'lodash'; +import { find, get } from 'lodash'; import Documentation from 'components/Documentation/index'; const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { @@ -81,6 +80,8 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { }); }; + const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab); + // get the length of active params, headers, asserts and vars const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []); const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []); @@ -99,7 +100,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('params')}> - Query + Params {activeParamsLength > 0 && {activeParamsLength}}
selectTab('body')}> @@ -136,9 +137,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { ) : null}
{getTabPanel(focusedTab.requestPaneTab)}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js index d3dc58d5c..5c3e1d537 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js @@ -1,6 +1,9 @@ import styled from 'styled-components'; const Wrapper = styled.div` + div.title { + color: var(--color-tab-inactive); + } table { width: 100%; border-collapse: collapse; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 54e3ee0b3..ecb84b6a9 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -1,12 +1,18 @@ import React from 'react'; import get from 'lodash/get'; import cloneDeep from 'lodash/cloneDeep'; +import has from 'lodash/has'; import { IconTrash } from '@tabler/icons'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; -import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections'; +import { + addQueryParam, + deleteQueryParam, + updatePathParam, + updateQueryParam +} from 'providers/ReduxStore/slices/collections'; import SingleLineEditor from 'components/SingleLineEditor'; -import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; @@ -14,8 +20,10 @@ const QueryParams = ({ item, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params'); + const queryParams = params.filter((param) => param.type === 'query'); + const pathParams = params.filter((param) => param.type === 'path'); - const handleAddParam = () => { + const handleAddQueryParam = () => { dispatch( addQueryParam({ itemUid: item.uid, @@ -26,24 +34,39 @@ const QueryParams = ({ item, collection }) => { const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleParamChange = (e, _param, type) => { - const param = cloneDeep(_param); + + const handleValueChange = (data, type, value) => { + const _data = cloneDeep(data); + + if (!has(_data, type)) { + return; + } + + _data[type] = value; + + return _data; + }; + + const handleQueryParamChange = (e, data, type) => { + let value; switch (type) { case 'name': { - param.name = e.target.value; + value = e.target.value; break; } case 'value': { - param.value = e.target.value; + value = e.target.value; break; } case 'enabled': { - param.enabled = e.target.checked; + value = e.target.checked; break; } } + const param = handleValueChange(data, type, value); + dispatch( updateQueryParam({ param, @@ -53,7 +76,21 @@ const QueryParams = ({ item, collection }) => { ); }; - const handleRemoveParam = (param) => { + const handlePathParamChange = (e, data) => { + let value = e.target.value; + + const path = handleValueChange(data, 'value', value); + + dispatch( + updatePathParam({ + path, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + + const handleRemoveQueryParam = (param) => { dispatch( deleteQueryParam({ paramUid: param.uid, @@ -64,75 +101,128 @@ const QueryParams = ({ item, collection }) => { }; return ( - - - - - - - - - - - {params && params.length - ? params.map((param, index) => { - return ( - - - - + + + + ); + }) + : null} + +
NameValue
- handleParamChange(e, param, 'name')} - /> - - - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - onRun={handleRun} - collection={collection} - /> - -
+ +
+
Query
+ + + + + + + + + + {queryParams && queryParams.length + ? queryParams.map((param, index) => { + return ( + + - - ); - }) - : null} - -
NameValue
handleParamChange(e, param, 'enabled')} + type="text" + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + value={param.name} + className="mousetrap" + onChange={(e) => handleQueryParamChange(e, param, 'name')} /> - - -
- +
+ + handleQueryParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleQueryParamChange(e, param, 'enabled')} + /> + +
+
+ +
Path
+ + + + + + + + + {pathParams && pathParams.length + ? pathParams.map((path, index) => { + return ( + + + + + ); + }) + : null} + +
NameValue
+ + + + handlePathParamChange( + { + target: { + value: newValue + } + }, + path + ) + } + onRun={handleRun} + collection={collection} + /> +
+
); }; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index ed1bc3f64..a768db8ce 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -2,8 +2,8 @@ import Modal from 'components/Modal/index'; import { useState } from 'react'; import CodeView from './CodeView'; import StyledWrapper from './StyledWrapper'; -import { isValidUrl } from 'utils/url/index'; -import get from 'lodash/get'; +import { isValidUrl } from 'utils/url'; +import { find, get } from 'lodash'; import { findEnvironmentInCollection } from 'utils/collections'; // Todo: Fix this @@ -27,6 +27,44 @@ const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) = }); }; +const joinPathUrl = (url, params) => { + const processPaths = (uri, paths) => { + return uri + .split('/') + .map((segment) => { + if (segment.startsWith(':')) { + const paramName = segment.slice(1); + const param = paths.find((p) => p.name === paramName && p.type === 'path' && p.enabled); + return param ? param.value : segment; + } + return segment; + }) + .join('/'); + }; + + const processQueryParams = (search, params) => { + const queryParams = new URLSearchParams(search); + params + .filter((p) => p.type === 'query' && p.enabled) + .forEach((param) => { + queryParams.set(param.name, param.value); + }); + return queryParams.toString(); + }; + + let uri; + try { + uri = new URL(url); + } catch (error) { + uri = new URL(`http://${url}`); + } + + const basePath = processPaths(uri.pathname, params); + const queryString = processQueryParams(uri.search, params); + + return `${uri.origin}${basePath}${queryString ? `?${queryString}` : ''}`; +}; + const languages = [ { name: 'HTTP', @@ -76,7 +114,10 @@ const languages = [ ]; const GenerateCodeItem = ({ collection, item, onClose }) => { - const url = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'); + const url = joinPathUrl( + get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'), + get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params') + ); const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); let envVars = {}; if (environment) { diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 15afa72f5..8cba29c5a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1,13 +1,6 @@ +import { uuid } from 'utils/common'; +import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash'; import { createSlice } from '@reduxjs/toolkit'; -import cloneDeep from 'lodash/cloneDeep'; -import concat from 'lodash/concat'; -import each from 'lodash/each'; -import filter from 'lodash/filter'; -import find from 'lodash/find'; -import forOwn from 'lodash/forOwn'; -import get from 'lodash/get'; -import map from 'lodash/map'; -import set from 'lodash/set'; import { addDepth, areItemsTheSameExceptSeqUpdate, @@ -21,9 +14,9 @@ import { findItemInCollectionByPathname, isItemARequest } from 'utils/collections'; -import { uuid } from 'utils/common'; -import { PATH_SEPARATOR, getDirectoryName, getSubdirectoriesFromRoot } from 'utils/common/platform'; -import { parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; +import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; +import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform'; +import toast from 'react-hot-toast'; const initialState = { collections: [], @@ -294,10 +287,35 @@ export const collectionsSlice = createSlice({ if (collection && collection.items && collection.items.length) { const parts = splitOnFirst(action.payload.requestUrl, '?'); - const params = parseQueryParams(parts[1]); - each(params, (urlParam) => { - urlParam.enabled = true; - }); + const queryParams = parseQueryParams(parts[1]); + + let pathParams = []; + try { + pathParams = parsePathParams(parts[0]); + } catch (err) { + console.error(err); + toast.error(err.message); + } + + const queryParamObjects = queryParams.map((param) => ({ + uid: uuid(), + name: param.key, + value: param.value, + description: '', + type: 'query', + enabled: true + })); + + const pathParamObjects = pathParams.map((param) => ({ + uid: uuid(), + name: param.key, + value: param.value, + description: '', + type: 'path', + enabled: true + })); + + const params = [...queryParamObjects, ...pathParamObjects]; const item = { uid: action.payload.uid, @@ -351,14 +369,26 @@ export const collectionsSlice = createSlice({ const parts = splitOnFirst(item.draft.request.url, '?'); const urlParams = parseQueryParams(parts[1]); + let urlPaths = []; + + try { + urlPaths = parsePathParams(parts[0]); + } catch (err) { + console.error(err); + toast.error(err.message); + } + const disabledParams = filter(item.draft.request.params, (p) => !p.enabled); - let enabledParams = filter(item.draft.request.params, (p) => p.enabled); + let enabledParams = filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'); + let oldPaths = filter(item.draft.request.params, (p) => p.enabled && p.type === 'path'); + let newPaths = []; // try and connect as much as old params uid's as possible each(urlParams, (urlParam) => { const existingParam = find(enabledParams, (p) => p.name === urlParam.name || p.value === urlParam.value); urlParam.uid = existingParam ? existingParam.uid : uuid(); urlParam.enabled = true; + urlParam.type = 'query'; // once found, remove it - trying our best here to accommodate duplicate query params if (existingParam) { @@ -366,10 +396,27 @@ export const collectionsSlice = createSlice({ } }); + // filter the newest path param and compare with previous data that already inserted + newPaths = filter(urlPaths, (urlPath) => { + const existingPath = find(oldPaths, (p) => p.name === urlPath.name); + if (existingPath) { + return false; + } + urlPath.uid = uuid(); + urlPath.enabled = true; + urlPath.type = 'path'; + return true; + }); + + // remove path param that not used or deleted when typing url + oldPaths = filter(oldPaths, (urlPath) => { + return find(urlPaths, (p) => p.name === urlPath.name); + }); + // ultimately params get replaced with params in url + the disabled ones that existed prior // the query params are the source of truth, the url in the queryurl input gets constructed using these params // we however are also storing the full url (with params) in the url itself - item.draft.request.params = concat(urlParams, disabledParams); + item.draft.request.params = concat(urlParams, newPaths, disabledParams, oldPaths); } } }, @@ -426,6 +473,7 @@ export const collectionsSlice = createSlice({ name: '', value: '', description: '', + type: 'query', enabled: true }); } @@ -441,16 +489,20 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid); - if (param) { - param.name = action.payload.param.name; - param.value = action.payload.param.value; - param.description = action.payload.param.description; - param.enabled = action.payload.param.enabled; + const queryParam = find( + item.draft.request.params, + (h) => h.uid === action.payload.param.uid && h.type === 'query' + ); + if (queryParam) { + queryParam.name = action.payload.param.name; + queryParam.value = action.payload.param.value; + queryParam.enabled = action.payload.param.enabled; // update request url const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled)); + const query = stringifyQueryParams( + filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') + ); // if no query is found, then strip the query params in url if (!query || !query.length) { @@ -486,7 +538,7 @@ export const collectionsSlice = createSlice({ // update request url const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled)); + const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); if (query && query.length) { item.draft.request.url = parts[0] + '?' + query; } else { @@ -495,6 +547,26 @@ export const collectionsSlice = createSlice({ } } }, + updatePathParam: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + + const param = find(item.draft.request.params, (p) => p.uid === action.payload.path.uid && p.type === 'path'); + + if (param) { + param.name = action.payload.path.name; + param.value = action.payload.path.value; + } + } + } + }, addRequestHeader: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1418,6 +1490,7 @@ export const { addQueryParam, updateQueryParam, deleteQueryParam, + updatePathParam, addRequestHeader, updateRequestHeader, deleteRequestHeader, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 0f2656370..0e3476256 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -24,7 +24,7 @@ const createHeaders = (headers) => { const createQuery = (queryParams = []) => { return queryParams - .filter((param) => param.enabled) + .filter((param) => param.enabled && param.type === 'query') .map((param) => ({ name: param.name, value: param.value diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index 17c979fe6..5ef7b1b49 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -29,9 +29,6 @@ export const deleteUidsInItems = (items) => { export const transformItem = (items = []) => { each(items, (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { - item.request.query = item.request.params; - delete item.request.params; - if (item.type === 'graphql-request') { item.type = 'graphql'; } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 615d81952..8b08174e2 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -228,13 +228,14 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; - const copyQueryParams = (params) => { + const copyParams = (params) => { return map(params, (param) => { return { uid: param.uid, name: param.name, value: param.value, description: param.description, + type: param.type, enabled: param.enabled }; }); @@ -283,7 +284,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} url: si.request.url, method: si.request.method, headers: copyHeaders(si.request.headers), - params: copyQueryParams(si.request.params), + params: copyParams(si.request.params), body: { mode: si.request.body.mode, json: si.request.body.json, @@ -441,6 +442,7 @@ export const transformRequestToSaveToFilesystem = (item) => { name: param.name, value: param.value, description: param.description, + type: param.type, enabled: param.enabled }); }); diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js index 309a77d05..719391f0a 100644 --- a/packages/bruno-app/src/utils/exporters/postman-collection.js +++ b/packages/bruno-app/src/utils/exporters/postman-collection.js @@ -171,11 +171,43 @@ export const exportCollection = (collection) => { } }; + const generateHost = (url) => { + try { + const { hostname } = new URL(url); + return hostname.split('.'); + } catch (error) { + console.error(`Invalid URL: ${url}`, error); + return []; + } + }; + + const generatePathParams = (params) => { + return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`); + }; + + const generateQueryParams = (params) => { + return params + .filter((param) => param.type === 'query') + .map(({ name, value, description }) => ({ key: name, value, description })); + }; + + const generateVariables = (params) => { + return params + .filter((param) => param.type === 'path') + .map(({ name, value, description }) => ({ key: name, value, description })); + }; + const generateRequestSection = (itemRequest) => { const requestObject = { method: itemRequest.method, header: generateHeaders(itemRequest.headers), - url: itemRequest.url, + url: { + raw: itemRequest.url, + host: generateHost(itemRequest.url), + path: generatePathParams(itemRequest.params), + query: generateQueryParams(itemRequest.params), + variable: generateVariables(itemRequest.params) + }, auth: generateAuth(itemRequest.auth) }; diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index f1e17ac00..c99048419 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -29,7 +29,6 @@ export const updateUidsInCollection = (_collection) => { item.uid = uuid(); each(get(item, 'request.headers'), (header) => (header.uid = uuid())); - each(get(item, 'request.query'), (param) => (param.uid = uuid())); each(get(item, 'request.params'), (param) => (param.uid = uuid())); each(get(item, 'request.vars.req'), (v) => (v.uid = uuid())); each(get(item, 'request.vars.res'), (v) => (v.uid = uuid())); @@ -66,8 +65,13 @@ export const transformItemsInCollection = (collection) => { if (['http', 'graphql'].includes(item.type)) { item.type = `${item.type}-request`; + if (item.request.query) { - item.request.params = item.request.query; + item.request.params = item.request.query.map((queryItem) => ({ + ...queryItem, + type: 'query', + uid: queryItem.uid || uuid() + })); } delete item.request.query; diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js index 0a38a85e5..0fec995ca 100644 --- a/packages/bruno-app/src/utils/importers/insomnia-collection.js +++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js @@ -112,10 +112,22 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { name: param.name, value: param.value, description: param.description, + type: 'query', enabled: !param.disabled }); }); + each(request.pathParameters, (param) => { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.name, + value: param.value, + description: '', + type: 'path', + enabled: true + }); + }); + const authType = get(request, 'authentication.type', ''); if (authType === 'basic') { diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 1900f6e47..01fb66c01 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -92,7 +92,17 @@ const transformOpenapiRequestItem = (request) => { name: param.name, value: '', description: param.description || '', - enabled: param.required + enabled: param.required, + type: 'query' + }); + } else if (param.in === 'path') { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.name, + value: '', + description: param.description || '', + enabled: param.required, + type: 'path' }); } else if (param.in === 'header') { brunoRequestItem.request.headers.push({ diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 902b1a2dc..e34530a19 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -275,10 +275,22 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = name: param.key, value: param.value, description: param.description, + type: 'query', enabled: !param.disabled }); }); + each(get(i, 'request.url.variable'), (param) => { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + type: 'path', + enabled: true + }); + }); + brunoParent.items.push(brunoRequestItem); } } diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index 328b22cdc..e1aea7316 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -2,6 +2,7 @@ import isEmpty from 'lodash/isEmpty'; import trim from 'lodash/trim'; import each from 'lodash/each'; import filter from 'lodash/filter'; +import find from 'lodash/find'; const hasLength = (str) => { if (!str || !str.length) { @@ -26,6 +27,41 @@ export const parseQueryParams = (query) => { return filter(params, (p) => hasLength(p.name)); }; +export const parsePathParams = (url) => { + let uri = url.slice(); + + if (!uri || !uri.length) { + return []; + } + + if (!uri.startsWith('http://') && !uri.startsWith('https://')) { + uri = `http://${uri}`; + } + + try { + uri = new URL(uri); + } catch (e) { + throw e; + } + + let paths = uri.pathname.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: '' }); + } + } + } + return acc; + }, []); + + return paths; +}; + export const stringifyQueryParams = (params) => { if (!params || isEmpty(params)) { return ''; diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index 02112cdf2..1f43affaf 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -1,4 +1,4 @@ -import { parseQueryParams, splitOnFirst } from './index'; +import { parseQueryParams, splitOnFirst, parsePathParams } from './index'; describe('Url Utils - parseQueryParams', () => { it('should parse query - case 1', () => { @@ -51,6 +51,51 @@ describe('Url Utils - parseQueryParams', () => { }); }); +describe('Url Utils - parsePathParams', () => { + it('should parse path - case 1', () => { + const params = parsePathParams('www.example.com'); + expect(params).toEqual([]); + }); + + it('should parse path - case 2', () => { + const params = parsePathParams('http://www.example.com'); + expect(params).toEqual([]); + }); + + it('should parse path - case 3', () => { + const params = parsePathParams('https://www.example.com'); + expect(params).toEqual([]); + }); + + it('should parse path - case 4', () => { + const params = parsePathParams('https://www.example.com/users/:id'); + expect(params).toEqual([{ name: 'id', value: '' }]); + }); + + it('should parse path - case 5', () => { + const params = parsePathParams('https://www.example.com/users/:id/'); + expect(params).toEqual([{ name: 'id', value: '' }]); + }); + + it('should parse path - case 6', () => { + const params = parsePathParams('https://www.example.com/users/:id/:'); + expect(params).toEqual([{ name: 'id', value: '' }]); + }); + + it('should parse path - case 7', () => { + const params = parsePathParams('https://www.example.com/users/:id/posts/:id'); + expect(params).toEqual([{ name: 'id', value: '' }]); + }); + + it('should parse path - case 8', () => { + const params = parsePathParams('https://www.example.com/users/:id/posts/:postId'); + expect(params).toEqual([ + { name: 'id', value: '' }, + { name: 'postId', value: '' } + ]); + }); +}); + describe('Url Utils - splitOnFirst', () => { it('should split on first - case 1', () => { const params = splitOnFirst('a', '='); diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 63ebdd4ca..e84fc7f0a 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -1,5 +1,5 @@ const { interpolate } = require('@usebruno/common'); -const { each, forOwn, cloneDeep } = require('lodash'); +const { each, forOwn, cloneDeep, find } = require('lodash'); const getContentType = (headers = {}) => { let contentType = ''; @@ -86,6 +86,36 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces param.value = _interpolate(param.value); }); + if (request.params.length) { + let url = request.url; + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `http://${url}`; + } + + try { + url = new URL(url); + } catch (e) { + throw { message: 'Invalid URL format', originalError: e.message }; + } + + const urlPaths = url.pathname + .split('/') + .filter((path) => path !== '') + .map((path) => { + if (path[0] !== ':') { + return '/' + path; + } else { + const name = path.slice(1); + const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name); + return existingPathParam ? '/' + existingPathParam.value : ''; + } + }) + .join(''); + + request.url = url.origin + urlPaths + url.search; + } + if (request.proxy) { request.proxy.protocol = _interpolate(request.proxy.protocol); request.proxy.hostname = _interpolate(request.proxy.hostname); diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 8b1b249db..1e7e083cd 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -29,7 +29,8 @@ const prepareRequest = (request, collectionRoot) => { let axiosRequest = { method: request.method, url: request.url, - headers: headers + headers: headers, + paths: request.paths }; const collectionAuth = get(collectionRoot, 'request.auth'); diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 34fb09c6b..262cca650 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -13,7 +13,7 @@ const collectionBruToJson = (bru) => { const transformedJson = { request: { - params: _.get(json, 'query', []), + params: _.get(json, 'params', []), headers: _.get(json, 'headers', []), auth: _.get(json, 'auth', {}), script: _.get(json, 'script', {}), @@ -60,7 +60,7 @@ const bruToJson = (bru) => { method: _.upperCase(_.get(json, 'http.method')), url: _.get(json, 'http.url'), auth: _.get(json, 'auth', {}), - params: _.get(json, 'query', []), + params: _.get(json, 'params', []), headers: _.get(json, 'headers', []), body: _.get(json, 'body', {}), vars: _.get(json, 'vars', []), diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index de1080ac0..41c35d53d 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -14,7 +14,7 @@ const collectionBruToJson = (bru) => { const transformedJson = { request: { - params: _.get(json, 'query', []), + params: _.get(json, 'params', []), headers: _.get(json, 'headers', []), auth: _.get(json, 'auth', {}), script: _.get(json, 'script', {}), @@ -33,7 +33,7 @@ const collectionBruToJson = (bru) => { const jsonToCollectionBru = (json) => { try { const collectionBruJson = { - query: _.get(json, 'request.params', []), + params: _.get(json, 'request.params', []), headers: _.get(json, 'request.headers', []), auth: _.get(json, 'request.auth', {}), script: { @@ -111,7 +111,7 @@ const bruToJson = (bru) => { request: { method: _.upperCase(_.get(json, 'http.method')), url: _.get(json, 'http.url'), - params: _.get(json, 'query', []), + params: _.get(json, 'params', []), headers: _.get(json, 'headers', []), auth: _.get(json, 'auth', {}), body: _.get(json, 'body', {}), @@ -162,7 +162,7 @@ const jsonToBru = (json) => { auth: _.get(json, 'request.auth.mode', 'none'), body: _.get(json, 'request.body.mode', 'none') }, - query: _.get(json, 'request.params', []), + params: _.get(json, 'request.params', []), headers: _.get(json, 'request.headers', []), auth: _.get(json, 'request.auth', {}), body: _.get(json, 'request.body', {}), diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 2139194a2..886ead69b 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -1,5 +1,5 @@ const { interpolate } = require('@usebruno/common'); -const { each, forOwn, cloneDeep } = require('lodash'); +const { each, forOwn, cloneDeep, find } = require('lodash'); const getContentType = (headers = {}) => { let contentType = ''; @@ -86,6 +86,36 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces param.value = _interpolate(param.value); }); + if (request.params.length) { + let url = request.url; + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `http://${url}`; + } + + try { + url = new URL(url); + } catch (e) { + throw { message: 'Invalid URL format', originalError: e.message }; + } + + const urlPaths = url.pathname + .split('/') + .filter((path) => path !== '') + .map((path) => { + if (path[0] !== ':') { + return '/' + path; + } else { + const name = path.slice(1); + const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name); + return existingPathParam ? '/' + existingPathParam.value : ''; + } + }) + .join(''); + + request.url = url.origin + urlPaths + url.search; + } + if (request.proxy) { request.proxy.protocol = _interpolate(request.proxy.protocol); request.proxy.hostname = _interpolate(request.proxy.hostname); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index e8c88275f..94fe834b1 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -161,6 +161,7 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { method: request.method, url, headers, + params: request.params.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 6f12a6ce5..d62888d2c 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -22,10 +22,11 @@ const { outdentString } = require('../../v1/src/utils'); * */ const grammar = ohm.grammar(`Bru { - BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)* + BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart + params = paramspath | paramsquery nl = "\\r"? "\\n" st = " " | "\\t" @@ -74,6 +75,8 @@ const grammar = ohm.grammar(`Bru { headers = "headers" dictionary query = "query" dictionary + paramspath = "params:path" dictionary + paramsquery = "params:query" dictionary varsandassert = varsreq | varsres | assert varsreq = "vars:pre-request" dictionary @@ -133,6 +136,28 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { }); }; +const mapPairListToKeyValPairsWithType = (pairList = [], type) => { + if (!pairList.length) { + return []; + } + return _.map(pairList[0], (pair) => { + let name = _.keys(pair)[0]; + let value = pair[name]; + let enabled = true; + if (name && name.length && name.charAt(0) === '~') { + name = name.slice(1); + enabled = false; + } + + return { + name, + value, + enabled, + type + }; + }); +}; + const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); @@ -321,7 +346,17 @@ const sem = grammar.createSemantics().addAttribute('ast', { }, query(_1, dictionary) { return { - query: mapPairListToKeyValPairs(dictionary.ast) + params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query') + }; + }, + paramspath(_1, dictionary) { + return { + params: mapPairListToKeyValPairsWithType(dictionary.ast, 'path') + }; + }, + paramsquery(_1, dictionary) { + return { + params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query') }; }, headers(_1, dictionary) { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 3357e5d09..45d7bba0f 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -30,7 +30,7 @@ const getValueString = (value) => { }; const jsonToBru = (json) => { - const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json; + const { meta, http, params, headers, auth, body, script, tests, vars, assertions, docs } = json; let bru = ''; @@ -62,25 +62,38 @@ const jsonToBru = (json) => { `; } - if (query && query.length) { - bru += 'query {'; - if (enabled(query).length) { - bru += `\n${indentString( - enabled(query) - .map((item) => `${item.name}: ${item.value}`) - .join('\n') - )}`; + if (params && params.length) { + const queryParams = params.filter((param) => param.type === 'query'); + const pathParams = params.filter((param) => param.type === 'path'); + + if (queryParams.length) { + bru += 'params:query {'; + if (enabled(queryParams).length) { + bru += `\n${indentString( + enabled(queryParams) + .map((item) => `${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + if (disabled(queryParams).length) { + bru += `\n${indentString( + disabled(queryParams) + .map((item) => `~${item.name}: ${item.value}`) + .join('\n') + )}`; + } + + bru += '\n}\n\n'; } - if (disabled(query).length) { - bru += `\n${indentString( - disabled(query) - .map((item) => `~${item.name}: ${item.value}`) - .join('\n') - )}`; - } + if (pathParams.length) { + bru += 'params:path {'; - bru += '\n}\n\n'; + bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${item.value}`).join('\n'))}`; + + bru += '\n}\n\n'; + } } if (headers && headers.length) { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 6bf8dd2e4..b1bf21d3e 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -185,6 +185,17 @@ const authSchema = Yup.object({ .noUnknown(true) .strict(); +const keyValueWithTypeSchema = Yup.object({ + uid: uidSchema, + name: Yup.string().nullable(), + value: Yup.string().nullable(), + description: Yup.string().nullable(), + type: Yup.string().oneOf(['query', 'path']).required('type is required'), + enabled: Yup.boolean() +}) + .noUnknown(true) + .strict(); + // Right now, the request schema is very tightly coupled with http request // As we introduce more request types in the future, we will improve the definition to support // schema structure based on other request type @@ -192,7 +203,7 @@ const requestSchema = Yup.object({ url: requestUrlSchema, method: requestMethodSchema, headers: Yup.array().of(keyValueSchema).required('headers are required'), - params: Yup.array().of(keyValueSchema).required('params are required'), + params: Yup.array().of(keyValueWithTypeSchema).required('params are required'), auth: authSchema, body: requestBodySchema, script: Yup.object({ diff --git a/packages/bruno-schema/src/collections/index.spec.js b/packages/bruno-schema/src/collections/index.spec.js index 16b683d08..0c6b69156 100644 --- a/packages/bruno-schema/src/collections/index.spec.js +++ b/packages/bruno-schema/src/collections/index.spec.js @@ -59,6 +59,7 @@ describe('Collection Schema Validation', () => { method: 'GET', headers: [], params: [], + paths: [], body: { mode: 'none' } @@ -116,6 +117,7 @@ describe('Collection Schema Validation', () => { method: 'GET', headers: [], params: [], + paths: [], body: { mode: 'none' } diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js index 87399c690..2430b2862 100644 --- a/packages/bruno-schema/src/collections/requestSchema.spec.js +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -9,6 +9,7 @@ describe('Request Schema Validation', () => { method: 'GET', headers: [], params: [], + paths: [], body: { mode: 'none' } @@ -24,6 +25,7 @@ describe('Request Schema Validation', () => { method: 'GET-junk', headers: [], params: [], + paths: [], body: { mode: 'none' } diff --git a/packages/bruno-toml/tests/index.spec.js b/packages/bruno-toml/tests/index.spec.js index bcd00b77f..cad9d2e17 100644 --- a/packages/bruno-toml/tests/index.spec.js +++ b/packages/bruno-toml/tests/index.spec.js @@ -43,3 +43,48 @@ describe('bruno toml', () => { }); }); }); +describe('joinPathUrl', () => { + it('should join path and query params correctly', () => { + const url = 'https://example.com/api/:id'; + const params = [ + { name: 'id', type: 'path', enabled: true, value: '123' }, + { name: 'sort', type: 'query', enabled: true, value: 'asc' }, + { name: 'filter', type: 'query', enabled: true, value: 'active' } + ]; + const expectedUrl = 'https://example.com/api/123?sort=asc&filter=active'; + + const result = joinPathUrl(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle empty path and query params', () => { + const url = 'https://example.com/api'; + const params = []; + const expectedUrl = 'https://example.com/api'; + + const result = joinPathUrl(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle empty query params', () => { + const url = 'https://example.com/api/:id'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'https://example.com/api/123'; + + const result = joinPathUrl(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle invalid URL', () => { + const url = 'example.com/api/:id'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'http://example.com/api/123'; + + const result = joinPathUrl(url, params); + + expect(result).toEqual(expectedUrl); + }); +}); From 470d162fb6c1821ad06ee87b6488d7cc3f281a74 Mon Sep 17 00:00:00 2001 From: lohit Date: Thu, 30 May 2024 23:09:34 +0530 Subject: [PATCH 009/904] fix/path param (#2388) * fix(#484): minor code fixes * code fixes * fixes for generateCode * var change * pr review fixes --- .../RequestPane/QueryParams/index.js | 36 +++++------ .../CollectionItem/GenerateCodeItem/index.js | 53 ++++++++-------- .../ReduxStore/slices/collections/index.js | 62 ++++++++++--------- .../bruno-cli/src/runner/interpolate-vars.js | 4 +- .../bruno-cli/src/runner/prepare-request.js | 3 +- .../src/ipc/network/interpolate-vars.js | 4 +- packages/bruno-lang/v2/src/bruToJson.js | 8 +-- .../bruno-schema/src/collections/index.js | 4 +- .../src/collections/index.spec.js | 2 - .../src/collections/requestSchema.spec.js | 2 - 10 files changed, 88 insertions(+), 90 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index ecb84b6a9..162d57a43 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -35,22 +35,10 @@ const QueryParams = ({ item, collection }) => { const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleValueChange = (data, type, value) => { - const _data = cloneDeep(data); - - if (!has(_data, type)) { - return; - } - - _data[type] = value; - - return _data; - }; - - const handleQueryParamChange = (e, data, type) => { + const handleQueryParamChange = (e, data, key) => { let value; - switch (type) { + switch (key) { case 'name': { value = e.target.value; break; @@ -65,11 +53,17 @@ const QueryParams = ({ item, collection }) => { } } - const param = handleValueChange(data, type, value); + let queryParam = cloneDeep(data); + + if (queryParam[key] === value) { + return; + } + + queryParam[key] = value; dispatch( updateQueryParam({ - param, + queryParam, itemUid: item.uid, collectionUid: collection.uid }) @@ -79,11 +73,17 @@ const QueryParams = ({ item, collection }) => { const handlePathParamChange = (e, data) => { let value = e.target.value; - const path = handleValueChange(data, 'value', value); + let pathParam = cloneDeep(data); + + if (pathParam['value'] === value) { + return; + } + + pathParam['value'] = value; dispatch( updatePathParam({ - path, + pathParam, itemUid: item.uid, collectionUid: collection.uid }) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index a768db8ce..59cdd6281 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -27,31 +27,22 @@ const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) = }); }; -const joinPathUrl = (url, params) => { - const processPaths = (uri, paths) => { - return uri +// interpolate URL paths +const interpolateUrlPathParams = (url, params) => { + const getInterpolatedBasePath = (pathname, params) => { + return pathname .split('/') .map((segment) => { if (segment.startsWith(':')) { - const paramName = segment.slice(1); - const param = paths.find((p) => p.name === paramName && p.type === 'path' && p.enabled); - return param ? param.value : segment; + const pathParamName = segment.slice(1); + const pathParam = params.find((p) => p?.name === pathParamName && p?.type === 'path'); + return pathParam ? pathParam.value : segment; } return segment; }) .join('/'); }; - const processQueryParams = (search, params) => { - const queryParams = new URLSearchParams(search); - params - .filter((p) => p.type === 'query' && p.enabled) - .forEach((param) => { - queryParams.set(param.name, param.value); - }); - return queryParams.toString(); - }; - let uri; try { uri = new URL(url); @@ -59,10 +50,9 @@ const joinPathUrl = (url, params) => { uri = new URL(`http://${url}`); } - const basePath = processPaths(uri.pathname, params); - const queryString = processQueryParams(uri.search, params); + const basePath = getInterpolatedBasePath(uri.pathname, params); - return `${uri.origin}${basePath}${queryString ? `?${queryString}` : ''}`; + return `${uri.origin}${basePath}${uri?.search || ''}`; }; const languages = [ @@ -114,10 +104,6 @@ const languages = [ ]; const GenerateCodeItem = ({ collection, item, onClose }) => { - const url = joinPathUrl( - get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'), - get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params') - ); const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); let envVars = {}; if (environment) { @@ -128,12 +114,23 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { }, {}); } + const requestUrl = + get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'); + + // interpolate the query params const interpolatedUrl = interpolateUrl({ - url, + url: requestUrl, envVars, collectionVariables: collection.collectionVariables, processEnvVars: collection.processEnvVariables }); + + // interpolate the path params + const finalUrl = interpolateUrlPathParams( + interpolatedUrl, + get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params') + ); + const [selectedLanguage, setSelectedLanguage] = useState(languages[0]); return ( @@ -157,7 +154,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
- {isValidUrl(interpolatedUrl) ? ( + {isValidUrl(finalUrl) ? ( { item.request.url !== '' ? { ...item.request, - url: interpolatedUrl + url: finalUrl } : { ...item.draft.request, - url: interpolatedUrl + url: finalUrl } }} /> ) : (
-

Invalid URL: {interpolatedUrl}

+

Invalid URL: {url}

Please check the URL and try again

diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 8cba29c5a..2a851c238 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -367,39 +367,42 @@ export const collectionsSlice = createSlice({ } item.draft.request.url = action.payload.url; - const parts = splitOnFirst(item.draft.request.url, '?'); - const urlParams = parseQueryParams(parts[1]); - let urlPaths = []; + const parts = splitOnFirst(item?.draft?.request?.url, '?'); + const urlQueryParams = parseQueryParams(parts[1]); + let urlPathParams = []; try { - urlPaths = parsePathParams(parts[0]); + urlPathParams = parsePathParams(parts[0]); } catch (err) { console.error(err); toast.error(err.message); } - const disabledParams = filter(item.draft.request.params, (p) => !p.enabled); - let enabledParams = filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'); - let oldPaths = filter(item.draft.request.params, (p) => p.enabled && p.type === 'path'); - let newPaths = []; + const disabledQueryParams = filter(item?.draft?.request?.params, (p) => !p.enabled && p.type === 'query'); + let enabledQueryParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'query'); + let oldPathParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'path'); + let newPathParams = []; // try and connect as much as old params uid's as possible - each(urlParams, (urlParam) => { - const existingParam = find(enabledParams, (p) => p.name === urlParam.name || p.value === urlParam.value); - urlParam.uid = existingParam ? existingParam.uid : uuid(); - urlParam.enabled = true; - urlParam.type = 'query'; + each(urlQueryParams, (urlQueryParam) => { + const existingQueryParam = find( + enabledQueryParams, + (p) => p?.name === urlQueryParam?.name || p?.value === urlQueryParam?.value + ); + urlQueryParam.uid = existingQueryParam?.uid || uuid(); + urlQueryParam.enabled = true; + urlQueryParam.type = 'query'; // once found, remove it - trying our best here to accommodate duplicate query params - if (existingParam) { - enabledParams = filter(enabledParams, (p) => p.uid !== existingParam.uid); + if (existingQueryParam) { + enabledQueryParams = filter(enabledQueryParams, (p) => p?.uid !== existingQueryParam?.uid); } }); // filter the newest path param and compare with previous data that already inserted - newPaths = filter(urlPaths, (urlPath) => { - const existingPath = find(oldPaths, (p) => p.name === urlPath.name); - if (existingPath) { + newPathParams = filter(urlPathParams, (urlPath) => { + const existingPathParam = find(oldPathParams, (p) => p.name === urlPath.name); + if (existingPathParam) { return false; } urlPath.uid = uuid(); @@ -409,14 +412,14 @@ export const collectionsSlice = createSlice({ }); // remove path param that not used or deleted when typing url - oldPaths = filter(oldPaths, (urlPath) => { - return find(urlPaths, (p) => p.name === urlPath.name); + oldPathParams = filter(oldPathParams, (urlPath) => { + return find(urlPathParams, (p) => p.name === urlPath.name); }); // ultimately params get replaced with params in url + the disabled ones that existed prior // the query params are the source of truth, the url in the queryurl input gets constructed using these params // we however are also storing the full url (with params) in the url itself - item.draft.request.params = concat(urlParams, newPaths, disabledParams, oldPaths); + item.draft.request.params = concat(urlQueryParams, newPathParams, disabledQueryParams, oldPathParams); } } }, @@ -491,12 +494,12 @@ export const collectionsSlice = createSlice({ } const queryParam = find( item.draft.request.params, - (h) => h.uid === action.payload.param.uid && h.type === 'query' + (h) => h.uid === action.payload.queryParam.uid && h.type === 'query' ); if (queryParam) { - queryParam.name = action.payload.param.name; - queryParam.value = action.payload.param.value; - queryParam.enabled = action.payload.param.enabled; + queryParam.name = action.payload.queryParam.name; + queryParam.value = action.payload.queryParam.value; + queryParam.enabled = action.payload.queryParam.enabled; // update request url const parts = splitOnFirst(item.draft.request.url, '?'); @@ -558,11 +561,14 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - const param = find(item.draft.request.params, (p) => p.uid === action.payload.path.uid && p.type === 'path'); + const param = find( + item.draft.request.params, + (p) => p.uid === action.payload.pathParam.uid && p.type === 'path' + ); if (param) { - param.name = action.payload.path.name; - param.value = action.payload.path.value; + param.name = action.payload.pathParam.name; + param.value = action.payload.pathParam.value; } } } diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index e84fc7f0a..5dff0564f 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -99,7 +99,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces throw { message: 'Invalid URL format', originalError: e.message }; } - const urlPaths = url.pathname + const interpolatedUrlPath = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { @@ -113,7 +113,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces }) .join(''); - request.url = url.origin + urlPaths + url.search; + request.url = url.origin + interpolatedUrlPath + url.search; } if (request.proxy) { diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 1e7e083cd..8b1b249db 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -29,8 +29,7 @@ const prepareRequest = (request, collectionRoot) => { let axiosRequest = { method: request.method, url: request.url, - headers: headers, - paths: request.paths + headers: headers }; const collectionAuth = get(collectionRoot, 'request.auth'); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 886ead69b..b544ed7d1 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -99,7 +99,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces throw { message: 'Invalid URL format', originalError: e.message }; } - const urlPaths = url.pathname + const urlPathnameInterpolatedWithPathParams = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { @@ -113,7 +113,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces }) .join(''); - request.url = url.origin + urlPaths + url.search; + request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search; } if (request.proxy) { diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index d62888d2c..e78b1b8ad 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -136,7 +136,7 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { }); }; -const mapPairListToKeyValPairsWithType = (pairList = [], type) => { +const mapRequestParams = (pairList = [], type) => { if (!pairList.length) { return []; } @@ -346,17 +346,17 @@ const sem = grammar.createSemantics().addAttribute('ast', { }, query(_1, dictionary) { return { - params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query') + params: mapRequestParams(dictionary.ast, 'query') }; }, paramspath(_1, dictionary) { return { - params: mapPairListToKeyValPairsWithType(dictionary.ast, 'path') + params: mapRequestParams(dictionary.ast, 'path') }; }, paramsquery(_1, dictionary) { return { - params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query') + params: mapRequestParams(dictionary.ast, 'query') }; }, headers(_1, dictionary) { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b1bf21d3e..3dc1352c8 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -185,7 +185,7 @@ const authSchema = Yup.object({ .noUnknown(true) .strict(); -const keyValueWithTypeSchema = Yup.object({ +const requestParamsSchema = Yup.object({ uid: uidSchema, name: Yup.string().nullable(), value: Yup.string().nullable(), @@ -203,7 +203,7 @@ const requestSchema = Yup.object({ url: requestUrlSchema, method: requestMethodSchema, headers: Yup.array().of(keyValueSchema).required('headers are required'), - params: Yup.array().of(keyValueWithTypeSchema).required('params are required'), + params: Yup.array().of(requestParamsSchema).required('params are required'), auth: authSchema, body: requestBodySchema, script: Yup.object({ diff --git a/packages/bruno-schema/src/collections/index.spec.js b/packages/bruno-schema/src/collections/index.spec.js index 0c6b69156..16b683d08 100644 --- a/packages/bruno-schema/src/collections/index.spec.js +++ b/packages/bruno-schema/src/collections/index.spec.js @@ -59,7 +59,6 @@ describe('Collection Schema Validation', () => { method: 'GET', headers: [], params: [], - paths: [], body: { mode: 'none' } @@ -117,7 +116,6 @@ describe('Collection Schema Validation', () => { method: 'GET', headers: [], params: [], - paths: [], body: { mode: 'none' } diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js index 2430b2862..87399c690 100644 --- a/packages/bruno-schema/src/collections/requestSchema.spec.js +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -9,7 +9,6 @@ describe('Request Schema Validation', () => { method: 'GET', headers: [], params: [], - paths: [], body: { mode: 'none' } @@ -25,7 +24,6 @@ describe('Request Schema Validation', () => { method: 'GET-junk', headers: [], params: [], - paths: [], body: { mode: 'none' } From 3ded960938d710ba0ca202c41877ebce36635bc9 Mon Sep 17 00:00:00 2001 From: lohit Date: Thu, 30 May 2024 23:24:04 +0530 Subject: [PATCH 010/904] fix(#2367): handle response body decode (#2371) --- packages/bruno-electron/package.json | 1 + packages/bruno-electron/src/ipc/network/index.js | 10 ++++++++-- packages/bruno-tests/src/echo/index.js | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 5d4714123..4898d4a22 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -41,6 +41,7 @@ "graphql": "^16.6.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", + "iconv-lite": "^0.6.3", "is-valid-path": "^0.1.1", "js-yaml": "^4.1.0", "json-bigint": "^1.0.0", diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index a74584461..5d2e00230 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -37,6 +37,7 @@ const { transformPasswordCredentialsRequest } = require('./oauth2-helper'); const Oauth2Store = require('../../store/oauth2'); +const iconv = require('iconv-lite'); // override the default escape function to prevent escaping Mustache.escape = function (value) { @@ -258,13 +259,18 @@ const configureRequest = async ( }; const parseDataFromResponse = (response) => { - const dataBuffer = Buffer.from(response.data); // Parse the charset from content type: https://stackoverflow.com/a/33192813 const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals const charsetValue = charsetMatch?.[1]; + const dataBuffer = Buffer.from(response.data); // Overwrite the original data for backwards compatibility - let data = dataBuffer.toString(charsetValue || 'utf-8'); + let data; + if (iconv.encodingExists(charsetValue)) { + data = iconv.decode(dataBuffer, charsetValue); + } else { + data = iconv.decode(dataBuffer, 'utf-8'); + } // Try to parse response to JSON, this can quietly fail try { // Filter out ZWNBSP character diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index 2a2b52eb3..ba9b403ae 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -31,4 +31,10 @@ router.get('/bom-json-test', (req, res) => { return res.send(jsonWithBom); }); +router.get('/iso-enc', (req, res) => { + res.set('Content-Type', 'text/plain; charset=ISO-8859-1'); + const responseText = 'éçà'; + return res.send(Buffer.from(responseText, 'latin1')); +}); + module.exports = router; From 32b1ba1c92d74ef867791a1324cf2aed069c76c7 Mon Sep 17 00:00:00 2001 From: Dhananjay Kadam Date: Fri, 31 May 2024 15:35:27 +0530 Subject: [PATCH 011/904] feat: add support for state param for OAuth2 Authorization Code flow (#2330) * feat: add support for state param * chore: revert package-lock.json * test: update tests with state param * chore: revert package-lock.json * chore: add state to missing places * Adding state to PKCE toggle --------- Co-authored-by: Kadam Dhananjay --- .../Auth/OAuth2/AuthorizationCode/index.js | 4 +++- .../Auth/OAuth2/AuthorizationCode/inputsConfig.js | 4 ++++ .../Auth/OAuth2/AuthorizationCode/index.js | 4 +++- .../Auth/OAuth2/AuthorizationCode/inputsConfig.js | 4 ++++ .../src/ipc/network/interpolate-vars.js | 1 + .../bruno-electron/src/ipc/network/oauth2-helper.js | 11 ++++++++--- .../bruno-electron/src/ipc/network/prepare-request.js | 1 + packages/bruno-lang/v2/src/bruToJson.js | 2 ++ packages/bruno-lang/v2/src/collectionBruToJson.js | 2 ++ packages/bruno-lang/v2/src/jsonToBru.js | 1 + packages/bruno-lang/v2/src/jsonToCollectionBru.js | 1 + packages/bruno-lang/v2/tests/fixtures/request.bru | 1 + packages/bruno-lang/v2/tests/fixtures/request.json | 1 + packages/bruno-schema/src/collections/index.js | 5 +++++ 14 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js index 674db53a8..8ec71a69a 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js @@ -22,7 +22,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth; + const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; const handleChange = (key, value) => { dispatch( @@ -37,6 +37,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { clientId, clientSecret, scope, + state, pkce, [key]: value } @@ -57,6 +58,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { clientId, clientSecret, scope, + state, pkce: !Boolean(oAuth?.['pkce']) } }) diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js index f7cc7801a..67bc277aa 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js @@ -22,6 +22,10 @@ const inputsConfig = [ { key: 'scope', label: 'Scope' + }, + { + key: 'state', + label: 'State' } ]; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index 08a77555c..3c813b14b 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -22,7 +22,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth; + const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; const handleChange = (key, value) => { dispatch( @@ -37,6 +37,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { accessTokenUrl, clientId, clientSecret, + state, scope, pkce, [key]: value @@ -58,6 +59,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { accessTokenUrl, clientId, clientSecret, + state, scope, pkce: !Boolean(oAuth?.['pkce']) } diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js index f7cc7801a..67bc277aa 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js @@ -22,6 +22,10 @@ const inputsConfig = [ { key: 'scope', label: 'Scope' + }, + { + key: 'state', + label: 'State' } ]; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index b544ed7d1..f8c02c4b7 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -169,6 +169,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.state = _interpolate(request.oauth2.state) || ''; request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false; break; case 'client_credentials': diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index e254e8c74..a7921e85a 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -23,14 +23,15 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) let requestCopy = cloneDeep(request); const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth; + const { clientId, clientSecret, callbackUrl, scope, state, pkce } = oAuth; const data = { grant_type: 'authorization_code', code: authorizationCode, redirect_uri: callbackUrl, client_id: clientId, client_secret: clientSecret, - scope: scope + scope: scope, + state: state }; if (pkce) { data['code_verifier'] = codeVerifier; @@ -46,7 +47,7 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { return new Promise(async (resolve, reject) => { const { oauth2 } = request; - const { callbackUrl, clientId, authorizationUrl, scope, pkce } = oauth2; + const { callbackUrl, clientId, authorizationUrl, scope, state, pkce } = oauth2; let oauth2QueryParams = (authorizationUrl.indexOf('?') > -1 ? '&' : '?') + `client_id=${clientId}&response_type=code`; @@ -59,6 +60,10 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { if (pkce) { oauth2QueryParams += `&code_challenge=${codeChallenge}&code_challenge_method=S256`; } + if (state) { + oauth2QueryParams += `&state=${state}`; + } + const authorizationUrlWithQueryParams = authorizationUrl + oauth2QueryParams; try { const oauth2Store = new Oauth2Store(); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 94fe834b1..ed13eeb54 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -112,6 +112,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { clientId: get(request, 'auth.oauth2.clientId'), clientSecret: get(request, 'auth.oauth2.clientSecret'), scope: get(request, 'auth.oauth2.scope'), + state: get(request, 'auth.oauth2.state'), pkce: get(request, 'auth.oauth2.pkce') }; break; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e78b1b8ad..08e4332c5 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -444,6 +444,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { const clientIdKey = _.find(auth, { name: 'client_id' }); const clientSecretKey = _.find(auth, { name: 'client_secret' }); const scopeKey = _.find(auth, { name: 'scope' }); + const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); return { auth: { @@ -467,6 +468,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', scope: scopeKey ? scopeKey.value : '', + state: stateKey ? stateKey.value : '', pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 355f2f966..3c02a6225 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -254,6 +254,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { const clientIdKey = _.find(auth, { name: 'client_id' }); const clientSecretKey = _.find(auth, { name: 'client_secret' }); const scopeKey = _.find(auth, { name: 'scope' }); + const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); return { auth: { @@ -277,6 +278,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', scope: scopeKey ? scopeKey.value : '', + state: stateKey ? stateKey.value : '', pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 45d7bba0f..e93c4915c 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -180,6 +180,7 @@ ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index e4d6ab5fd..11df88da4 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -138,6 +138,7 @@ ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index fcfe7b818..e333df0ec 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -53,6 +53,7 @@ auth:oauth2 { client_id: client_id_1 client_secret: client_secret_1 scope: read write + state: 807061d5f0be pkce: false } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index afb7ca3f9..a69ff1813 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -72,6 +72,7 @@ "callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback", "accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token", "scope": "read write", + "state": "807061d5f0be", "pkce": false } }, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3dc1352c8..cc93ed671 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -163,6 +163,11 @@ const oauth2Schema = Yup.object({ then: Yup.string().nullable(), otherwise: Yup.string().nullable().strip() }), + state: Yup.string().when('grantType', { + is: (val) => ['authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), pkce: Yup.boolean().when('grantType', { is: (val) => ['authorization_code'].includes(val), then: Yup.boolean().default(false), From 46df2e967fa4cd55758f36396eb3b54ebf808a3e Mon Sep 17 00:00:00 2001 From: Dakshin K <36289388+dakshin-k@users.noreply.github.com> Date: Fri, 31 May 2024 15:41:31 +0530 Subject: [PATCH 012/904] fix: Check OAuth2 redirect URL for matching callback URL and authorization code in query parameters (#2148) * Check OAuth2 redirect URL for matching callback URL and authorization code in query parameters In an Authorization code flow, there may be multiple intermediate redirects before reaching the final one which matches the callback URL and has a code in the query params. We should wait until we see a redirect URI that matches both the conditions. This fixes the issue where, when a redirect contains `code` as a query param but is not the final one (i.e., is not to the callback URL) an error is thrown saying the callback URL is invalid. Fixes #2147 * Add test cases for callback URL check * Update check to cover URLs with same host but different endpoints --- .../ipc/network/authorize-user-in-window.js | 16 ++++++++++------ .../tests/network/authorize-user.spec.js | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 packages/bruno-electron/tests/network/authorize-user.spec.js diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js index 3ed05d45c..8e4abf4bb 100644 --- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -1,6 +1,10 @@ const { BrowserWindow } = require('electron'); const { preferencesUtil } = require('../../store/preferences'); +const matchesCallbackUrl = (url, callbackUrl) => { + return url ? url.href.startsWith(callbackUrl.href) : false; +}; + const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { return new Promise(async (resolve, reject) => { let finalUrl = null; @@ -30,12 +34,12 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { }); function onWindowRedirect(url) { - // check if the url contains an authorization code - if (new URL(url).searchParams.has('code')) { - finalUrl = url; - if (!url || !finalUrl.includes(callbackUrl)) { - reject(new Error('Invalid Callback Url')); + // check if the redirect is to the callback URL and if it contains an authorization code + if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) { + if (!new URL(url).searchParams.has('code')) { + reject(new Error('Invalid Callback URL: Does not contain an authorization code')); } + finalUrl = url; window.close(); } if (url.match(/(error=).*/) || url.match(/(error_description=).*/) || url.match(/(error_uri=).*/)) { @@ -93,4 +97,4 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { }); }; -module.exports = { authorizeUserInWindow }; +module.exports = { authorizeUserInWindow, matchesCallbackUrl }; diff --git a/packages/bruno-electron/tests/network/authorize-user.spec.js b/packages/bruno-electron/tests/network/authorize-user.spec.js new file mode 100644 index 000000000..03c76cfb8 --- /dev/null +++ b/packages/bruno-electron/tests/network/authorize-user.spec.js @@ -0,0 +1,19 @@ +const { matchesCallbackUrl } = require('../../src/ipc/network/authorize-user-in-window'); + +describe('matchesCallbackUrl', () => { + const testCases = [ + { url: 'https://random-url/endpoint', expected: false }, + { url: 'https://random-url/endpoint?code=abcd', expected: false }, + { url: 'https://callback.url/endpoint?code=abcd', expected: true }, + { url: 'https://callback.url/endpoint/?code=abcd', expected: true }, + { url: 'https://callback.url/random-endpoint/?code=abcd', expected: false } + ]; + + it.each(testCases)('$url - should be $expected', ({ url, expected }) => { + let callBackUrl = 'https://callback.url/endpoint'; + + let actual = matchesCallbackUrl(new URL(url), new URL(callBackUrl)); + + expect(actual).toBe(expected); + }); +}); From b63ae360fe9c0bbdf8ad0613371dbf0d18a5f1ca Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 31 May 2024 15:55:23 +0530 Subject: [PATCH 013/904] fix: params test (#2395) --- .../CollectionItem/GenerateCodeItem/index.js | 52 +------------- packages/bruno-app/src/utils/url/index.js | 52 ++++++++++++++ .../bruno-app/src/utils/url/index.spec.js | 72 ++++++++++++++++++- packages/bruno-toml/tests/index.spec.js | 45 ------------ 4 files changed, 125 insertions(+), 96 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index 59cdd6281..dc8d525da 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -5,55 +5,7 @@ import StyledWrapper from './StyledWrapper'; import { isValidUrl } from 'utils/url'; import { find, get } from 'lodash'; import { findEnvironmentInCollection } from 'utils/collections'; - -// Todo: Fix this -// import { interpolate } from '@usebruno/common'; -import brunoCommon from '@usebruno/common'; -const { interpolate } = brunoCommon; - -const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => { - if (!url || !url.length || typeof url !== 'string') { - return; - } - - return interpolate(url, { - ...envVars, - ...collectionVariables, - process: { - env: { - ...processEnvVars - } - } - }); -}; - -// interpolate URL paths -const interpolateUrlPathParams = (url, params) => { - const getInterpolatedBasePath = (pathname, params) => { - 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; - } - return segment; - }) - .join('/'); - }; - - let uri; - try { - uri = new URL(url); - } catch (error) { - uri = new URL(`http://${url}`); - } - - const basePath = getInterpolatedBasePath(uri.pathname, params); - - return `${uri.origin}${basePath}${uri?.search || ''}`; -}; +import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index'; const languages = [ { @@ -117,7 +69,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { const requestUrl = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'); - // interpolate the query params + // interpolate the url const interpolatedUrl = interpolateUrl({ url: requestUrl, envVars, diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index e1aea7316..0b9f2201a 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -4,6 +4,9 @@ import each from 'lodash/each'; import filter from 'lodash/filter'; import find from 'lodash/find'; +import brunoCommon from '@usebruno/common'; +const { interpolate } = brunoCommon; + const hasLength = (str) => { if (!str || !str.length) { return false; @@ -103,3 +106,52 @@ export const isValidUrl = (url) => { return false; } }; + +export const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => { + if (!url || !url.length || typeof url !== 'string') { + return; + } + + return interpolate(url, { + ...envVars, + ...collectionVariables, + process: { + env: { + ...processEnvVars + } + } + }); +}; + +export const interpolateUrlPathParams = (url, params) => { + const getInterpolatedBasePath = (pathname, params) => { + 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; + } + return segment; + }) + .join('/'); + }; + + let uri; + + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `http://${url}`; + } + + try { + uri = new URL(url); + } catch (error) { + // if the URL is invalid, return the URL as is + return url; + } + + const basePath = getInterpolatedBasePath(uri.pathname, params); + + return `${uri.origin}${basePath}${uri?.search || ''}`; +}; diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index 1f43affaf..2fd3f0815 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -1,4 +1,4 @@ -import { parseQueryParams, splitOnFirst, parsePathParams } from './index'; +import { parseQueryParams, splitOnFirst, parsePathParams, interpolateUrl, interpolateUrlPathParams } from './index'; describe('Url Utils - parseQueryParams', () => { it('should parse query - case 1', () => { @@ -122,3 +122,73 @@ describe('Url Utils - splitOnFirst', () => { expect(params).toEqual(['a=1', 'b=2']); }); }); + +describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => { + it('should interpolate url correctly', () => { + const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}'; + const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value'; + + const envVars = { host: 'https://example.com', foo: 'foo_value' }; + const collectionVariables = { bar: 'bar_value' }; + const processEnvVars = { baz: 'baz_value' }; + + const result = interpolateUrl({ url, envVars, collectionVariables, processEnvVars }); + + expect(result).toEqual(expectedUrl); + }); + + it('should interpolate path params correctly', () => { + const url = 'https://example.com/api/:id/path'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'https://example.com/api/123/path'; + + const result = interpolateUrlPathParams(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should interpolate url and path params correctly', () => { + const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value'; + + const envVars = { host: 'https://example.com', foo: 'foo_value' }; + const collectionVariables = { bar: 'bar_value' }; + const processEnvVars = { baz: 'baz_value' }; + + const intermediateResult = interpolateUrl({ url, envVars, collectionVariables, processEnvVars }); + const result = interpolateUrlPathParams(intermediateResult, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle empty params', () => { + const url = 'https://example.com/api'; + const params = []; + const expectedUrl = 'https://example.com/api'; + + const result = interpolateUrlPathParams(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle invalid URL, case 1', () => { + const url = 'example.com/api/:id'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'http://example.com/api/123'; + + const result = interpolateUrlPathParams(url, params); + + expect(result).toEqual(expectedUrl); + }); + + it('should handle invalid URL, case 2', () => { + const url = 'http://1.1.1.1:3000:id'; + const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; + const expectedUrl = 'http://1.1.1.1:3000:id'; + + const result = interpolateUrlPathParams(url, params); + + expect(result).toEqual(expectedUrl); + }); +}); diff --git a/packages/bruno-toml/tests/index.spec.js b/packages/bruno-toml/tests/index.spec.js index cad9d2e17..bcd00b77f 100644 --- a/packages/bruno-toml/tests/index.spec.js +++ b/packages/bruno-toml/tests/index.spec.js @@ -43,48 +43,3 @@ describe('bruno toml', () => { }); }); }); -describe('joinPathUrl', () => { - it('should join path and query params correctly', () => { - const url = 'https://example.com/api/:id'; - const params = [ - { name: 'id', type: 'path', enabled: true, value: '123' }, - { name: 'sort', type: 'query', enabled: true, value: 'asc' }, - { name: 'filter', type: 'query', enabled: true, value: 'active' } - ]; - const expectedUrl = 'https://example.com/api/123?sort=asc&filter=active'; - - const result = joinPathUrl(url, params); - - expect(result).toEqual(expectedUrl); - }); - - it('should handle empty path and query params', () => { - const url = 'https://example.com/api'; - const params = []; - const expectedUrl = 'https://example.com/api'; - - const result = joinPathUrl(url, params); - - expect(result).toEqual(expectedUrl); - }); - - it('should handle empty query params', () => { - const url = 'https://example.com/api/:id'; - const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; - const expectedUrl = 'https://example.com/api/123'; - - const result = joinPathUrl(url, params); - - expect(result).toEqual(expectedUrl); - }); - - it('should handle invalid URL', () => { - const url = 'example.com/api/:id'; - const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; - const expectedUrl = 'http://example.com/api/123'; - - const result = joinPathUrl(url, params); - - expect(result).toEqual(expectedUrl); - }); -}); From 2621c384c0f5a001411948b2a432307122d27c0c Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 31 May 2024 17:52:14 +0530 Subject: [PATCH 014/904] Fix/params tests (#2398) * fix: params test * undeclared variable old code --- .../Collection/CollectionItem/GenerateCodeItem/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index dc8d525da..a62469910 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -126,7 +126,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => { ) : (
-

Invalid URL: {url}

+

Invalid URL: {finalUrl}

Please check the URL and try again

From ee4dba54f3c3490f56380d80ac7d7d926d3b15cb Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:16:07 +0530 Subject: [PATCH 015/904] feat: update jsonToBru.js to use getValueString for text type items in multipartForm (#2407) --- packages/bruno-lang/v2/src/jsonToBru.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index e93c4915c..4b6e6b967 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -263,7 +263,7 @@ ${indentString(body.sparql)} const enabled = item.enabled ? '' : '~'; if (item.type === 'text') { - return `${enabled}${item.name}: ${item.value}`; + return `${enabled}${item.name}: ${getValueString(item.value)}`; } if (item.type === 'file') { From bcdbc0ebede4ecd60f5e0a750c7634e106400a6b Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 4 Jun 2024 14:17:36 +0530 Subject: [PATCH 016/904] fix tests for bruno-electron & bruno-lang (#2410) --- .../src/ipc/network/interpolate-vars.js | 2 +- packages/bruno-lang/v2/tests/fixtures/request.bru | 8 ++++++-- packages/bruno-lang/v2/tests/fixtures/request.json | 13 +++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index f8c02c4b7..40fe9faa6 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -86,7 +86,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces param.value = _interpolate(param.value); }); - if (request.params.length) { + if (request?.params?.length) { let url = request.url; if (!url.startsWith('http://') && !url.startsWith('https://')) { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index e333df0ec..c4ff61558 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -5,17 +5,21 @@ meta { } get { - url: https://api.textlocal.in/send + url: https://api.textlocal.in/send/:id body: json auth: bearer } -query { +params:query { apiKey: secret numbers: 998877665 ~message: hello } +params:path { + id: 123 +} + headers { content-type: application/json Authorization: Bearer 123 diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index a69ff1813..d0bd996f6 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -6,25 +6,34 @@ }, "http": { "method": "get", - "url": "https://api.textlocal.in/send", + "url": "https://api.textlocal.in/send/:id", "body": "json", "auth": "bearer" }, - "query": [ + "params": [ { "name": "apiKey", "value": "secret", + "type": "query", "enabled": true }, { "name": "numbers", "value": "998877665", + "type": "query", "enabled": true }, { "name": "message", "value": "hello", + "type": "query", "enabled": false + }, + { + "name": "id", + "value": "123", + "type": "path", + "enabled": true } ], "headers": [ From 811a6e6034295054ec4f160edfcacab177db7300 Mon Sep 17 00:00:00 2001 From: Johannes Zorn <46689904+jzorn@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:59:59 +0200 Subject: [PATCH 017/904] fix(#2122): add specific error status codes (#2126) * fix: add specific error status codes #2122 This change adds specific exit status codes to identify different problems programatically * feat(#2122): Document the cli exit status codes * Update packages/bruno-cli/src/constants.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/constants.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel * Update packages/bruno-cli/src/commands/run.js Co-authored-by: Andreas Siegel --------- Co-authored-by: Andreas Siegel --- packages/bruno-cli/readme.md | 17 ++++++++++++++++ packages/bruno-cli/src/commands/run.js | 21 ++++++++++---------- packages/bruno-cli/src/constants.js | 27 +++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md index db41a4d7c..1eedb839d 100644 --- a/packages/bruno-cli/readme.md +++ b/packages/bruno-cli/readme.md @@ -44,6 +44,22 @@ If you need to collect the results of your API tests, you can specify the --outp bru run folder --output results.json ``` +## Scripting + +Bruno cli returns the following exit status codes: + +- `0` -- execution successful +- `1` -- an assertion, test, or request in the executed collection failed +- `2` -- the specified output directory does not exist +- `3` -- the request chain seems to loop endlessly +- `4` -- bru was called outside of a colection root directory +- `5` -- the specified input file does not exist +- `6` -- the specified environment does not exist +- `7` -- the environment override was not a string or object +- `8` -- an environment override is malformed +- `9` -- an invalid output format was requested +- `255` -- another error occured + ## Demo ![demo](assets/images/cli-demo.png) @@ -57,6 +73,7 @@ Thank you for using Bruno CLI! ## Changelog + See [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases) ## License diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 1d95afc75..00f592bd2 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -10,6 +10,7 @@ const makeHtmlOutput = require('../reporters/html'); const { rpad } = require('../utils/common'); const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); const { dotenvToJson } = require('@usebruno/lang'); +const constants = require('../constants'); const command = 'run [filename]'; const desc = 'Run a request'; @@ -255,7 +256,7 @@ const handler = async function (argv) { const brunoJsonExists = await exists(brunoJsonPath); if (!brunoJsonExists) { console.error(chalk.red(`You can run only at the root of a collection`)); - return; + process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); } const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8'); @@ -266,7 +267,7 @@ const handler = async function (argv) { const pathExists = await exists(filename); if (!pathExists) { console.error(chalk.red(`File or directory ${filename} does not exist`)); - return; + process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND); } } else { filename = './'; @@ -282,7 +283,7 @@ const handler = async function (argv) { if (!envPathExists) { console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`)); - return; + process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND); } const envBruContent = fs.readFileSync(envFile, 'utf8'); @@ -299,7 +300,7 @@ const handler = async function (argv) { processVars = envVar; } else { console.error(chalk.red(`overridable environment variables not parsable: use name=value`)); - return; + process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE); } if (processVars && Array.isArray(processVars)) { for (const value of processVars.values()) { @@ -310,7 +311,7 @@ const handler = async function (argv) { chalk.red(`Overridable environment variable not correct: use name=value - presented: `) + chalk.dim(`${value}`) ); - return; + process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE); } envVars[match[1]] = match[2]; } @@ -339,7 +340,7 @@ const handler = async function (argv) { if (['json', 'junit', 'html'].indexOf(format) === -1) { console.error(chalk.red(`Format must be one of "json", "junit or "html"`)); - return; + process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT); } // load .env file at root of collection if it exists @@ -451,7 +452,7 @@ const handler = async function (argv) { nJumps++; if (nJumps > 10000) { console.error(chalk.red(`Too many jumps, possible infinite loop`)); - process.exit(1); + process.exit(constants.EXIT_STATUS.ERROR_INFINTE_LOOP); } if (nextRequestName === null) { break; @@ -477,7 +478,7 @@ const handler = async function (argv) { const outputDirExists = await exists(outputDir); if (!outputDirExists) { console.error(chalk.red(`Output directory ${outputDir} does not exist`)); - process.exit(1); + process.exit(constants.EXIT_STATUS.ERROR_MISSING_OUTPUT_DIR); } const outputJson = { @@ -497,12 +498,12 @@ const handler = async function (argv) { } if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) { - process.exit(1); + process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION); } } catch (err) { console.log('Something went wrong'); console.error(chalk.red(err.message)); - process.exit(1); + process.exit(constants.EXIT_STATUS.ERROR_GENERIC); } }; diff --git a/packages/bruno-cli/src/constants.js b/packages/bruno-cli/src/constants.js index a952f893b..6d0f877d7 100644 --- a/packages/bruno-cli/src/constants.js +++ b/packages/bruno-cli/src/constants.js @@ -3,7 +3,32 @@ const { version } = require('../package.json'); const CLI_EPILOGUE = `Documentation: https://docs.usebruno.com (v${version})`; const CLI_VERSION = version; +// Exit codes +const EXIT_STATUS = { + // One or more assertions, tests, or requests failed + ERROR_FAILED_COLLECTION: 1, + // The specified output dir does not exist + ERROR_MISSING_OUTPUT_DIR: 2, + // request chain caused an endless loop + ERROR_INFINTE_LOOP: 3, + // bru was called outside of a collection root + ERROR_NOT_IN_COLLECTION: 4, + // The specified file was not found + ERROR_FILE_NOT_FOUND: 5, + // The specified environment was not found + ERROR_ENV_NOT_FOUND: 6, + // Environment override not presented as string or object + ERROR_MALFORMED_ENV_OVERRIDE: 7, + // Environment overrides format incorrect + ERROR_INCORRECT_ENV_OVERRIDE: 8, + // Invalid output format requested + ERROR_INCORRECT_OUTPUT_FORMAT: 9, + // Everything else + ERROR_GENERIC: 255, +}; + module.exports = { CLI_EPILOGUE, - CLI_VERSION + CLI_VERSION, + EXIT_STATUS }; From f9b33dde3850de27775af32eb798bf24a96e3e0d Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 5 Jun 2024 20:31:31 +0530 Subject: [PATCH 018/904] chore: fix cli tests (#2412) * fix tests for bruno-electron & bruno-lang * chore: fix validatioon breaking cli tests --- packages/bruno-cli/src/runner/interpolate-vars.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 5dff0564f..886974c0f 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -66,7 +66,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces } if (typeof request.data === 'string') { - if (request.data.length) { + if (request?.data?.length) { request.data = _interpolate(request.data); } } @@ -86,7 +86,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces param.value = _interpolate(param.value); }); - if (request.params.length) { + if (request?.params?.length) { let url = request.url; if (!url.startsWith('http://') && !url.startsWith('https://')) { From 1b4d9b8f6edfc8f712c165c347f9e3819c133532 Mon Sep 17 00:00:00 2001 From: Johannes Zorn <46689904+jzorn@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:05:28 +0200 Subject: [PATCH 019/904] fix: Move output to stderr #2123 (#2125) This change moves informational command outputs from console.log (prints on stdout) to console.warn/console.error (prints on stderr) to enable stdout processing in pipelines. Co-authored-by: Anoop M D --- packages/bruno-cli/src/commands/run.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 00f592bd2..7cf5293fb 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -406,8 +406,7 @@ const handler = async function (argv) { return aSequence - bSequence; }); } else { - console.log(chalk.yellow('Running Folder Recursively \n')); - + console.warn(chalk.yellow('Running Folder Recursively \n')); bruJsons = getBruFilesRecursively(filename, testsOnly); } } @@ -461,7 +460,7 @@ const handler = async function (argv) { if (nextRequestIdx >= 0) { currentRequestIndex = nextRequestIdx; } else { - console.error("Could not find request with name '" + nextRequestName + "'"); + console.warn("Could not find request with name '" + nextRequestName + "'"); currentRequestIndex++; } } else { @@ -471,7 +470,7 @@ const handler = async function (argv) { const summary = printRunSummary(results); const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0); - console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`))); + console.error(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`))); if (outputPath && outputPath.length) { const outputDir = path.dirname(outputPath); @@ -494,15 +493,14 @@ const handler = async function (argv) { makeHtmlOutput(outputJson, outputPath); } - console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`))); + console.error(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`))); } if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) { process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION); } } catch (err) { - console.log('Something went wrong'); - console.error(chalk.red(err.message)); + console.error(chalk.red('Something went wrong: ' + err.message)); process.exit(constants.EXIT_STATUS.ERROR_GENERIC); } }; From 9b382fa0911536a0bfa27ae0d8a67007cc1364f7 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Wed, 5 Jun 2024 20:37:15 +0530 Subject: [PATCH 020/904] Revert "fix: Move output to stderr #2123 (#2125)" (#2418) This reverts commit 1b4d9b8f6edfc8f712c165c347f9e3819c133532. --- packages/bruno-cli/src/commands/run.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 7cf5293fb..00f592bd2 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -406,7 +406,8 @@ const handler = async function (argv) { return aSequence - bSequence; }); } else { - console.warn(chalk.yellow('Running Folder Recursively \n')); + console.log(chalk.yellow('Running Folder Recursively \n')); + bruJsons = getBruFilesRecursively(filename, testsOnly); } } @@ -460,7 +461,7 @@ const handler = async function (argv) { if (nextRequestIdx >= 0) { currentRequestIndex = nextRequestIdx; } else { - console.warn("Could not find request with name '" + nextRequestName + "'"); + console.error("Could not find request with name '" + nextRequestName + "'"); currentRequestIndex++; } } else { @@ -470,7 +471,7 @@ const handler = async function (argv) { const summary = printRunSummary(results); const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0); - console.error(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`))); + console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`))); if (outputPath && outputPath.length) { const outputDir = path.dirname(outputPath); @@ -493,14 +494,15 @@ const handler = async function (argv) { makeHtmlOutput(outputJson, outputPath); } - console.error(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`))); + console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`))); } if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) { process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION); } } catch (err) { - console.error(chalk.red('Something went wrong: ' + err.message)); + console.log('Something went wrong'); + console.error(chalk.red(err.message)); process.exit(constants.EXIT_STATUS.ERROR_GENERIC); } }; From 8628bdbe89ea23df11cfb322b4a45bf235f5feba Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Fri, 14 Jun 2024 13:36:39 +0200 Subject: [PATCH 021/904] Fix: update broken relative links between various document translations and embedded images (#2392) --- contributing.md | 19 +++++++++++-- docs/contributing/contributing_bn.md | 18 +++++++++++- docs/contributing/contributing_cn.md | 18 +++++++++++- docs/contributing/contributing_de.md | 18 +++++++++++- docs/contributing/contributing_es.md | 18 ++++++++++++ docs/contributing/contributing_fr.md | 18 +++++++++++- docs/contributing/contributing_hi.md | 19 +++++++++++-- docs/contributing/contributing_it.md | 18 ++++++++++++ docs/contributing/contributing_ja.md | 19 +++++++++++-- docs/contributing/contributing_kr.md | 19 +++++++++++-- docs/contributing/contributing_pl.md | 24 +++++++++++++--- docs/contributing/contributing_pt_br.md | 18 ++++++++++++ docs/contributing/contributing_ro.md | 18 +++++++++++- docs/contributing/contributing_ru.md | 18 +++++++++++- docs/contributing/contributing_tr.md | 18 +++++++++++- docs/contributing/contributing_ua.md | 18 +++++++++++- docs/contributing/contributing_zhtw.md | 18 +++++++++++- docs/publishing/publishing_bn.md | 12 +++++++- docs/publishing/publishing_cn.md | 12 +++++++- docs/publishing/publishing_de.md | 12 +++++++- docs/publishing/publishing_fr.md | 12 +++++++- docs/publishing/publishing_ja.md | 12 +++++++- docs/publishing/publishing_pl.md | 12 +++++++- docs/publishing/publishing_pt_br.md | 12 +++++++- docs/publishing/publishing_ro.md | 12 +++++++- docs/publishing/publishing_tr.md | 12 +++++++- docs/publishing/publishing_zhtw.md | 12 +++++++- docs/readme/readme_ar.md | 32 +++++++++++++++------ docs/readme/readme_bn.md | 22 ++++++++++++-- docs/readme/readme_cn.md | 22 ++++++++++++-- docs/readme/readme_de.md | 22 ++++++++++++-- docs/readme/readme_es.md | 25 ++++++++++++---- docs/readme/readme_fr.md | 20 +++++++++++-- docs/readme/readme_it.md | 24 ++++++++++++++-- docs/readme/readme_ja.md | 34 ++++++++++++++++------ docs/readme/readme_kr.md | 22 ++++++++++++-- docs/readme/readme_pl.md | 38 +++++++++++++++++++------ docs/readme/readme_pt_br.md | 20 ++++++++++++- docs/readme/readme_ro.md | 24 +++++++++++++--- docs/readme/readme_ru.md | 20 +++++++++++-- docs/readme/readme_tr.md | 24 +++++++++++++--- docs/readme/readme_ua.md | 20 +++++++++++-- docs/readme/readme_zhtw.md | 18 +++++++++++- publishing.md | 12 +++++++- readme.md | 18 +++++++++++- 45 files changed, 760 insertions(+), 93 deletions(-) diff --git a/contributing.md b/contributing.md index 81fc27a22..e16d55f87 100644 --- a/contributing.md +++ b/contributing.md @@ -1,5 +1,20 @@ -**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) -| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md) +**English** +| [Українська](docs/contributing/contributing_ua.md) +| [Русский](docs/contributing/contributing_ru.md) +| [Türkçe](docs/contributing/contributing_tr.md) +| [Deutsch](docs/contributing/contributing_de.md) +| [Français](docs/contributing/contributing_fr.md) +| [Português (BR)](docs/contributing/contributing_pt_br.md) +| [한국어](docs/contributing/contributing_kr.md) +| [বাংলা](docs/contributing/contributing_bn.md) +| [Español](docs/contributing/contributing_es.md) +| [Italiano](docs/contributing/contributing_it.md) +| [Română](docs/contributing/contributing_ro.md) +| [Polski](docs/contributing/contributing_pl.md) +| [简体中文](docs/contributing/contributing_cn.md) +| [正體中文](docs/contributing/contributing_zhtw.md) +| [日本語](docs/contributing/contributing_ja.md) +| [हिंदी](docs/contributing/contributing_hi.md) ## Let's make Bruno better, together !! diff --git a/docs/contributing/contributing_bn.md b/docs/contributing/contributing_bn.md index 88f67cfde..9348919fb 100644 --- a/docs/contributing/contributing_bn.md +++ b/docs/contributing/contributing_bn.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](/contributing_fr.md) | **বাংলা** | [हिंदी](./contributing_hi.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| **বাংলা** +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!! diff --git a/docs/contributing/contributing_cn.md b/docs/contributing/contributing_cn.md index f9a12f60a..4b1f1f662 100644 --- a/docs/contributing/contributing_cn.md +++ b/docs/contributing/contributing_cn.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](./contributing_ua.md) | [Русский](./contributing_ru.md) | [Türkçe](./contributing_tr.md) | [Deutsch](./contributing_de.md) | [Français](./contributing_fr.md) | [Português (BR)](./contributing_pt_br.md) | [বাংলা](./contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](./contributing_es.md) | [Română](./contributing_ro.md) | [Polski](./contributing_pl.md) | **简体中文** | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| **简体中文** +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## 让我们一起改进 Bruno! diff --git a/docs/contributing/contributing_de.md b/docs/contributing/contributing_de.md index b812ffc2e..723afcdda 100644 --- a/docs/contributing/contributing_de.md +++ b/docs/contributing/contributing_de.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | **Deutsch** | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| **Deutsch** +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Lass uns Bruno noch besser machen, gemeinsam !! diff --git a/docs/contributing/contributing_es.md b/docs/contributing/contributing_es.md index 3e69a2acc..c640458d6 100644 --- a/docs/contributing/contributing_es.md +++ b/docs/contributing/contributing_es.md @@ -1,3 +1,21 @@ +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| **Español** +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) + ## ¡Juntos, hagamos a Bruno mejor! Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encontrarás las instrucciones para empezar a trabajar con Bruno en tu computadora. diff --git a/docs/contributing/contributing_fr.md b/docs/contributing/contributing_fr.md index 0cdc6d93e..d3cf0e79b 100644 --- a/docs/contributing/contributing_fr.md +++ b/docs/contributing/contributing_fr.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | **Français** | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| **Français** +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Ensemble, améliorons Bruno ! diff --git a/docs/contributing/contributing_hi.md b/docs/contributing/contributing_hi.md index 2bf738833..3ae80bbb3 100644 --- a/docs/contributing/contributing_hi.md +++ b/docs/contributing/contributing_hi.md @@ -1,5 +1,20 @@ -[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | **हिंदी** | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) -| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| **हिंदी** ## आइए मिलकर Bruno को बेहतर बनाएं !! diff --git a/docs/contributing/contributing_it.md b/docs/contributing/contributing_it.md index 53bf61e74..3d2524c8d 100644 --- a/docs/contributing/contributing_it.md +++ b/docs/contributing/contributing_it.md @@ -1,3 +1,21 @@ +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| **Italiano** +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) + ## Insieme, miglioriamo Bruno! Sono felice di vedere che hai intenzione di migliorare Bruno. Di seguito, troverai le regole e le guide per ripristinare Bruno sul tuo computer. diff --git a/docs/contributing/contributing_ja.md b/docs/contributing/contributing_ja.md index c40e01681..d224a0848 100644 --- a/docs/contributing/contributing_ja.md +++ b/docs/contributing/contributing_ja.md @@ -1,5 +1,20 @@ -[English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) -| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | **日本語** +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| **日本語** +| [हिंदी](./contributing_hi.md) ## 一緒に Bruno をよりよいものにしていきましょう!! diff --git a/docs/contributing/contributing_kr.md b/docs/contributing/contributing_kr.md index d2df25dff..07dd213d6 100644 --- a/docs/contributing/contributing_kr.md +++ b/docs/contributing/contributing_kr.md @@ -1,5 +1,20 @@ -[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) -| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | **한국어** +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| **한국어** +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## 함께 Bruno를 더 좋게 만들어요!! diff --git a/docs/contributing/contributing_pl.md b/docs/contributing/contributing_pl.md index 0d67045c4..e60c8b7d6 100644 --- a/docs/contributing/contributing_pl.md +++ b/docs/contributing/contributing_pl.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | **Polski** | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| **Polski** +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Wspólnie uczynijmy Bruno lepszym !! @@ -33,7 +49,7 @@ Bruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, u ### Lokalny Rozwój -````bash +```bash # użyj wersji nodejs 18 nvm use @@ -51,7 +67,7 @@ npm run dev:web # uruchom aplikację electron (terminal 2) npm run dev:electron - +``` ### Rozwiązywanie Problemów @@ -66,7 +82,7 @@ done # Usuń package-lock w podkatalogach find . -type f -name "package-lock.json" -delete -```` +``` ### Testowanie diff --git a/docs/contributing/contributing_pt_br.md b/docs/contributing/contributing_pt_br.md index 7f7baabb3..e89426b13 100644 --- a/docs/contributing/contributing_pt_br.md +++ b/docs/contributing/contributing_pt_br.md @@ -1,3 +1,21 @@ +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| **Português (BR)** +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) + ## Vamos tornar o Bruno melhor, juntos!! Estamos felizes que você queira ajudar a melhorar o Bruno. Abaixo estão as diretrizes e orientações para começar a executar o Bruno no seu computador. diff --git a/docs/contributing/contributing_ro.md b/docs/contributing/contributing_ro.md index d702504b1..2449604e1 100644 --- a/docs/contributing/contributing_ro.md +++ b/docs/contributing/contributing_ro.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](/docs/contributing/contributing_ua.md) | [Русский](/docs/contributing/contributing_ru.md) | [Türkçe](/docs/contributing/contributing_tr.md) | [Deutsch](/docs/contributing/contributing_de.md) | [Français](/docs/contributing/contributing_fr.md) | [Português (BR)](/docs/contributing/contributing_pt_br.md) | [বাংলা](/docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](/docs/contributing/contributing_es.md) | [Italiano](/docs/contributing/contributing_it.md) | **Română** | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| **Română** +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Haideţi să îmbunătățim Bruno, împreună!! diff --git a/docs/contributing/contributing_ru.md b/docs/contributing/contributing_ru.md index 99950e089..8a9165395 100644 --- a/docs/contributing/contributing_ru.md +++ b/docs/contributing/contributing_ru.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| **Русский** +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Давайте вместе сделаем Бруно лучше!!! diff --git a/docs/contributing/contributing_tr.md b/docs/contributing/contributing_tr.md index 89a8d9520..e2a2279a4 100644 --- a/docs/contributing/contributing_tr.md +++ b/docs/contributing/contributing_tr.md @@ -1,4 +1,20 @@ -[English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | **Türkçe** | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| **Türkçe** +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Bruno'yu birlikte daha iyi hale getirelim!!! diff --git a/docs/contributing/contributing_ua.md b/docs/contributing/contributing_ua.md index 6ce335379..14e896cc4 100644 --- a/docs/contributing/contributing_ua.md +++ b/docs/contributing/contributing_ua.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [हिंदी](./contributing_hi.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) +[English](../../contributing.md) +| **Українська** +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| [正體中文](./contributing_zhtw.md) +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## Давайте зробимо Bruno краще, разом !! diff --git a/docs/contributing/contributing_zhtw.md b/docs/contributing/contributing_zhtw.md index 18f9e3e0e..8ba54a50d 100644 --- a/docs/contributing/contributing_zhtw.md +++ b/docs/contributing/contributing_zhtw.md @@ -1,4 +1,20 @@ -[English](/contributing.md) | [Українська](./contributing_ua.md) | [Русский](./contributing_ru.md) | [Türkçe](./contributing_tr.md) | [Deutsch](./contributing_de.md) | [Français](./contributing_fr.md) | [Português (BR)](./contributing_pt_br.md) | [বাংলা](./contributing_bn.md) | [हिंदी](./contributing_hi.md) | [Español](./contributing_es.md) | [Română](./contributing_ro.md) | [Polski](./contributing_pl.md) | [简体中文](./contributing_cn.md) | **正體中文** +[English](../../contributing.md) +| [Українська](./contributing_ua.md) +| [Русский](./contributing_ru.md) +| [Türkçe](./contributing_tr.md) +| [Deutsch](./contributing_de.md) +| [Français](./contributing_fr.md) +| [Português (BR)](./contributing_pt_br.md) +| [한국어](./contributing_kr.md) +| [বাংলা](./contributing_bn.md) +| [Español](./contributing_es.md) +| [Italiano](./contributing_it.md) +| [Română](./contributing_ro.md) +| [Polski](./contributing_pl.md) +| [简体中文](./contributing_cn.md) +| **正體中文** +| [日本語](./contributing_ja.md) +| [हिंदी](./contributing_hi.md) ## 讓我們一起來讓 Bruno 變得更好! diff --git a/docs/publishing/publishing_bn.md b/docs/publishing/publishing_bn.md index 6999cba91..44cc55aef 100644 --- a/docs/publishing/publishing_bn.md +++ b/docs/publishing/publishing_bn.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | **বাংলা** | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| **বাংলা** +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা diff --git a/docs/publishing/publishing_cn.md b/docs/publishing/publishing_cn.md index 3f5059ad7..04bd23c81 100644 --- a/docs/publishing/publishing_cn.md +++ b/docs/publishing/publishing_cn.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | **简体中文** | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| **简体中文** +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### 将 Bruno 发布到新的包管理器 diff --git a/docs/publishing/publishing_de.md b/docs/publishing/publishing_de.md index c5c634221..01f866c06 100644 --- a/docs/publishing/publishing_de.md +++ b/docs/publishing/publishing_de.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | **Deutsch** +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| **Deutsch** +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Veröffentlichung von Bruno über neue Paket-Manager diff --git a/docs/publishing/publishing_fr.md b/docs/publishing/publishing_fr.md index fef4a7acb..fd88a8dc8 100644 --- a/docs/publishing/publishing_fr.md +++ b/docs/publishing/publishing_fr.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | **Français** | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| **Français** +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Publier Bruno dans un nouveau gestionnaire de paquets diff --git a/docs/publishing/publishing_ja.md b/docs/publishing/publishing_ja.md index f5d60b00e..adb3f120d 100644 --- a/docs/publishing/publishing_ja.md +++ b/docs/publishing/publishing_ja.md @@ -1,4 +1,14 @@ -[English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [正體中文](docs/publishing/publishing_zhtw.md) | **日本語** +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| **日本語** ### Bruno を新しいパッケージマネージャに公開する場合の注意 diff --git a/docs/publishing/publishing_pl.md b/docs/publishing/publishing_pl.md index e87ca69c7..37127c237 100644 --- a/docs/publishing/publishing_pl.md +++ b/docs/publishing/publishing_pl.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | **Polski** | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| **Polski** +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Publikowanie Bruno w nowym menedżerze pakietów diff --git a/docs/publishing/publishing_pt_br.md b/docs/publishing/publishing_pt_br.md index e1918c6e0..3cb43cf53 100644 --- a/docs/publishing/publishing_pt_br.md +++ b/docs/publishing/publishing_pt_br.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | **Português (BR)** | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| **Português (BR)** +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Publicando Bruno em um novo gerenciador de pacotes diff --git a/docs/publishing/publishing_ro.md b/docs/publishing/publishing_ro.md index 5ebbcd5d4..44fcca7cc 100644 --- a/docs/publishing/publishing_ro.md +++ b/docs/publishing/publishing_ro.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](/docs/publishing/publishing_pt_br.md) | **Română** | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](/docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| **Română** +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Publicarea lui Bruno la un gestionar de pachete nou diff --git a/docs/publishing/publishing_tr.md b/docs/publishing/publishing_tr.md index a145ec60e..5dfb87637 100644 --- a/docs/publishing/publishing_tr.md +++ b/docs/publishing/publishing_tr.md @@ -1,4 +1,14 @@ -[English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | **Türkçe** | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| **Türkçe** +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| [正體中文](./publishing_zhtw.md) +| [日本語](./publishing_ja.md) ### Bruno'yu yeni bir paket yöneticisine yayınlama diff --git a/docs/publishing/publishing_zhtw.md b/docs/publishing/publishing_zhtw.md index 7f8718378..87548c21e 100644 --- a/docs/publishing/publishing_zhtw.md +++ b/docs/publishing/publishing_zhtw.md @@ -1,4 +1,14 @@ -[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | **正體中文** | [简体中文](docs/publishing/publishing_cn.md) | [Deutsch](docs/publishing/publishing_de.md) +[English](../../publishing.md) +| [Türkçe](./publishing_tr.md) +| [Deutsch](./publishing_de.md) +| [Français](./publishing_fr.md) +| [Português (BR)](./publishing_pt_br.md) +| [বাংলা](./publishing_bn.md) +| [Română](./publishing_ro.md) +| [Polski](./publishing_pl.md) +| [简体中文](./publishing_cn.md) +| **正體中文** +| [日本語](./publishing_ja.md) ### 將 Bruno 發佈到新的套件管理器 diff --git a/docs/readme/readme_ar.md b/docs/readme/readme_ar.md index 614d60a14..7a64e9812 100644 --- a/docs/readme/readme_ar.md +++ b/docs/readme/readme_ar.md @@ -1,5 +1,5 @@
- + ### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs). @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -**English** | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) | [العربية](docs/readme/readme_ar.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| **العربية** +| [日本語](./readme_ja.md) برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك. @@ -22,7 +38,7 @@ 📢 شاهد حديثنا الأخير في مؤتمر India FOSS 3.0 [هنا](https://www.youtube.com/watch?v=7bSMFpbcPiY) -![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/landing-2.png)

+![bruno](/assets/images/landing-2.png)

### الطبعة الذهبية ✨ @@ -67,13 +83,13 @@ sudo apt ### التشغيل عبر منصات متعددة 🖥️ -![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/run-anywhere.png)

+![bruno](/assets/images/run-anywhere.png)

### التعاون عبر Git 👩‍💻🧑‍💻 أو أي نظام تحكم في الإصدار الذي تفضله -![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/version-control.png)

+![bruno](/assets/images/version-control.png)

### الروابط المهمة 📌 @@ -102,7 +118,7 @@ sudo apt ### نشر إلى مديري الحزم الجديدة -يرجى الرجوع [هنا](publishing.md) لمزيد من المعلومات. +يرجى الرجوع [هنا](../../publishing.md) لمزيد من المعلومات. ### تواصل معنا 🌐 @@ -123,7 +139,7 @@ sudo apt ### المساهمة 👩‍💻🧑‍💻 -يسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](contributing.md) +يسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](../../contributing.md) حتى إذا لم تكن قادرًا على التساهم بشكل مباشر من خلال الشيفرة، فلا تتردد في الإبلاغ عن الأخطاء وطلب الميزات التي يجب تنفيذها لحل حالتك. @@ -137,4 +153,4 @@ sudo apt ### الرخصة 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_bn.md b/docs/readme/readme_bn.md index d353201f0..6dc528caa 100644 --- a/docs/readme/readme_bn.md +++ b/docs/readme/readme_bn.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | **বাংলা** | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| **বাংলা** +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো। @@ -89,7 +105,7 @@ sudo apt install bruno ### অবদান 👩‍💻🧑‍💻 -আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](contributing.md) দেখুন +আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](../contributing/contributing_bn.md) দেখুন আপনি কোডের মাধ্যমে অবদান রাখতে না পারলেও, অনুগ্রহ করে বাগ এবং বৈশিষ্ট্যের অনুরোধ ফাইল করতে দ্বিধা করবেন না যা আপনার ব্যবহারের ক্ষেত্রে সমাধান করার জন্য প্রয়োগ করা প্রয়োজন। @@ -120,4 +136,4 @@ sudo apt install bruno ### লাইসেন্স 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_cn.md b/docs/readme/readme_cn.md index 196ee2ff9..9bd8306e8 100644 --- a/docs/readme/readme_cn.md +++ b/docs/readme/readme_cn.md @@ -10,7 +10,23 @@ [![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | **简体中文** | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| **简体中文** +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。 @@ -91,11 +107,11 @@ sudo apt install bruno ### 发布到新的包管理器 -有关更多信息,请参见 [此处](../../publishing_cn.md) 。 +有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。 ### 贡献 👩‍💻🧑‍💻 -我很高兴您希望改进 bruno。请查看 [贡献指南](../../contributing_cn.md)。 +我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。 即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。 diff --git a/docs/readme/readme_de.md b/docs/readme/readme_de.md index ac1a75fe6..96c22768a 100644 --- a/docs/readme/readme_de.md +++ b/docs/readme/readme_de.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | **Deutsch** | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| **Deutsch** +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll. @@ -114,7 +130,7 @@ Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte ni ### Bereitstellung in neuen Paket-Managern -Mehr Informationen findest du [hier](/publishing.md). +Mehr Informationen findest du [hier](../publishing/publishing_de.md). ### Mitmachen 👩‍💻🧑‍💻 @@ -149,4 +165,4 @@ Das Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizen ### Lizenz 📄 -[MIT](/license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_es.md b/docs/readme/readme_es.md index b1efdafa3..6775d9648 100644 --- a/docs/readme/readme_es.md +++ b/docs/readme/readme_es.md @@ -10,8 +10,23 @@ [![Sitio Web](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Descargas](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | **Español** | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](./readme_zhtw.md) - +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| **Español** +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares. Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs. @@ -101,11 +116,11 @@ Si Bruno te ha ayudado en tu trabajo y con tus equipos, por favor, no olvides co ### Publicar en nuevos gestores de paquetes -Por favor, consulta [aquí](publishing.md) para más información. +Por favor, consulta [aquí](../../publishing.md) para más información. ### Contribuye 👩‍💻🧑‍💻 -Estamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](contributing_es.md) para más información. +Estamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](../contributing/contributing_es.md) para más información. Incluso si no puedes contribuir con código, no dudes en reportar errores y solicitar nuevas funcionalidades que necesites para resolver tu caso de uso. @@ -136,4 +151,4 @@ El logo fue obtenido de [OpenMoji](https://openmoji.org/library/emoji-1F436/). L ### Licencia 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_fr.md b/docs/readme/readme_fr.md index 54a735c76..60eae2833 100644 --- a/docs/readme/readme_fr.md +++ b/docs/readme/readme_fr.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | **Français** | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| **Français** +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils. @@ -126,4 +142,4 @@ Licence : CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### Licence 📄 -[MIT](/license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_it.md b/docs/readme/readme_it.md index ff29804aa..5c21b7945 100644 --- a/docs/readme/readme_it.md +++ b/docs/readme/readme_it.md @@ -10,6 +10,24 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| **Italiano** +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) + Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili. Bruno memorizza le tue raccolte direttamente in una cartella del tuo filesystem. Utilizziamo un linguaggio di markup in testo semplice chiamato Bru per salvare informazioni sulle richeste API. @@ -83,11 +101,11 @@ Se Bruno ti ha aiutato con il tuo lavoro ed il tuo team, per favore non dimentic ### Pubblica Bruno su un nuovo gestore di pacchetti -Per favore vedi [qui](publishing.md) per accedere a più informazioni. +Per favore vedi [qui](../../publishing.md) per accedere a più informazioni. ### Contribuire 👩‍💻🧑‍💻 -Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](contributing.md) +Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](../contributing/contributing_it.md) Anche se non sei in grado di contribuire tramite il codice, non esitare a segnalare bug e richieste di funzionalità che devono essere implementati per risolvere il tuo caso d'uso. @@ -118,4 +136,4 @@ Il logo è stato creato da [OpenMoji](https://openmoji.org/library/emoji-1F436/) ### Licenza 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_ja.md b/docs/readme/readme_ja.md index bd8571969..f81e8713d 100644 --- a/docs/readme/readme_ja.md +++ b/docs/readme/readme_ja.md @@ -1,5 +1,5 @@
- + ### Bruno - API の検証・動作テストのためのオープンソース IDE. @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) | [العربية](docs/readme/readme_ar.md) | **日本語** +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| **日本語** Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。 @@ -24,7 +40,7 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同 📢 India FOSS 3.0 Conference での発表の様子は[こちら](https://www.youtube.com/watch?v=7bSMFpbcPiY)から -![bruno](assets/images/landing-2.png)

+![bruno](/assets/images/landing-2.png)

### ゴールデンエディション ✨ @@ -71,28 +87,28 @@ sudo apt install bruno ### マルチプラットフォームでの実行に対応 🖥️ -![bruno](assets/images/run-anywhere.png)

+![bruno](/assets/images/run-anywhere.png)

### Git との連携が可能 👩‍💻🧑‍💻 または任意のバージョン管理システムにも対応しています。 -![bruno](assets/images/version-control.png)

+![bruno](/assets/images/version-control.png)

### スポンサー #### ゴールドスポンサー - + #### シルバースポンサー - + #### ブロンズスポンサー - + ### 主要リンク 📌 @@ -157,4 +173,4 @@ Bruno を改善していただけるのは歓迎です。[コントリビュー ### ライセンス 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_kr.md b/docs/readme/readme_kr.md index 451e11ec0..4cc812d02 100644 --- a/docs/readme/readme_kr.md +++ b/docs/readme/readme_kr.md @@ -10,6 +10,24 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| **한국어** +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) + Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다. Bruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다. @@ -87,7 +105,7 @@ Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 ### 컨트리뷰트 👩‍💻🧑‍💻 -컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](/docs/contributing/contributing_kr.md) +컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](../contributing/contributing_kr.md) 코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요. @@ -118,4 +136,4 @@ The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). ### License 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_pl.md b/docs/readme/readme_pl.md index e8462f655..39866a4c4 100644 --- a/docs/readme/readme_pl.md +++ b/docs/readme/readme_pl.md @@ -1,7 +1,7 @@
-### Bruno - Otwartoźródłowe IDE do exploracji i testów APIs. +### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) @@ -10,9 +10,25 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md)) | [한국어](docs/readme/readme_kr.md) ) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | **Polski** | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| **Polski** +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) -Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowy przez Postman i podobne narzędzia. +Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman. Bruno przechowuje twoje kolekcje bezpośrednio w folderze na twoim systemie plików. Używamy prostego języka znaczników, Bru, do zapisywania informacji o żądaniach API. @@ -41,14 +57,20 @@ choco install bruno scoop bucket add extras scoop install bruno +# On Windows via winget +winget install Bruno.Bruno + # On Linux via Snap snap install bruno +# On Linux via Flatpak +flatpak install com.usebruno.Bruno + # On Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266 -echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list +echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update sudo apt install bruno @@ -87,15 +109,15 @@ Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ ### Udostępnij Opinie 📣 -Jeśli Bruno pomógł Tobie w pracy i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343) +Jeśli Bruno pomógł w pracy Tobie i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343) ### Publikowanie w Nowych Menedżerach Pakietów -Więcej informacji znajdziesz [tutaj](publishing.md). +Więcej informacji znajdziesz [tutaj](../publishing/publishing_pl.md). ### Współpraca 👩‍💻🧑‍💻 -Cieszę się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](contributing.md) +Cieszymy się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](../contributing/contributing_pl.md) Nawet jeśli nie jesteś w stanie przyczynić się poprzez kod, nie wahaj się zgłaszać błędów i wniosków o funkcje, które muszą zostać zaimplementowane, aby rozwiązać Twój przypadek użycia. @@ -126,4 +148,4 @@ Logo pochodzi z [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencja: ### Licencja 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_pt_br.md b/docs/readme/readme_pt_br.md index 84bf2b2b8..300ec7ff0 100644 --- a/docs/readme/readme_pt_br.md +++ b/docs/readme/readme_pt_br.md @@ -10,6 +10,24 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| **Português (BR)** +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) + Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes. Bruno armazena suas coleções diretamente em uma pasta no seu sistema de arquivos. Utilizamos uma linguagem de marcação de texto simples, chamada Bru, para salvar informações sobre requisições de API. @@ -147,4 +165,4 @@ Mesmo que você não possa contribuir codificando, não deixe de relatar problem ### Licença 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_ro.md b/docs/readme/readme_ro.md index de160f70d..7733ec0ff 100644 --- a/docs/readme/readme_ro.md +++ b/docs/readme/readme_ro.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](/docs/readme/readme_ua.md) | [Русский](/docs/readme/readme_ru.md) | [Türkçe](/docs/readme/readme_tr.md) | [Deutsch](/docs/readme/readme_de.md) | [Français](/docs/readme/readme_fr.md) | [Português (BR)](/docs/readme/readme_pt_br.md)) | [한국어](/docs/readme/readme_kr.md) | [বাংলা](/docs/readme/readme_bn.md) | [Español](/docs/readme/readme_es.md) | [Italiano](/docs/readme/readme_it.md) | **Română** | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| **Română** +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare. @@ -87,11 +103,11 @@ Dacă Bruno va ajutat la locul de muncă și la echipele dvs., vă rugăm să nu ### Publicarea la gestionari de pachete noi -Vă rugăm să citiţi [aici](/docs/publishing/publishing_ro.md) pentru mai multă informaţie. +Vă rugăm să citiţi [aici](../publishing/publishing_ro.md) pentru mai multă informaţie. ### Contribuiți 👩‍💻🧑‍💻 -Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](/docs/contributing/contributing_ro.md) +Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](../contributing/contributing_ro.md) Chiar dacă nu puteți face contribuții prin cod, vă rugăm să nu ezitați să raportați erori și să solicitați funcții care trebuie implementate pentru a rezolva cazul dvs. de utilizare. @@ -122,4 +138,4 @@ Logo-ul provine de la [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lic ### Licența 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_ru.md b/docs/readme/readme_ru.md index 26dfbb901..23ae24309 100644 --- a/docs/readme/readme_ru.md +++ b/docs/readme/readme_ru.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | [Українська](/readme_ua.md) | **Русский** | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| **Русский** +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами. @@ -76,4 +92,4 @@ Bruno работает только в автономном режиме. Доб ### Лицензия 📄 -[MIT](/license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_tr.md b/docs/readme/readme_tr.md index 7c4e2747e..8d1341e29 100644 --- a/docs/readme/readme_tr.md +++ b/docs/readme/readme_tr.md @@ -10,7 +10,23 @@ [![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | **Türkçe** | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| **Türkçe** +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir. @@ -91,11 +107,11 @@ Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartı ### Yeni Paket Yöneticilerine Yayınlama -Daha fazla bilgi için lütfen [buraya](publishing.md) bakın. +Daha fazla bilgi için lütfen [buraya](../publishing/publishing_tr.md) bakın. ### Katkıda Bulunun 👩‍💻🧑‍💻 -Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](contributing.md) göz atın +Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](../contributing/contributing_tr.md) göz atın Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin. @@ -126,4 +142,4 @@ Logo [OpenMoji](https://openmoji.org/library/emoji-1F436/) adresinden alınmış ### Lisans 📄 -[MIT](license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_ua.md b/docs/readme/readme_ua.md index 378a8e670..16042b6cb 100644 --- a/docs/readme/readme_ua.md +++ b/docs/readme/readme_ua.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](/readme.md) | **Українська** | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) +[English](../../readme.md) +| **Українська** +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman. @@ -77,4 +93,4 @@ Bruno є повністю автономним. Немає жодних план ### Ліцензія 📄 -[MIT](/license.md) +[MIT](../../license.md) diff --git a/docs/readme/readme_zhtw.md b/docs/readme/readme_zhtw.md index 222c75b97..aef3da24d 100644 --- a/docs/readme/readme_zhtw.md +++ b/docs/readme/readme_zhtw.md @@ -10,7 +10,23 @@ [![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -[English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | **正體中文** +[English](../../readme.md) +| [Українська](./readme_ua.md) +| [Русский](./readme_ru.md) +| [Türkçe](./readme_tr.md) +| [Deutsch](./readme_de.md) +| [Français](./readme_fr.md) +| [Português (BR)](./readme_pt_br.md) +| [한국어](./readme_kr.md) +| [বাংলা](./readme_bn.md) +| [Español](./readme_es.md) +| [Italiano](./readme_it.md) +| [Română](./readme_ro.md) +| [Polski](./readme_pl.md) +| [简体中文](./readme_cn.md) +| **正體中文** +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。 diff --git a/publishing.md b/publishing.md index c95d48f92..632fe18cc 100644 --- a/publishing.md +++ b/publishing.md @@ -1,4 +1,14 @@ -**English** | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [正體中文](docs/publishing/publishing_zhtw.md) |[ 简体中文](docs/publishing/publishing_cn.md) |[日本語](docs/publishing/publishing_ja.md) | [Deutsch](docs/publishing/publishing_de.md) +**English** +| [Türkçe](docs/publishing/publishing_tr.md) +| [Deutsch](docs/publishing/publishing_de.md) +| [Français](docs/publishing/publishing_fr.md) +| [Português (BR)](docs/publishing/publishing_pt_br.md) +| [বাংলা](docs/publishing/publishing_bn.md) +| [Română](docs/publishing/publishing_ro.md) +| [Polski](docs/publishing/publishing_pl.md) +| [简体中文](docs/publishing/publishing_cn.md) +| [正體中文](docs/publishing/publishing_zhtw.md) +| [日本語](docs/publishing/publishing_ja.md) ### Publishing Bruno to a new package manager diff --git a/readme.md b/readme.md index aae5d2f17..46af199d3 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,23 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) -**English** | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) | [العربية](docs/readme/readme_ar.md) | [日本語](docs/readme/readme_ja.md) +**English** +| [Українська](docs/readme/readme_ua.md) +| [Русский](docs/readme/readme_ru.md) +| [Türkçe](docs/readme/readme_tr.md) +| [Deutsch](docs/readme/readme_de.md) +| [Français](docs/readme/readme_fr.md) +| [Português (BR)](docs/readme/readme_pt_br.md) +| [한국어](docs/readme/readme_kr.md) +| [বাংলা](docs/readme/readme_bn.md) +| [Español](docs/readme/readme_es.md) +| [Italiano](docs/readme/readme_it.md) +| [Română](docs/readme/readme_ro.md) +| [Polski](docs/readme/readme_pl.md) +| [简体中文](docs/readme/readme_cn.md) +| [正體中文](docs/readme/readme_zhtw.md) +| [العربية](docs/readme/readme_ar.md) +| [日本語](docs/readme/readme_ja.md) Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there. From 693f882f5aecd672db4564c22d6d2409d9346677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bideau?= <7304827+rbideau@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:38:57 +0200 Subject: [PATCH 022/904] catch post request variable evaluation errors (#2324) fix #2005 - display post request variable evaluation errors in a toast, each individual variable error on a new line - display the response body (was previously replaced by the an error "Error invoking remote method 'send-http-request': ..." --- .../bruno-electron/src/ipc/network/index.js | 4 ++++ packages/bruno-js/src/runtime/vars-runtime.js | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 5d2e00230..0a5fee775 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -401,6 +401,10 @@ const registerNetworkIpc = (mainWindow) => { collectionUid }); } + + if (result?.error) { + mainWindow.webContents.send('main:display-error', result.error); + } } // run post-response script diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js index 0185ebddc..770822213 100644 --- a/packages/bruno-js/src/runtime/vars-runtime.js +++ b/packages/bruno-js/src/runtime/vars-runtime.js @@ -56,14 +56,27 @@ class VarsRuntime { ...bruContext }; + const errors = new Map(); _.each(enabledVars, (v) => { - const value = evaluateJsExpression(v.value, context); - bru.setVar(v.name, value); + try { + const value = evaluateJsExpression(v.value, context); + bru.setVar(v.name, value); + } catch (error) { + errors.set(v.name, error); + } }); + let error = null; + if (errors.size > 0) { + // Format all errors as a single string to be displayed in a toast + const errorMessage = [...errors.entries()].map(([name, err]) => `${name}: ${err.message ?? err}`).join('\n'); + error = `${errors.size} error${errors.size === 1 ? '' : 's'} in post response variables: \n${errorMessage}`; + } + return { envVariables, - collectionVariables + collectionVariables, + error }; } } From 5c04f884e29d80e246361b8156c4a2f91a5832b0 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:24:25 +0530 Subject: [PATCH 023/904] refactor: Handle errors when importing collections (#2455) --- .../src/components/Sidebar/TitleBar/index.js | 15 +++++++++++---- .../bruno-app/src/components/Welcome/index.js | 15 +++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js index 7962da07f..bec0c92a5 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js @@ -31,10 +31,17 @@ const TitleBar = () => { }; const handleImportCollectionLocation = (collectionLocation) => { - dispatch(importCollection(importedCollection, collectionLocation)); - setImportCollectionLocationModalOpen(false); - setImportedCollection(null); - toast.success('Collection imported successfully'); + dispatch(importCollection(importedCollection, collectionLocation)) + .then(() => { + setImportCollectionLocationModalOpen(false); + setImportedCollection(null); + toast.success('Collection imported successfully'); + }) + .catch((err) => { + setImportCollectionLocationModalOpen(false); + console.error(err); + toast.error('An error occurred while importing the collection. Check the logs for more information.'); + }); }; const menuDropdownTippyRef = useRef(); diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js index 7f60377b6..385a71486 100644 --- a/packages/bruno-app/src/components/Welcome/index.js +++ b/packages/bruno-app/src/components/Welcome/index.js @@ -34,10 +34,17 @@ const Welcome = () => { }; const handleImportCollectionLocation = (collectionLocation) => { - dispatch(importCollection(importedCollection, collectionLocation)); - setImportCollectionLocationModalOpen(false); - setImportedCollection(null); - toast.success('Collection imported successfully'); + dispatch(importCollection(importedCollection, collectionLocation)) + .then(() => { + setImportCollectionLocationModalOpen(false); + setImportedCollection(null); + toast.success('Collection imported successfully'); + }) + .catch((err) => { + setImportCollectionLocationModalOpen(false); + console.error(err); + toast.error('An error occurred while importing the collection. Check the logs for more information.'); + }); }; return ( From b01f2e0c5fe7cf6c0d4b3d111033f71f9d8be56b Mon Sep 17 00:00:00 2001 From: slowjoe007 Date: Mon, 17 Jun 2024 12:58:39 +0200 Subject: [PATCH 024/904] feature: Augment default truststore by default, optionally limit to custom CA certs (#2057) * feat: Allow default truststore extension on bru CLI * feat: augment default truststore by default, optionally limit to custom CA certs * feat: augment default truststore by default, optionally limit to custom CA certs * feat: Allow default truststore extension on bru CLI * feat: augment default truststore by default, optionally limit to custom CA certs --- .../components/Preferences/General/index.js | 2 +- .../src/providers/ReduxStore/slices/app.js | 2 +- packages/bruno-cli/readme.md | 16 ++++++++-- packages/bruno-cli/src/commands/run.js | 32 +++++++++++++++++-- .../src/runner/run-single-request.js | 7 +++- .../bruno-electron/src/store/preferences.js | 4 +-- 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index addb72a4d..9855c2747 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -47,7 +47,7 @@ const General = ({ close }) => { filePath: get(preferences, 'request.customCaCertificate.filePath', null) }, keepDefaultCaCertificates: { - enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', false) + enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', true) }, timeout: preferences.request.timeout, storeCookies: get(preferences, 'request.storeCookies', true), diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index beb3d1fcd..3cd276880 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -18,7 +18,7 @@ const initialState = { filePath: null }, keepDefaultCaCertificates: { - enabled: false + enabled: true }, timeout: 0 }, diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md index 1eedb839d..5cc98f3c0 100644 --- a/packages/bruno-cli/readme.md +++ b/packages/bruno-cli/readme.md @@ -32,18 +32,30 @@ Or run all requests in a collection's subfolder: bru run folder ``` -If you need to use an environment, you can specify it with the --env option: +If you need to use an environment, you can specify it with the `--env` option: ```bash bru run folder --env Local ``` -If you need to collect the results of your API tests, you can specify the --output option: +If you need to collect the results of your API tests, you can specify the `--output` option: ```bash bru run folder --output results.json ``` +If you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore: + +```bash +bru run folder --cacert myCustomCA.pem +``` + +If you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore: + +```bash +bru run request.bru --cacert myCustomCA.pem --ignore-truststore +``` + ## Scripting Bruno cli returns the following exit status codes: diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 00f592bd2..35192b128 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -190,6 +190,12 @@ const builder = async (yargs) => { type: 'string', description: 'CA certificate to verify peer against' }) + .option('ignore-truststore', { + type: 'boolean', + default: false, + description: + 'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with "--cacert" only.' + }) .option('env', { describe: 'Environment variables', type: 'string' @@ -241,12 +247,33 @@ const builder = async (yargs) => { '$0 run request.bru --output results.html --format html', 'Run a request and write the results to results.html in html format in the current directory' ) - .example('$0 run request.bru --tests-only', 'Run all requests that have a test'); + + .example('$0 run request.bru --tests-only', 'Run all requests that have a test') + .example( + '$0 run request.bru --cacert myCustomCA.pem', + 'Use a custom CA certificate in combination with the default truststore when validating the peer of this request.' + ) + .example( + '$0 run folder --cacert myCustomCA.pem --ignore-truststore', + 'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.' + ); }; const handler = async function (argv) { try { - let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath, format, testsOnly, bail } = argv; + let { + filename, + cacert, + ignoreTruststore, + env, + envVar, + insecure, + r: recursive, + output: outputPath, + format, + testsOnly, + bail + } = argv; const collectionPath = process.cwd(); // todo @@ -337,6 +364,7 @@ const handler = async function (argv) { } } } + options['ignoreTruststore'] = ignoreTruststore; if (['json', 'junit', 'html'].indexOf(format) === -1) { console.error(chalk.red(`Format must be one of "json", "junit or "html"`)); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index ec4767efb..187f371cf 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -3,6 +3,7 @@ const qs = require('qs'); const chalk = require('chalk'); const decomment = require('decomment'); const fs = require('fs'); +const tls = require('tls'); const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash'); const FormData = require('form-data'); const prepareRequest = require('./prepare-request'); @@ -106,7 +107,11 @@ const runSingleRequest = async function ( const caCert = caCertArray.find((el) => el); if (caCert && caCert.length > 1) { try { - httpsAgentRequestFields['ca'] = fs.readFileSync(caCert); + let caCertBuffer = fs.readFileSync(caCert); + if (!options['ignoreTruststore']) { + caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates + } + httpsAgentRequestFields['ca'] = caCertBuffer; } catch (err) { console.log('Error reading CA cert file:' + caCert, err); } diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 844e96b06..f9497abee 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -16,7 +16,7 @@ const defaultPreferences = { filePath: null }, keepDefaultCaCertificates: { - enabled: false + enabled: true }, storeCookies: true, sendCookies: true, @@ -118,7 +118,7 @@ const preferencesUtil = { return get(getPreferences(), 'request.customCaCertificate.enabled', false); }, shouldKeepDefaultCaCertificates: () => { - return get(getPreferences(), 'request.keepDefaultCaCertificates.enabled', false); + return get(getPreferences(), 'request.keepDefaultCaCertificates.enabled', true); }, getCustomCaCertificateFilePath: () => { return get(getPreferences(), 'request.customCaCertificate.filePath', null); From ec1b734b3afcf01c1a5ba5d711c1632cbbc03431 Mon Sep 17 00:00:00 2001 From: anusreesubash <65728079+anusreesubash@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:23:13 +0530 Subject: [PATCH 025/904] fix: changed title from 'bruno' to 'Bruno' (#2476) Co-authored-by: Anusree Subash --- packages/bruno-app/src/pages/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/pages/index.js b/packages/bruno-app/src/pages/index.js index 1567ef2c4..de969b5a5 100644 --- a/packages/bruno-app/src/pages/index.js +++ b/packages/bruno-app/src/pages/index.js @@ -6,7 +6,7 @@ export default function Home() { return (
- bruno + Bruno From 271f988d99ad3c99ac6842319c874428422eab58 Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Lima <36305985+leoferreiralima@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:38:14 -0300 Subject: [PATCH 026/904] feat: add recursive interpolate (#2234) * feat: add recursive interpolate fixes #2227 * test(bruno-common): fix test with 3 level of recursion * fix(bruno-common): add ability to reference the same variable repeatedly --- .../src/utils/codemirror/brunoVarInfo.js | 7 +- packages/bruno-common/package.json | 1 + .../src/interpolate/index.spec.ts | 167 ++++++++++++++++++ .../bruno-common/src/interpolate/index.ts | 39 +++- 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index d37e10bb6..045c6acf7 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -6,6 +6,11 @@ * LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3 */ +// Todo: Fix this +// import { interpolate } from '@usebruno/common'; +import brunoCommon from '@usebruno/common'; +const { interpolate } = brunoCommon; + let CodeMirror; const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const { get } = require('lodash'); @@ -21,7 +26,7 @@ if (!SERVER_RENDERED) { // str is of format {{variableName}}, extract variableName // we are seeing that from the gql query editor, the token string is of format variableName const variableName = str.replace('{{', '').replace('}}', '').trim(); - const variableValue = get(options.variables, variableName); + const variableValue = interpolate(get(options.variables, variableName), options.variables); const into = document.createElement('div'); const descriptionDiv = document.createElement('div'); diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json index d8e598420..cc25f2337 100644 --- a/packages/bruno-common/package.json +++ b/packages/bruno-common/package.json @@ -13,6 +13,7 @@ "scripts": { "clean": "rimraf dist", "test": "jest", + "test:watch": "jest --watch", "prebuild": "npm run clean", "build": "rollup -c", "prepack": "npm run test && npm run build" diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts index 9779021ee..a1994d473 100644 --- a/packages/bruno-common/src/interpolate/index.spec.ts +++ b/packages/bruno-common/src/interpolate/index.spec.ts @@ -169,3 +169,170 @@ describe('interpolate - value edge cases', () => { expect(result).toBe(inputString); }); }); + +describe('interpolate - recursive', () => { + it('should replace placeholders with 1 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + 'user.name': 'Bruno', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno and I am 4 years old'); + }); + + it('should replace placeholders with 2 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + 'user.name': 'Bruno {{user.lastName}}', + 'user.lastName': 'Dog', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old'); + }); + + it('should replace placeholders with 3 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.full_name}} and I am {{user.age}} years old', + 'user.full_name': '{{user.name}}', + 'user.name': 'Bruno {{user.lastName}}', + 'user.lastName': 'Dog', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old'); + }); + + it('should handle missing values with 1 level of recursion by leaving the placeholders unchanged using {{}} as delimiters', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is {{user.name}} and I am 4 years old'); + }); + + it('should handle all valid keys with 1 level of recursion', () => { + const message = ` + Hi, I am {{user.full_name}}, + I am {{user.age}} years old. + My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}. + I like attention: {{user.want.attention}} +`; + const inputObject = { + user: { + message, + full_name: 'Bruno', + age: 4, + 'fav-food': ['egg', 'meat'], + 'want.attention': true + } + }; + + const inputStr = '{{user.message}}'; + const expectedStr = ` + Hi, I am Bruno, + I am 4 years old. + My favorite food is egg and meat. + I like attention: true +`; + const result = interpolate(inputStr, inputObject); + expect(result).toBe(expectedStr); + }); + + it('should not process 1 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion}}'); + }); + + it('should not process 2 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion2}}', + recursion2: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion2}}'); + }); + + it('should not process 3 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion2}}', + recursion2: '{{recursion3}}', + recursion3: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion2}}'); + }); + + it('should replace repetead placeholders with 1 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: 'repetead2' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(2).fill('repetead2').join(' ')); + }); + + it('should replace repetead placeholders with 2 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}', + repetead3: 'repetead3' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(6).fill('repetead3').join(' ')); + }); + + it('should replace repetead placeholders with 3 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}', + repetead3: '{{repetead4}} {{repetead4}} {{repetead4}} {{repetead4}}', + repetead4: 'repetead4' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(24).fill('repetead4').join(' ')); + }); +}); diff --git a/packages/bruno-common/src/interpolate/index.ts b/packages/bruno-common/src/interpolate/index.ts index 8ad86c5b1..d4bd7cb6b 100644 --- a/packages/bruno-common/src/interpolate/index.ts +++ b/packages/bruno-common/src/interpolate/index.ts @@ -11,6 +11,7 @@ * Output: Hello, my name is Bruno and I am 4 years old */ +import { Set } from 'typescript'; import { flattenObject } from '../utils'; const interpolate = (str: string, obj: Record): string => { @@ -18,14 +19,40 @@ const interpolate = (str: string, obj: Record): string => { return str; } - const patternRegex = /\{\{([^}]+)\}\}/g; const flattenedObj = flattenObject(obj); - const result = str.replace(patternRegex, (match, placeholder) => { - const replacement = flattenedObj[placeholder]; - return replacement !== undefined ? replacement : match; - }); - return result; + return replace(str, flattenedObj); +}; + +const replace = ( + str: string, + flattenedObj: Record, + visited = new Set(), + results = new Map() +): string => { + const patternRegex = /\{\{([^}]+)\}\}/g; + + return str.replace(patternRegex, (match, placeholder) => { + const replacement = flattenedObj[placeholder]; + + if (results.has(match)) { + return results.get(match); + } + + if (patternRegex.test(replacement) && !visited.has(match)) { + visited.add(match); + const result = replace(replacement, flattenedObj, visited, results); + results.set(match, result); + + return result; + } + + visited.add(match); + const result = replacement !== undefined ? replacement : match; + results.set(match, result); + + return result; + }); }; export default interpolate; From 9588b442f7b18fa4625ac58735301e54460ae8eb Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:37:08 +0530 Subject: [PATCH 027/904] refactor: now the key params in the axiosRequest creation phase is pathParams to resolve the naming conflicts of axios. (#2465) --- packages/bruno-electron/src/ipc/network/index.js | 4 ++++ packages/bruno-electron/src/ipc/network/interpolate-vars.js | 6 +++--- packages/bruno-electron/src/ipc/network/prepare-request.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 0a5fee775..dfa9d7aca 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -255,6 +255,10 @@ const configureRequest = async ( request.headers['cookie'] = cookieString; } } + + // Remove pathParams, already in URL (Issue #2439) + delete request.pathParams; + return axiosInstance; }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 40fe9faa6..138083e1d 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -82,11 +82,11 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces request.data = _interpolate(request.data); } - each(request.params, (param) => { + each(request.pathParams, (param) => { param.value = _interpolate(param.value); }); - if (request?.params?.length) { + if (request?.pathParams?.length) { let url = request.url; if (!url.startsWith('http://') && !url.startsWith('https://')) { @@ -107,7 +107,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces return '/' + path; } else { const name = path.slice(1); - const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name); + const existingPathParam = request.pathParams.find((param) => param.type === 'path' && param.name === name); return existingPathParam ? '/' + existingPathParam.value : ''; } }) diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index ed13eeb54..36e064919 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -162,7 +162,7 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { method: request.method, url, headers, - params: request.params.filter((param) => param.type === 'path'), + pathParams: request.params.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; From 3db7d54acad3cccbe8282a2467302824f7241c23 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:05:09 +0530 Subject: [PATCH 028/904] bugfix(#2462) (#2463) * fixed * refactor: conditionals. * refactor: Update importPostmanV2CollectionItem function to handle file and text formdata --- .../src/utils/importers/postman-collection.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index e34530a19..dc291dbdb 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -180,12 +180,27 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = 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: 'text', + type: type, name: param.key, - value: param.value, + value: value, description: param.description, enabled: !param.disabled }); From d88bb68014d23dd55a4ecccf2e254825b2798009 Mon Sep 17 00:00:00 2001 From: Nicolas Nobelis Date: Wed, 19 Jun 2024 12:32:56 +0200 Subject: [PATCH 029/904] fix: Pass the scope conditionally for OAuth2 grant types (#2447) Fixes: #2446. Signed-off-by: Nicolas Nobelis --- .../src/ipc/network/oauth2-helper.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index a7921e85a..33b845e59 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -30,9 +30,11 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) redirect_uri: callbackUrl, client_id: clientId, client_secret: clientSecret, - scope: scope, state: state }; + if (scope) { + data['scope'] = scope; + } if (pkce) { data['code_verifier'] = codeVerifier; } @@ -88,9 +90,11 @@ const transformClientCredentialsRequest = async (request) => { const data = { grant_type: 'client_credentials', client_id: clientId, - client_secret: clientSecret, - scope + client_secret: clientSecret }; + if (scope) { + data.scope = scope; + } const url = requestCopy?.oauth2?.accessTokenUrl; return { data, @@ -109,9 +113,11 @@ const transformPasswordCredentialsRequest = async (request) => { username, password, client_id: clientId, - client_secret: clientSecret, - scope + client_secret: clientSecret }; + if (scope) { + data.scope = scope; + } const url = requestCopy?.oauth2?.accessTokenUrl; return { data, From 9ec67b5da9a2f689c6eb807c750a4542883cc796 Mon Sep 17 00:00:00 2001 From: Fabio GRANDE Date: Wed, 19 Jun 2024 12:34:57 +0200 Subject: [PATCH 030/904] Added IsArray on Assert (#2413) Co-authored-by: Fabio Grande --- .../Assertions/AssertionOperator/index.js | 4 +++- .../RequestPane/Assertions/AssertionRow/index.js | 10 +++++++--- packages/bruno-js/src/runtime/assert-runtime.js | 13 ++++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js index 068747846..0f246db7c 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js @@ -32,6 +32,7 @@ import lightTheme from 'themes/light'; * isNumber : is number * isString : is string * isBoolean : is boolean + * isArray : is array */ const AssertionOperator = ({ operator, onChange }) => { @@ -61,7 +62,8 @@ const AssertionOperator = ({ operator, onChange }) => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; const handleChange = (e) => { diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js index 5b27061e9..375fa0ec4 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js @@ -33,6 +33,7 @@ import { useTheme } from 'providers/Theme'; * isNumber : is number * isString : is string * isBoolean : is boolean + * isArray : is array */ const parseAssertionOperator = (str = '') => { if (!str || typeof str !== 'string' || !str.length) { @@ -68,7 +69,8 @@ const parseAssertionOperator = (str = '') => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; const unaryOperators = [ @@ -81,7 +83,8 @@ const parseAssertionOperator = (str = '') => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; const [operator, ...rest] = str.trim().split(' '); @@ -118,7 +121,8 @@ const isUnaryOperator = (operator) => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; return unaryOperators.includes(operator); diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index 36fd7c9f2..dae68fc1a 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -66,6 +66,7 @@ chai.use(function (chai, utils) { * isNumber : is number * isString : is string * isBoolean : is boolean + * isArray : is array */ const parseAssertionOperator = (str = '') => { if (!str || typeof str !== 'string' || !str.length) { @@ -101,7 +102,8 @@ const parseAssertionOperator = (str = '') => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; const unaryOperators = [ @@ -114,7 +116,8 @@ const parseAssertionOperator = (str = '') => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; const [operator, ...rest] = str.trim().split(' '); @@ -151,7 +154,8 @@ const isUnaryOperator = (operator) => { 'isJson', 'isNumber', 'isString', - 'isBoolean' + 'isBoolean', + 'isArray' ]; return unaryOperators.includes(operator); @@ -313,6 +317,9 @@ class AssertRuntime { case 'isBoolean': expect(lhs).to.be.a('boolean'); break; + case 'isArray': + expect(lhs).to.be.a('array'); + break; default: expect(lhs).to.equal(rhs); break; From b432e94a4eacdbfa10d1be8711fbed2f25af4e14 Mon Sep 17 00:00:00 2001 From: Jack Evans Date: Wed, 19 Jun 2024 07:36:45 -0300 Subject: [PATCH 031/904] interpolate vars correctly fixes #704 (#2317) --- packages/bruno-electron/src/ipc/network/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dfa9d7aca..06517767e 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -744,11 +744,11 @@ const registerNetworkIpc = (mainWindow) => { }); }); - ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, request, collection) => { + ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { try { const envVars = getEnvVars(environment); const collectionRoot = get(collection, 'root', {}); - const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot); + const request = prepareGqlIntrospectionRequest(endpoint, envVars, _request, collectionRoot); request.timeout = preferencesUtil.getRequestTimeout(); @@ -778,16 +778,16 @@ const registerNetworkIpc = (mainWindow) => { scriptingConfig ); - interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars); + interpolateVars(request, envVars, collection.collectionVariables, processEnvVars); const axiosInstance = await configureRequest( collection.uid, - preparedRequest, + request, envVars, collection.collectionVariables, processEnvVars, collectionPath ); - const response = await axiosInstance(preparedRequest); + const response = await axiosInstance(request); await runPostResponse( request, From a0df5138b3acc80744a4d40a4679224ea7c677fb Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Lima <36305985+leoferreiralima@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:39:21 -0300 Subject: [PATCH 032/904] fix: add authorization token to generate code (#1792) fixes #1791 --- .../GenerateCodeItem/CodeView/index.js | 11 +++++-- .../bruno-app/src/utils/codegenerator/auth.js | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-app/src/utils/codegenerator/auth.js diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js index f0fa506b2..6771f4ced 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -9,6 +9,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import toast from 'react-hot-toast'; import { IconCopy } from '@tabler/icons'; import { findCollectionByItemUid } from '../../../../../../../utils/collections/index'; +import { getAuthHeaders } from '../../../../../../../utils/codegenerator/auth'; const CodeView = ({ language, item }) => { const { displayedTheme } = useTheme(); @@ -20,10 +21,16 @@ const CodeView = ({ language, item }) => { item.uid ); - const headers = [...(collection?.root?.request?.headers || []), ...(requestHeaders || [])]; + const collectionRootAuth = collection?.root?.request?.auth; + const requestAuth = item.draft ? get(item, 'draft.request.auth') : get(item, 'request.auth'); + + const headers = [ + ...getAuthHeaders(collectionRootAuth, requestAuth), + ...(collection?.root?.request?.headers || []), + ...(requestHeaders || []) + ]; let snippet = ''; - try { snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client); } catch (e) { diff --git a/packages/bruno-app/src/utils/codegenerator/auth.js b/packages/bruno-app/src/utils/codegenerator/auth.js new file mode 100644 index 000000000..4b3d23e64 --- /dev/null +++ b/packages/bruno-app/src/utils/codegenerator/auth.js @@ -0,0 +1,30 @@ +import get from 'lodash/get'; + +export const getAuthHeaders = (collectionRootAuth, requestAuth) => { + const auth = collectionRootAuth && ['inherit', 'none'].includes(requestAuth.mode) ? collectionRootAuth : requestAuth; + + switch (auth.mode) { + case 'basic': + const username = get(auth, 'basic.username'); + const password = get(auth, 'basic.password'); + const basicToken = Buffer.from(`${username}:${password}`).toString('base64'); + + return [ + { + enabled: true, + name: 'Authorization', + value: `Basic ${basicToken}` + } + ]; + case 'bearer': + return [ + { + enabled: true, + name: 'Authorization', + value: `Bearer ${get(auth, 'bearer.token')}` + } + ]; + default: + return []; + } +}; From 9c11e27d1c7ef24d8678e37373637fe61cf41f43 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Wed, 19 Jun 2024 13:48:36 +0300 Subject: [PATCH 033/904] fix: decomment not working in json body (#1819) Request body json was not decommented if json parsing fails, which would happen if variables are not quoted. Fixes usebruno#888 --- .../bruno-cli/src/runner/prepare-request.js | 5 +++-- .../tests/runner/prepare-request.spec.js | 21 +++++++++++++++++++ .../src/ipc/network/prepare-request.js | 5 +++-- .../tests/network/prepare-request.spec.js | 21 +++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 packages/bruno-cli/tests/runner/prepare-request.spec.js create mode 100644 packages/bruno-electron/tests/network/prepare-request.spec.js diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 8b1b249db..eab44f8c0 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -76,10 +76,11 @@ const prepareRequest = (request, collectionRoot) => { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } + const jsonBody = decomment(request.body.json); try { - axiosRequest.data = JSONbig.parse(decomment(request.body.json)); + axiosRequest.data = JSONbig.parse(jsonBody); } catch (ex) { - axiosRequest.data = request.body.json; + axiosRequest.data = jsonBody; } } diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js new file mode 100644 index 000000000..6e2219af8 --- /dev/null +++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js @@ -0,0 +1,21 @@ +const { describe, it, expect } = require('@jest/globals'); + +const prepareRequest = require('../../src/runner/prepare-request'); + +describe('prepare-request: prepareRequest', () => { + describe('Decomments request body', () => { + it('If request body is valid JSON', async () => { + const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' }; + const expected = { test: '{{someVar}}' }; + const result = prepareRequest({ body }); + expect(result.data).toEqual(expected); + }); + + it('If request body is not valid JSON', async () => { + const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' }; + const expected = '{\n"test": {{someVar}} \n}'; + const result = prepareRequest({ body }); + expect(result.data).toEqual(expected); + }); + }); +}); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 36e064919..eaa6c8de0 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -172,10 +172,11 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } + const body = decomment(request.body.json); try { - axiosRequest.data = JSONbig.parse(decomment(request.body.json)); + axiosRequest.data = JSONbig.parse(body); } catch (ex) { - axiosRequest.data = request.body.json; + axiosRequest.data = body; } } diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js new file mode 100644 index 000000000..833f58310 --- /dev/null +++ b/packages/bruno-electron/tests/network/prepare-request.spec.js @@ -0,0 +1,21 @@ +const { describe, it, expect } = require('@jest/globals'); + +const prepareRequest = require('../../src/ipc/network/prepare-request'); + +describe('prepare-request: prepareRequest', () => { + describe('Decomments request body', () => { + it('If request body is valid JSON', async () => { + const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' }; + const expected = { test: '{{someVar}}' }; + const result = prepareRequest({ body }); + expect(result.data).toEqual(expected); + }); + + it('If request body is not valid JSON', async () => { + const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' }; + const expected = '{\n"test": {{someVar}} \n}'; + const result = prepareRequest({ body }); + expect(result.data).toEqual(expected); + }); + }); +}); From 1e2b07dead2d0fc463597f3017bef2acba2d2034 Mon Sep 17 00:00:00 2001 From: Baptiste Poulain <64689165+bpoulaindev@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:11:39 +0200 Subject: [PATCH 034/904] Fix postman translation : support postman. ; additional translations (#2453) * fix(postman-translation): cleanup logs after each import, pm.expect.fail(), remove duplicated _test_ key, flex wrap * fix(postman-translation): support for postman. - update jest file --------- Co-authored-by: bpoulaindev --- .../Sidebar/ImportCollectionLocation/index.js | 20 +++++++++---------- .../src/utils/importers/postman-collection.js | 8 ++++++-- .../utils/importers/translators/index.spec.js | 18 +++++++++++++++-- .../translators/postman_translation.js | 17 ++++++++++++---- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index 96fade0db..4211e8ff1 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -65,20 +65,18 @@ const TranslationLog = ({ translationLog }) => {
{value.script && ( -
- - test : - {value.script.map((scriptValue, index) => ( - - {scriptValue} - {index < value.script.length - 1 && <> - } - - ))} - +
+ script : + {value.script.map((scriptValue, index) => ( + + {scriptValue} + {index < value.script.length - 1 && <> - } + + ))}
)} {value.test && ( -
+
test : {value.test.map((testValue, index) => (
diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index dc291dbdb..0683a3bbc 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -54,7 +54,7 @@ const convertV21Auth = (array) => { }, {}); }; -const translationLog = {}; +let translationLog = {}; const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => { brunoParent.items = brunoParent.items || []; @@ -394,9 +394,13 @@ const importCollection = (options) => { .then((collection) => resolve({ collection, translationLog })) .catch((err) => { console.log(err); + translationLog = {}; reject(new BrunoError('Import collection failed')); }) - .then(() => logTranslationDetails(translationLog)); + .then(() => { + logTranslationDetails(translationLog); + translationLog = {}; + }); }); }; diff --git a/packages/bruno-app/src/utils/importers/translators/index.spec.js b/packages/bruno-app/src/utils/importers/translators/index.spec.js index 6f9d2fb1a..b70691f21 100644 --- a/packages/bruno-app/src/utils/importers/translators/index.spec.js +++ b/packages/bruno-app/src/utils/importers/translators/index.spec.js @@ -40,8 +40,8 @@ describe('postmanTranslation function', () => { }); test('should comment non-translated pm commands', () => { - const inputScript = "pm.test('random test', () => pm.variables.replaceIn('{{$guid}}'));"; - const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));"; + const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; + const expectedOutput = "// test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); test('should handle multiple pm commands on the same line', () => { @@ -137,3 +137,17 @@ describe('postmanTranslation function', () => { expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); }); + +test('should handle response commands', () => { + const inputScript = ` + const responseTime = pm.response.responseTime; + const responseCode = pm.response.code; + const responseText = pm.response.text(); + `; + const expectedOutput = ` + const responseTime = res.getResponseTime(); + const responseCode = res.getStatus(); + const responseText = res.getBody()?.toString(); + `; + expect(postmanTranslation(inputScript)).toBe(expectedOutput); +}); diff --git a/packages/bruno-app/src/utils/importers/translators/postman_translation.js b/packages/bruno-app/src/utils/importers/translators/postman_translation.js index 7844f91f8..bd60fbcac 100644 --- a/packages/bruno-app/src/utils/importers/translators/postman_translation.js +++ b/packages/bruno-app/src/utils/importers/translators/postman_translation.js @@ -13,10 +13,19 @@ const replacements = { 'pm\\.expect\\(': 'expect(', 'pm\\.environment\\.has\\(([^)]+)\\)': 'bru.getEnvVar($1) !== undefined && bru.getEnvVar($1) !== null', 'pm\\.response\\.code': 'res.getStatus()', - 'pm\\.response\\.text\\(': 'res.getBody()?.toString(' + 'pm\\.response\\.text\\(': 'res.getBody()?.toString(', + 'pm\\.expect\\.fail\\(': 'expect.fail(', + 'pm\\.response\\.responseTime': 'res.getResponseTime()' }; -const compiledReplacements = Object.entries(replacements).map(([pattern, replacement]) => ({ +const extendedReplacements = Object.keys(replacements).reduce((acc, key) => { + const newKey = key.replace(/^pm\\\./, 'postman\\.'); + acc[key] = replacements[key]; + acc[newKey] = replacements[key]; + return acc; +}, {}); + +const compiledReplacements = Object.entries(extendedReplacements).map(([pattern, replacement]) => ({ regex: new RegExp(pattern, 'g'), replacement })); @@ -31,8 +40,8 @@ export const postmanTranslation = (script, logCallback) => { modified = true; } } - if (modifiedScript.includes('pm.')) { - modifiedScript = modifiedScript.replace(/^(.*pm\..*)$/gm, '// $1'); + if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) { + modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); logCallback?.(); } return modifiedScript; From 929d2b529990de4e834b63be0e0dfc7ecd181b76 Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 21 Jun 2024 10:44:24 +0530 Subject: [PATCH 035/904] fix/generate code auth header validation (#2490) * geenrate code for request to include collection auth headers only if request auth is of type inherit * validations --- packages/bruno-app/src/utils/codegenerator/auth.js | 8 ++++---- .../bruno-electron/src/ipc/network/prepare-request.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/utils/codegenerator/auth.js b/packages/bruno-app/src/utils/codegenerator/auth.js index 4b3d23e64..981d6cec2 100644 --- a/packages/bruno-app/src/utils/codegenerator/auth.js +++ b/packages/bruno-app/src/utils/codegenerator/auth.js @@ -1,12 +1,12 @@ import get from 'lodash/get'; export const getAuthHeaders = (collectionRootAuth, requestAuth) => { - const auth = collectionRootAuth && ['inherit', 'none'].includes(requestAuth.mode) ? collectionRootAuth : requestAuth; + const auth = collectionRootAuth && ['inherit'].includes(requestAuth?.mode) ? collectionRootAuth : requestAuth; switch (auth.mode) { case 'basic': - const username = get(auth, 'basic.username'); - const password = get(auth, 'basic.password'); + const username = get(auth, 'basic.username', ''); + const password = get(auth, 'basic.password', ''); const basicToken = Buffer.from(`${username}:${password}`).toString('base64'); return [ @@ -21,7 +21,7 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth) => { { enabled: true, name: 'Authorization', - value: `Bearer ${get(auth, 'bearer.token')}` + value: `Bearer ${get(auth, 'bearer.token', '')}` } ]; default: diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index eaa6c8de0..bd09619f4 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -162,7 +162,7 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { method: request.method, url, headers, - pathParams: request.params.filter((param) => param.type === 'path'), + pathParams: request?.params?.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; From b031e1f00976065af3753f1623bbbaa9b867719a Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 21 Jun 2024 10:45:12 +0530 Subject: [PATCH 036/904] add decomment step in a try catch block (#2485) * add decomment step in a try catch block * request params validation - fix unit tests --- packages/bruno-cli/src/runner/prepare-request.js | 9 +++++++-- .../src/ipc/network/prepare-request.js | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index eab44f8c0..aaeb15441 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -76,10 +76,15 @@ const prepareRequest = (request, collectionRoot) => { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } - const jsonBody = decomment(request.body.json); + let jsonBody; + try { + jsonBody = decomment(request?.body?.json); + } catch (error) { + jsonBody = request?.body?.json; + } try { axiosRequest.data = JSONbig.parse(jsonBody); - } catch (ex) { + } catch (error) { axiosRequest.data = jsonBody; } } diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index bd09619f4..73b8bf71c 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -172,11 +172,16 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } - const body = decomment(request.body.json); + let jsonBody; try { - axiosRequest.data = JSONbig.parse(body); - } catch (ex) { - axiosRequest.data = body; + jsonBody = decomment(request?.body?.json); + } catch (error) { + jsonBody = request?.body?.json; + } + try { + axiosRequest.data = JSONbig.parse(jsonBody); + } catch (error) { + axiosRequest.data = jsonBody; } } From 963b197afcd52eafd83da696dec4f9c9ad7d7ed4 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:46:51 +0530 Subject: [PATCH 037/904] Refactor countRequests function to use getTotalRequestCountInCollection in CollectionSettings/Info/index.js (#2140) --- .../CollectionSettings/Info/index.js | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Info/index.js index f69e0b25a..3b0a1297b 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Info/index.js @@ -1,26 +1,10 @@ import React from 'react'; import StyledWrapper from './StyledWrapper'; - -function countRequests(items) { - let count = 0; - - function recurse(item) { - if (item && typeof item === 'object') { - if (item.type !== 'folder') { - count++; - } - if (Array.isArray(item.items)) { - item.items.forEach(recurse); - } - } - } - - items.forEach(recurse); - - return count; -} +import { getTotalRequestCountInCollection } from 'utils/collections/'; const Info = ({ collection }) => { + const totalRequestsInCollection = getTotalRequestCountInCollection(collection); + return (
General information about the collection.
@@ -44,7 +28,7 @@ const Info = ({ collection }) => { Requests : - {countRequests(collection.items)} + {totalRequestsInCollection} From 01e93b5c2be98c61adf9366b8525e31911cc2230 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:03:50 +0530 Subject: [PATCH 038/904] Feat/highlight path params (#2415) * fix: Update CodeMirror mode to use combinedmode for SingleLineEditor. This will highlight the pathparams in the QueryUrl. * Refactor: Updated url highlighting. * refactor: Improved the hinting part. * refactor: CodeEditor, MultiLineEditor, and QueryEditor to use defineCombinedCodeMirrorMode for highlighting Bruno variables instead of defineCodeMirrorBrunoVariablesMode * refactor: Updated defineCombinedCodeMirrorMode to defineCodeMirrorCombinedVariablesMode. Now the pathparams at the end of the URL is also highlighted. * refactor: Update CodeMirror mode to use defineCodeMirrorBrunoVariablesMode instead of defineCodeMirrorCombinedVariablesMode. Now the path params in the URL will be highlighted on application open. --------- Co-authored-by: Anoop M D --- .../components/RequestPane/QueryUrl/index.js | 1 + .../src/components/SingleLineEditor/index.js | 18 +++--- .../src/utils/codemirror/brunoVarInfo.js | 21 +++++-- .../bruno-app/src/utils/collections/index.js | 18 +++++- .../bruno-app/src/utils/common/codemirror.js | 55 ++++++++++++++----- 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 4989ac80d..88fe4ee01 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -69,6 +69,7 @@ const QueryUrl = ({ item, collection, handleRun }) => { onChange={(newValue) => onUrlChange(newValue)} onRun={handleRun} collection={collection} + item={item} />
{ - if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) { + if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.key !== 'Enter') { /*Enter - do not open autocomplete list just after item has been selected in it*/ CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete }); } @@ -90,7 +92,7 @@ class SingleLineEditor extends Component { } this.editor.setValue(String(this.props.value) || ''); this.editor.on('change', this._onEdit); - this.addOverlay(); + this.addOverlay(variables); } _onEdit = () => { @@ -108,10 +110,10 @@ class SingleLineEditor extends Component { // event loop. this.ignoreChangeEvent = true; - let variables = getAllVariables(this.props.collection); + let variables = getAllVariables(this.props.collection, this.props.item); if (!isEqual(variables, this.variables)) { this.editor.options.brunoVarInfo.variables = variables; - this.addOverlay(); + this.addOverlay(variables); } if (this.props.theme !== prevProps.theme && this.editor) { this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); @@ -127,12 +129,10 @@ class SingleLineEditor extends Component { this.editor.getWrapperElement().remove(); } - addOverlay = () => { - let variables = getAllVariables(this.props.collection); + addOverlay = (variables) => { this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, 'text/plain'); - this.editor.setOption('mode', 'brunovariables'); + this.editor.setOption('mode', 'combinedmode'); }; render() { diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index 045c6acf7..1632aa43a 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -23,10 +23,23 @@ if (!SERVER_RENDERED) { if (!str || !str.length || typeof str !== 'string') { return; } - // str is of format {{variableName}}, extract variableName - // we are seeing that from the gql query editor, the token string is of format variableName - const variableName = str.replace('{{', '').replace('}}', '').trim(); - const variableValue = interpolate(get(options.variables, variableName), options.variables); + + // str is of format {{variableName}} or :variableName, extract variableName + let variableName; + let variableValue; + + if (str.startsWith('{{')) { + variableName = str.replace('{{', '').replace('}}', '').trim(); + variableValue = interpolate(get(options.variables, variableName), options.variables); + } else if (str.startsWith(':')) { + variableName = str.replace(':', '').trim(); + variableValue = + options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined; + } + + if (variableValue === undefined) { + return; + } const into = document.createElement('div'); const descriptionDiv = document.createElement('div'); diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 8b08174e2..6145ee200 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -657,6 +657,18 @@ export const getEnvironmentVariables = (collection) => { return variables; }; +const getPathParams = (item) => { + let pathParams = {}; + if (item && item.request && item.request.params) { + item.request.params.forEach((param) => { + if (param.type === 'path' && param.name && param.value) { + pathParams[param.name] = param.value; + } + }); + } + return pathParams; +}; + export const getTotalRequestCountInCollection = (collection) => { let count = 0; each(collection.items, (item) => { @@ -670,12 +682,16 @@ export const getTotalRequestCountInCollection = (collection) => { return count; }; -export const getAllVariables = (collection) => { +export const getAllVariables = (collection, item) => { const environmentVariables = getEnvironmentVariables(collection); + const pathParams = getPathParams(item); return { ...environmentVariables, ...collection.collectionVariables, + pathParams: { + ...pathParams + }, process: { env: { ...collection.processEnvVariables diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index a6bf52aed..ecd197428 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -13,32 +13,61 @@ const pathFoundInVariables = (path, obj) => { }; export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => { - CodeMirror.defineMode('brunovariables', function (config, parserConfig) { - let variablesOverlay = { - token: function (stream, state) { + CodeMirror.defineMode('combinedmode', function (config, parserConfig) { + const variablesOverlay = { + token: function (stream) { if (stream.match('{{', true)) { let ch; let word = ''; while ((ch = stream.next()) != null) { - if (ch == '}' && stream.next() == '}') { + if (ch === '}' && stream.peek() === '}') { stream.eat('}'); - let found = pathFoundInVariables(word, variables); - if (found) { - return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9); - } else { - return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9); - } - // Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror. + const found = pathFoundInVariables(word, variables); + const status = found ? 'valid' : 'invalid'; + const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`; + return `variable-${status} ${randomClass}`; } word += ch; } } - while (stream.next() != null && !stream.match('{{', false)) {} + stream.skipTo('{{') || stream.skipToEnd(); return null; } }; - return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay); + const urlPathParamsOverlay = { + token: function (stream) { + if (stream.match(':', true)) { + let ch; + let word = ''; + while ((ch = stream.next()) != null) { + if (ch === '/' || ch === '?' || ch === '&' || ch === '=') { + stream.backUp(1); + const found = pathFoundInVariables(word, variables?.pathParams); + const status = found ? 'valid' : 'invalid'; + const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`; + return `variable-${status} ${randomClass}`; + } + word += ch; + } + + // If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL. + if (word) { + const found = pathFoundInVariables(word, variables?.pathParams); + const status = found ? 'valid' : 'invalid'; + const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`; + return `variable-${status} ${randomClass}`; + } + } + stream.skipTo(':') || stream.skipToEnd(); + return null; + } + }; + + return CodeMirror.overlayMode( + CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay), + urlPathParamsOverlay + ); }); }; From afcdf30b495f3dcb85f186771ed9fb8282f5f528 Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 21 Jun 2024 11:07:29 +0530 Subject: [PATCH 039/904] Fix/graphql file load (#2484) * fix graphql schema file load issue * feat: update file load logic --- .../GraphQLSchemaActions/useGraphqlSchema.js | 26 ++++++++++++++----- packages/bruno-electron/src/ipc/collection.js | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js index 0a5f2bd01..5b1b6c277 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js @@ -1,8 +1,8 @@ import { useState } from 'react'; import toast from 'react-hot-toast'; -import { buildClientSchema } from 'graphql'; +import { buildClientSchema, buildSchema } from 'graphql'; import { fetchGqlSchema } from 'utils/network'; -import { simpleHash } from 'utils/common'; +import { simpleHash, safeParseJSON } from 'utils/common'; const schemaHashPrefix = 'bruno.graphqlSchema'; @@ -18,7 +18,12 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => { if (!saved) { return null; } - return buildClientSchema(JSON.parse(saved)); + let parsedData = safeParseJSON(saved); + if (typeof parsedData === 'object') { + return buildClientSchema(parsedData); + } else { + return buildSchema(parsedData); + } } catch { localStorage.setItem(localStorageKey, null); return null; @@ -48,7 +53,7 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => { return; } setSchemaSource('file'); - return schemaContent.data; + return schemaContent?.data || schemaContent; }; const loadSchema = async (schemaSource) => { @@ -66,11 +71,18 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => { // fallback to introspection if source is unknown data = await loadSchemaFromIntrospection(); } - setSchema(buildClientSchema(data)); - localStorage.setItem(localStorageKey, JSON.stringify(data)); - toast.success('GraphQL Schema loaded successfully'); + if (data) { + if (typeof data === 'object') { + setSchema(buildClientSchema(data)); + } else { + setSchema(buildSchema(data)); + } + localStorage.setItem(localStorageKey, JSON.stringify(data)); + toast.success('GraphQL Schema loaded successfully'); + } } catch (err) { setError(err); + console.error(err); toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`); } diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 648f893e4..5f8b63c3b 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -601,7 +601,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } const jsonData = fs.readFileSync(filePaths[0], 'utf8'); - return JSON.parse(jsonData); + return safeParseJSON(jsonData); } catch (err) { return Promise.reject(new Error('Failed to load GraphQL schema file')); } From 5259c5fb4aff9e00582278c434b6b7a60c5e7d94 Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 21 Jun 2024 11:13:52 +0530 Subject: [PATCH 040/904] Feat/client cert types (#2482) * feat: pfx/cert client certificates * ui updates * file tooltip * feat: updated client cert logic * feat: updated validations * const to let * throw error incase of invalid file paths * fix htmlFor label * updated cli error messages --- .../ClientCertSettings/index.js | 248 +++++++++++++++--- .../src/runner/run-single-request.js | 34 ++- .../bruno-electron/src/ipc/network/index.js | 45 ++-- 3 files changed, 265 insertions(+), 62 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js index c3e618edb..0baa9d39c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js @@ -7,32 +7,90 @@ import { IconEye, IconEyeOff } from '@tabler/icons'; import { useState } from 'react'; import StyledWrapper from './StyledWrapper'; +import { useRef } from 'react'; +import path from 'path'; +import slash from 'utils/common/slash'; const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => { + const certFilePathInputRef = useRef(); + const keyFilePathInputRef = useRef(); + const pfxFilePathInputRef = useRef(); + const formik = useFormik({ initialValues: { domain: '', + type: 'cert', certFilePath: '', keyFilePath: '', + pfxFilePath: '', passphrase: '' }, validationSchema: Yup.object({ domain: Yup.string().required(), - certFilePath: Yup.string().required(), - keyFilePath: Yup.string().required(), + type: Yup.string().required().oneOf(['cert', 'pfx']), + certFilePath: Yup.string().when('type', { + is: (type) => type == 'cert', + then: Yup.string().min(1, 'certFilePath is a required field').required() + }), + keyFilePath: Yup.string().when('type', { + is: (type) => type == 'cert', + then: Yup.string().min(1, 'keyFilePath is a required field').required() + }), + pfxFilePath: Yup.string().when('type', { + is: (type) => type == 'pfx', + then: Yup.string().min(1, 'pfxFilePath is a required field').required() + }), passphrase: Yup.string() }), onSubmit: (values) => { - onUpdate(values); + let relevantValues = {}; + if (values.type === 'cert') { + relevantValues = { + domain: values.domain, + type: values.type, + certFilePath: values.certFilePath, + keyFilePath: values.keyFilePath, + passphrase: values.passphrase + }; + } else { + relevantValues = { + domain: values.domain, + type: values.type, + pfxFilePath: values.pfxFilePath, + passphrase: values.passphrase + }; + } + onUpdate(relevantValues); + formik.resetForm(); + resetFileInputFields(); } }); const getFile = (e) => { - formik.values[e.name] = e.files[0].path; + e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path); + }; + + const resetFileInputFields = () => { + certFilePathInputRef.current.value = ''; + keyFilePathInputRef.current.value = ''; + pfxFilePathInputRef.current.value = ''; }; const [passwordVisible, setPasswordVisible] = useState(false); + const handleTypeChange = (e) => { + formik.setFieldValue('type', e.target.value); + if (e.target.value === 'cert') { + formik.setFieldValue('pfxFilePath', ''); + pfxFilePathInputRef.current.value = ''; + } else { + formik.setFieldValue('certFilePath', ''); + certFilePathInputRef.current.value = ''; + formik.setFieldValue('keyFilePath', ''); + keyFilePathInputRef.current.value = ''; + } + }; + return (
Add client certificates to be used for specific domains.
@@ -76,35 +134,163 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => { ) : null}
-
-
- - getFile(e.target)} - /> - {formik.touched.keyFilePath && formik.errors.keyFilePath ? ( -
{formik.errors.keyFilePath}
- ) : null} +
+ + +
+ {formik.values.type === 'cert' ? ( + <> +
+ +
+ getFile(e.target)} + ref={certFilePathInputRef} + /> + {formik.values.certFilePath ? ( +
+
+ {path.basename(slash(formik.values.certFilePath))} +
+ { + formik.setFieldValue('certFilePath', ''); + certFilePathInputRef.current.value = ''; + }} + /> +
+ ) : ( + <> + )} +
+ {formik.touched.certFilePath && formik.errors.certFilePath ? ( +
{formik.errors.certFilePath}
+ ) : null} +
+
+ +
+ getFile(e.target)} + ref={keyFilePathInputRef} + /> + {formik.values.keyFilePath ? ( +
+
+ {path.basename(slash(formik.values.keyFilePath))} +
+ { + formik.setFieldValue('keyFilePath', ''); + keyFilePathInputRef.current.value = ''; + }} + /> +
+ ) : ( + <> + )} +
+ {formik.touched.keyFilePath && formik.errors.keyFilePath ? ( +
{formik.errors.keyFilePath}
+ ) : null} +
+ + ) : ( + <> +
+ +
+ getFile(e.target)} + ref={pfxFilePathInputRef} + /> + {formik.values.pfxFilePath ? ( +
+
+ {path.basename(slash(formik.values.pfxFilePath))} +
+ { + formik.setFieldValue('pfxFilePath', ''); + pfxFilePathInputRef.current.value = ''; + }} + /> +
+ ) : ( + <> + )} +
+ {formik.touched.pfxFilePath && formik.errors.pfxFilePath ? ( +
{formik.errors.pfxFilePath}
+ ) : null} +
+ + )}
- {showShareCollectionModal && } + {showShareCollectionModal && }
diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js index f9e34fa33..621ae6815 100644 --- a/packages/bruno-app/src/components/FolderSettings/index.js +++ b/packages/bruno-app/src/components/FolderSettings/index.js @@ -28,7 +28,7 @@ const FolderSettings = ({ collection, folder }) => { tab = folderLevelSettingsSelectedTab[folder?.uid]; } - const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root; + const folderRoot = folder?.root; const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req; const hasTests = folderRoot?.request?.tests; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 9f3e600d0..321ed4fd5 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
{generateCodeItemModalOpen && ( - setGenerateCodeItemModalOpen(false)} /> + setGenerateCodeItemModalOpen(false)} /> )} ); diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 562fc319f..072394b7f 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -261,13 +261,14 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col return ( {showAddNewRequestModal && ( - setShowAddNewRequestModal(false)} /> + setShowAddNewRequestModal(false)} /> )} {showCloneRequestModal && ( setShowCloneRequestModal(false)} /> )} diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index d0cd0b459..1e1503a85 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -79,7 +79,7 @@ const RequestTabs = () => { return ( {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {collectionRequestTabs && collectionRequestTabs.length ? ( <> diff --git a/packages/bruno-app/src/components/ShareCollection/index.js b/packages/bruno-app/src/components/ShareCollection/index.js index 19f5f00be..d0db00905 100644 --- a/packages/bruno-app/src/components/ShareCollection/index.js +++ b/packages/bruno-app/src/components/ShareCollection/index.js @@ -7,8 +7,11 @@ import exportBrunoCollection from 'utils/collections/export'; import exportPostmanCollection from 'utils/exporters/postman-collection'; import { cloneDeep } from 'lodash'; import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; +import { useSelector } from 'react-redux'; +import { findCollectionByUid } from 'utils/collections/index'; -const ShareCollection = ({ onClose, collection }) => { +const ShareCollection = ({ onClose, collectionUid }) => { + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const handleExportBrunoCollection = () => { const collectionCopy = cloneDeep(collection); exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js index ab9fc1a7f..a0a1e6c09 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -1,5 +1,5 @@ import React, { useRef, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; @@ -11,11 +11,13 @@ import Help from 'components/Help'; import PathDisplay from 'components/PathDisplay'; import { useState } from 'react'; import { IconArrowBackUp, IconEdit } from "@tabler/icons"; +import { findCollectionByUid } from 'utils/collections/index'; -const CloneCollection = ({ onClose, collection }) => { +const CloneCollection = ({ onClose, collectionUid }) => { const inputRef = useRef(); const dispatch = useDispatch(); const [isEditing, toggleEditing] = useState(false); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const { name } = collection; const formik = useFormik({ @@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => { values.collectionName, values.collectionFolderName, values.collectionLocation, - collection.pathname + collection?.pathname ) ) .then(() => { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 9194e8a64..31a58c2dd 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -15,7 +15,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const CloneCollectionItem = ({ collection, item, onClose }) => { +const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => { .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)) }), onSubmit: (values) => { - dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid)) + dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid)) .then(() => { toast.success('Request cloned!'); onClose(); @@ -172,8 +172,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js index 2646bf676..3f397c78c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js @@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions'; import { recursivelyGetAllItemUids } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; -const DeleteCollectionItem = ({ onClose, item, collection }) => { +const DeleteCollectionItem = ({ onClose, item, collectionUid }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const onConfirm = () => { - dispatch(deleteItem(item.uid, collection.uid)).then(() => { + dispatch(deleteItem(item.uid, collectionUid)).then(() => { if (isFolder) { // close all tabs that belong to the folder diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index 792736b12..42f0bc8ca 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -10,9 +10,11 @@ import { getLanguages } from 'utils/codegenerator/targets'; import { useSelector } from 'react-redux'; import { getGlobalEnvironmentVariables } from 'utils/collections/index'; -const GenerateCodeItem = ({ collection, item, onClose }) => { +const GenerateCodeItem = ({ collectionUid, item, onClose }) => { const languages = getLanguages(); + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 705c45c79..583a914b0 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -16,7 +16,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const RenameCollectionItem = ({ collection, item, onClose }) => { +const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { return; } if (!isFolder && item.draft) { - await dispatch(saveRequest(item.uid, collection.uid, true)); + await dispatch(saveRequest(item.uid, collectionUid, true)); } const { name: newName, filename: newFilename } = values; try { let renameConfig = { itemUid: item.uid, - collectionUid: collection.uid, + collectionUid, }; renameConfig['newName'] = newName; if (itemFilename !== newFilename) { @@ -191,8 +191,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index cfd236f8c..8aaaa749c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -2,16 +2,18 @@ import React from 'react'; import get from 'lodash/get'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions'; import { flattenItems } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; -const RunCollectionItem = ({ collection, item, onClose }) => { +const RunCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); + const onSubmit = (recursive) => { dispatch( addTab({ @@ -34,8 +36,6 @@ const RunCollectionItem = ({ collection, item, onClose }) => { const recursiveRunLength = getRequestsCount(flattenedItems); const isFolderLoading = areItemsLoading(item); - console.log(item); - console.log(isFolderLoading); return ( diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js index 8d61203e1..d47c820c3 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js @@ -22,6 +22,65 @@ const Wrapper = styled.div` height: 1.875rem; cursor: pointer; user-select: none; + position: relative; + + /* Common styles for drop indicators */ + &::before, + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: ${(props) => props.theme.dragAndDrop.border}; + opacity: 0; + pointer-events: none; + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } + + /* Drop target styles */ + &.drop-target { + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + + &::before, + &::after { + opacity: 0; + } + } + + &.drop-target-above { + &::before { + opacity: 1; + height: 2px; + } + } + + &.drop-target-below { + &::after { + opacity: 1; + height: 2px; + } + } + + /* Inside drop target style */ + &.drop-target { + &::before { + top: 0; + bottom: 0; + height: 100%; + opacity: 1; + background: ${(props) => props.theme.dragAndDrop.hoverBg}; + border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + // border-radius: 4px; + } + } .rotate-90 { transform: rotateZ(90deg); @@ -45,6 +104,20 @@ const Wrapper = styled.div` } } + &.item-target { + background: #ccc3; + } + + &.item-seperator { + .seperator { + bottom: 0px; + position: absolute; + height: 3px; + width: 100%; + background: #ccc3; + } + } + &.item-focused-in-tab { background: ${(props) => props.theme.sidebar.collection.item.bg}; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 9b520f4a5..92e025405 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, forwardRef, useEffect } from 'react'; +import React, { useState, useRef, forwardRef } from 'react'; import range from 'lodash/range'; import filter from 'lodash/filter'; import classnames from 'classnames'; @@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; -import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { handleCollectionItemDrop, moveItem, sendRequest, showInFolder, updateItemsSequences } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -26,13 +26,21 @@ import NetworkError from 'components/ResponsePane/NetworkError/index'; import CollectionItemInfo from './CollectionItemInfo/index'; import CollectionItemIcon from './CollectionItemIcon'; import { scrollToTheActiveTab } from 'utils/tabs'; +import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab'; +import { isEqual } from 'lodash'; -const CollectionItem = ({ item, collection, searchText }) => { - const tabs = useSelector((state) => state.tabs.tabs); - const activeTabUid = useSelector((state) => state.tabs.activeTabUid); +const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => { + const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid }); + const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual); + + const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid }); + const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual); + const isSidebarDragging = useSelector((state) => state.app.isDragging); const dispatch = useDispatch(); - const collectionItemRef = useRef(null); + + // We use a single ref for drag and drop. + const ref = useRef(null); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); @@ -44,9 +52,12 @@ const CollectionItem = ({ item, collection, searchText }) => { const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false); const hasSearchText = searchText && searchText?.trim()?.length; const itemIsCollapsed = hasSearchText ? false : item.collapsed; + const isFolder = isItemAFolder(item); + + const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside' const [{ isDragging }, drag] = useDrag({ - type: `collection-item-${collection.uid}`, + type: `collection-item-${collectionUid}`, item: item, collect: (monitor) => ({ isDragging: monitor.isDragging() @@ -56,21 +67,51 @@ const CollectionItem = ({ item, collection, searchText }) => { } }); - const [{ isOver }, drop] = useDrop({ - accept: `collection-item-${collection.uid}`, - drop: (draggedItem) => { - dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); + const determineDropType = (monitor) => { + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + if (!hoverBoundingRect || !clientOffset) return null; + + const clientY = clientOffset.y - hoverBoundingRect.top; + const folderUpperThreshold = hoverBoundingRect.height * 0.35; + const fileUpperThreshold = hoverBoundingRect.height * 0.5; + + if (isItemAFolder(item)) { + return clientY < folderUpperThreshold ? 'adjacent' : 'inside'; + } else { + return clientY < fileUpperThreshold ? 'adjacent' : null; + } + }; + + const [{ isOver, canDrop }, drop] = useDrop({ + accept: `collection-item-${collectionUid}`, + hover: (draggedItem, monitor) => { + const { uid: targetItemUid } = item; + const { uid: draggedItemUid } = draggedItem; + + if (draggedItemUid === targetItemUid) return; + + const dropType = determineDropType(monitor); + setDropType(dropType); }, - canDrop: (draggedItem) => { - return draggedItem.uid !== item.uid; + drop: async (draggedItem, monitor) => { + const { uid: targetItemUid } = item; + const { uid: draggedItemUid } = draggedItem; + + if (draggedItemUid === targetItemUid) return; + + const dropType = determineDropType(monitor); + if (!dropType) return; + + await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid })) + setDropType(null); }, + canDrop: (draggedItem) => draggedItem.uid !== item.uid, collect: (monitor) => ({ - isOver: monitor.isOver(), + isOver: monitor.isOver() }), }); - drag(drop(collectionItemRef)); - const dropdownTippyRef = useRef(); const MenuIcon = forwardRef((props, ref) => { return ( @@ -84,13 +125,15 @@ const CollectionItem = ({ item, collection, searchText }) => { 'rotate-90': !itemIsCollapsed }); - const itemRowClassName = classnames('flex collection-item-name items-center', { - 'item-focused-in-tab': item.uid == activeTabUid, - 'item-hovered': isOver + const itemRowClassName = classnames('flex collection-item-name relative items-center', { + 'item-focused-in-tab': isTabForItemActive, + 'item-hovered': isOver && canDrop, + 'drop-target': isOver && dropType === 'inside', + 'drop-target-above': isOver && dropType === 'adjacent' }); const handleRun = async () => { - dispatch(sendRequest(item, collection.uid)).catch((err) => + dispatch(sendRequest(item, collectionUid)).catch((err) => toast.custom((t) => toast.dismiss(t.id)} />, { duration: 5000 }) @@ -101,12 +144,10 @@ const CollectionItem = ({ item, collection, searchText }) => { if (event && event.detail != 1) return; //scroll to the active tab setTimeout(scrollToTheActiveTab, 50); - const isRequest = isItemARequest(item); - if (isRequest) { dispatch(hideHomePage()); - if (itemIsOpenedInTabs(item, tabs)) { + if (isTabForItemPresent) { dispatch( focusTab({ uid: item.uid @@ -114,11 +155,10 @@ const CollectionItem = ({ item, collection, searchText }) => { ); return; } - dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid, + collectionUid: collectionUid, requestPaneTab: getDefaultRequestPaneTab(item), type: 'request', }) @@ -127,14 +167,14 @@ const CollectionItem = ({ item, collection, searchText }) => { dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid, + collectionUid: collectionUid, type: 'folder-settings', }) ); dispatch( collectionFolderClicked({ itemUid: item.uid, - collectionUid: collection.uid + collectionUid: collectionUid }) ); } @@ -146,10 +186,10 @@ const CollectionItem = ({ item, collection, searchText }) => { dispatch( collectionFolderClicked({ itemUid: item.uid, - collectionUid: collection.uid + collectionUid: collectionUid }) ); - } + }; const handleRightClick = (event) => { const _menuDropdown = dropdownTippyRef.current; @@ -164,7 +204,6 @@ const CollectionItem = ({ item, collection, searchText }) => { let indents = range(item.depth); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const isFolder = isItemAFolder(item); const className = classnames('flex flex-col w-full', { 'is-sidebar-dragging': isSidebarDragging @@ -183,49 +222,14 @@ const CollectionItem = ({ item, collection, searchText }) => { } const handleDoubleClick = (event) => { - dispatch(makeTabPermanent({ uid: item.uid })) + dispatch(makeTabPermanent({ uid: item.uid })); }; - // we need to sort request items by seq property - const sortRequestItems = (items = []) => { + // Sort items by their "seq" property. + const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; - // we need to sort folder items by name alphabetically - const sortFolderItems = (items = []) => { - return items.sort((a, b) => a.name.localeCompare(b.name)); - }; - const handleGenerateCode = (e) => { - e.stopPropagation(); - dropdownTippyRef.current.hide(); - if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) { - setGenerateCodeItemModalOpen(true); - } else { - toast.error('URL is required'); - } - }; - - const viewFolderSettings = () => { - if (isItemAFolder(item)) { - if (itemIsOpenedInTabs(item, tabs)) { - dispatch( - focusTab({ - uid: item.uid - }) - ); - return; - } - dispatch( - addTab({ - uid: item.uid, - collectionUid: collection.uid, - type: 'folder-settings' - }) - ); - return; - } - }; - const handleShowInFolder = () => { dispatch(showInFolder(item.pathname)).catch((error) => { console.error('Error opening the folder', error); @@ -233,62 +237,89 @@ const CollectionItem = ({ item, collection, searchText }) => { }); }; - const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i))); - const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i))); + const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i))); + const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i))); + + const handleGenerateCode = (e) => { + e.stopPropagation(); + dropdownTippyRef.current.hide(); + if ( + (item?.request?.url !== '') || + (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '') + ) { + setGenerateCodeItemModalOpen(true); + } else { + toast.error('URL is required'); + } + }; + + const viewFolderSettings = () => { + if (isItemAFolder(item)) { + if (itemIsOpenedInTabs(item, tabs)) { + dispatch(focusTab({ uid: item.uid })); + return; + } + dispatch( + addTab({ + uid: item.uid, + collectionUid, + type: 'folder-settings' + }) + ); + } + }; return ( {renameItemModalOpen && ( - setRenameItemModalOpen(false)} /> + setRenameItemModalOpen(false)} /> )} {cloneItemModalOpen && ( - setCloneItemModalOpen(false)} /> + setCloneItemModalOpen(false)} /> )} {deleteItemModalOpen && ( - setDeleteItemModalOpen(false)} /> + setDeleteItemModalOpen(false)} /> )} {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {newFolderModalOpen && ( - setNewFolderModalOpen(false)} /> + setNewFolderModalOpen(false)} /> )} {runCollectionModalOpen && ( - setRunCollectionModalOpen(false)} /> + setRunCollectionModalOpen(false)} /> )} {generateCodeItemModalOpen && ( - setGenerateCodeItemModalOpen(false)} /> + setGenerateCodeItemModalOpen(false)} /> )} {itemInfoModalOpen && ( - setItemInfoModalOpen(false)} /> + setItemInfoModalOpen(false)} /> )} -
+
{ + ref.current = node; + drag(drop(node)); + }} + >
{indents && indents.length - ? indents.map((i) => { - return ( -
-  {/* Indent */} -
- ); - }) + ? indents.map((i) => ( +
+  {/* Indent */} +
+ )) : null}
{ /> ) : null}
- -
+
{item.name} @@ -429,17 +457,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
- {!itemIsCollapsed ? (
{folderItems && folderItems.length ? folderItems.map((i) => { - return ; + return ; }) : null} {requestItems && requestItems.length ? requestItems.map((i) => { - return ; + return ; }) : null}
@@ -448,4 +475,4 @@ const CollectionItem = ({ item, collection, searchText }) => { ); }; -export default CollectionItem; \ No newline at end of file +export default React.memo(CollectionItem); \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js index 9cba09179..17b6dc007 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js @@ -1,12 +1,14 @@ import React from 'react'; import toast from 'react-hot-toast'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { IconFiles } from '@tabler/icons'; import { removeCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { findCollectionByUid } from 'utils/collections/index'; -const RemoveCollection = ({ onClose, collection }) => { +const RemoveCollection = ({ onClose, collectionUid }) => { const dispatch = useDispatch(); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const onConfirm = () => { dispatch(removeCollection(collection.uid)) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js index a6e11051e..0d3a4c34a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js @@ -2,13 +2,15 @@ import React, { useRef, useEffect } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import toast from 'react-hot-toast'; import { renameCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { findCollectionByUid } from 'utils/collections/index'; -const RenameCollection = ({ collection, onClose }) => { +const RenameCollection = ({ collectionUid, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); + const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); const formik = useFormik({ enableReinitialize: true, initialValues: { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index 5c06cc42a..0378d9ad9 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -62,6 +62,36 @@ const Wrapper = styled.div` color: white; } } + + &.drop-target { + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + + &.drop-target-above { + border: none; + border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + margin-top: -2px; + background: transparent; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + + &.drop-target-below { + border: none; + border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + margin-bottom: -2px; + background: transparent; + transition: ${(props) => props.theme.dragAndDrop.transition}; + } + } + + .collection-name.drop-target { + border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + border-radius: 4px; + background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; + margin: -2px; + transition: ${(props) => props.theme.dragAndDrop.transition}; + box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg}; } #sidebar-collection-name { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index e81b43a1f..aebea5092 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -6,7 +6,7 @@ import { useDrop, useDrag } from 'react-dnd'; import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; -import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions'; +import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch, useSelector } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -33,7 +33,7 @@ const Collection = ({ collection, searchText }) => { const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); const collectionRef = useRef(null); - + const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); const MenuIcon = forwardRef((props, ref) => { @@ -144,7 +144,7 @@ const Collection = ({ collection, searchText }) => { drop: (draggedItem, monitor) => { const itemType = monitor.getItemType(); if (isCollectionItem(itemType)) { - dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)) + dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid })) } else { dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection})); } @@ -170,33 +170,28 @@ const Collection = ({ collection, searchText }) => { }); // we need to sort request items by seq property - const sortRequestItems = (items = []) => { + const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; - // we need to sort folder items by name alphabetically - const sortFolderItems = (items = []) => { - return items.sort((a, b) => a.name.localeCompare(b.name)); - }; - - const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i))); - const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i))); + const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i))); + const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i))); return ( - {showNewRequestModal && setShowNewRequestModal(false)} />} - {showNewFolderModal && setShowNewFolderModal(false)} />} + {showNewRequestModal && setShowNewRequestModal(false)} />} + {showNewFolderModal && setShowNewFolderModal(false)} />} {showRenameCollectionModal && ( - setShowRenameCollectionModal(false)} /> + setShowRenameCollectionModal(false)} /> )} {showRemoveCollectionModal && ( - setShowRemoveCollectionModal(false)} /> + setShowRemoveCollectionModal(false)} /> )} {showShareCollectionModal && ( - setShowShareCollectionModal(false)} /> + setShowShareCollectionModal(false)} /> )} {showCloneCollectionModalOpen && ( - setShowCloneCollectionModalOpen(false)} /> + setShowCloneCollectionModalOpen(false)} /> )}
{
{!collectionIsCollapsed ? (
- {folderItems && folderItems.length - ? folderItems.map((i) => { - return ; - }) - : null} - {requestItems && requestItems.length - ? requestItems.map((i) => { - return ; - }) - : null} + {folderItems?.map?.((i) => { + return ; + })} + {requestItems?.map?.((i) => { + return ; + })}
) : null}
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js index c0b39b727..83c243653 100644 --- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js @@ -14,7 +14,7 @@ import Dropdown from "components/Dropdown"; import { IconCaretDown } from "@tabler/icons"; import StyledWrapper from './StyledWrapper'; -const NewFolder = ({ collection, item, onClose }) => { +const NewFolder = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); const [isEditing, toggleEditing] = useState(false); @@ -52,7 +52,7 @@ const NewFolder = ({ collection, item, onClose }) => { }) }), onSubmit: (values) => { - dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null)) + dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null)) .then(() => { toast.success('New folder created!'); onClose(); diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 61fdcd22a..ec8f5dfda 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -5,7 +5,7 @@ import toast from 'react-hot-toast'; import path from 'utils/common/path'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections'; import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'; import { addTab } from 'providers/ReduxStore/slices/tabs'; @@ -20,9 +20,11 @@ import Portal from 'components/Portal'; import Help from 'components/Help'; import StyledWrapper from './StyledWrapper'; -const NewRequest = ({ collection, item, isEphemeral, onClose }) => { +const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); + + const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); const { brunoConfig: { presets: collectionPresets = {} } } = collection; @@ -135,14 +137,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, - collectionUid: collection.uid + collectionUid: collectionUid }) ) .then(() => { dispatch( addTab({ uid: uid, - collectionUid: collection.uid, + collectionUid: collectionUid, requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType }) }) ); @@ -158,7 +160,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: curlRequestTypeDetected, requestUrl: request.url, requestMethod: request.method, - collectionUid: collection.uid, + collectionUid: collectionUid, itemUid: item ? item.uid : null, headers: request.headers, body: request.body, @@ -178,7 +180,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, - collectionUid: collection.uid, + collectionUid: collectionUid, itemUid: item ? item.uid : null }) ) @@ -389,8 +391,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { ) : (
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index f6ac62bc9..86ad618aa 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -13,12 +13,9 @@ import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection, - getItemsToResequence, isItemAFolder, refreshUidsInItem, isItemARequest, - moveCollectionItem, - moveCollectionItemToRootOfCollection, transformRequestToSaveToFilesystem } from 'utils/collections'; import { uuid, waitForNextTick } from 'utils/common'; @@ -47,8 +44,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; -import { getGlobalEnvironmentVariables } from 'utils/collections/index'; -import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; +import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory } from 'utils/collections/index'; import { sanitizeName } from 'utils/common/regex'; import { safeParseJSON, safeStringifyJSON } from 'utils/common/index'; @@ -358,6 +354,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); + const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection; + const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i)); return new Promise((resolve, reject) => { if (!collection) { @@ -372,10 +370,27 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => if (!folderWithSameNameExists) { const fullName = path.join(collection.pathname, directoryName); const { ipcRenderer } = window; - ipcRenderer - .invoke('renderer:new-folder', fullName, folderName) - .then(() => resolve()) + .invoke('renderer:new-folder', fullName) + .then(async () => { + const folderData = { + name: folderName, + pathname: fullName, + root: { + meta: { + name: folderName, + seq: items?.length + 1 + } + } + }; + ipcRenderer + .invoke('renderer:save-folder-root', folderData) + .then(resolve) + .catch((err) => { + toast.error('Failed to save folder settings!'); + reject(err); + }); + }) .catch((error) => reject(error)); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); @@ -392,8 +407,26 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => const { ipcRenderer } = window; ipcRenderer - .invoke('renderer:new-folder', fullName, folderName) - .then(() => resolve()) + .invoke('renderer:new-folder', fullName) + .then(async () => { + const folderData = { + name: folderName, + pathname: fullName, + root: { + meta: { + name: folderName, + seq: items?.length + 1 + } + } + }; + ipcRenderer + .invoke('renderer:save-folder-root', folderData) + .then(resolve) + .catch((err) => { + toast.error('Failed to save folder settings!'); + reject(err); + }); + }) .catch((error) => reject(error)); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); @@ -495,7 +528,8 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp set(item, 'name', newName); set(item, 'filename', newFilename); set(item, 'root.meta.name', newName); - + set(item, 'root.meta.seq', parentFolder?.items?.length + 1); + const collectionPath = path.join(parentFolder.pathname, newFilename); ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject); return; @@ -594,176 +628,129 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { export const sortCollections = (payload) => (dispatch) => { dispatch(_sortCollections(payload)); }; -export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => { + +export const moveItem = ({ targetDirname, sourcePathname }) => (dispatch, getState) => { + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + + ipcRenderer.invoke('renderer:move-item', { targetDirname, sourcePathname }) + .then(resolve) + .catch(reject); + }); +} + +export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, collectionUid }) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); + const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; + const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; + const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection; + const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items); + const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection; + const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items); + const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType }) => { + const { pathname: targetItemPathname } = targetItem; + const { filename: draggedItemFilename } = draggedItem; + const targetItemDirname = path.dirname(targetItemPathname); + const isTargetTheCollection = targetItemPathname === collection.pathname; + const isTargetItemAFolder = isItemAFolder(targetItem); + + if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) { + return path.join(targetItemPathname, draggedItemFilename) + } else if (dropType === 'adjacent') { + return path.join(targetItemDirname, draggedItemFilename) + } + return null; + }; + + const handleMoveToNewLocation = async ({ draggedItem, draggedItemDirectoryItems, targetItem, targetItemDirectoryItems, newPathname, dropType }) => { + const { uid: targetItemUid } = targetItem; + const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem; + + const newDirname = path.dirname(newPathname); + await dispatch(moveItem({ + targetDirname: newDirname, + sourcePathname: draggedItemPathname + })); + + // Update sequences in the source directory + if (draggedItemDirectoryItems?.length) { + // reorder items in the source directory + const draggedItemDirectoryItemsWithoutDraggedItem = draggedItemDirectoryItems.filter(i => i.uid !== draggedItemUid); + const reorderedSourceItems = getReorderedItemsInSourceDirectory({ items: draggedItemDirectoryItemsWithoutDraggedItem }); + if (reorderedSourceItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedSourceItems })); + } + } + + // Update sequences in the target directory (if dropping adjacent) + if (dropType === 'adjacent') { + const targetItemSequence = targetItemDirectoryItems.findIndex(i => i.uid === targetItemUid)?.seq; + + const draggedItemWithNewPathAndSequence = { + ...draggedItem, + pathname: newPathname, + seq: targetItemSequence + }; + + // draggedItem is added to the targetItem's directory + const reorderedTargetItems = getReorderedItemsInTargetDirectory({ + items: [ ...targetItemDirectoryItems, draggedItemWithNewPathAndSequence ], + targetItemUid, + draggedItemUid + }); + + if (reorderedTargetItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedTargetItems })); + } + } + }; + + const handleReorderInSameLocation = async ({ draggedItem, targetItem, targetItemDirectoryItems }) => { + const { uid: targetItemUid } = targetItem; + const { uid: draggedItemUid } = draggedItem; + + // reorder items in the targetItem's directory + const reorderedItems = getReorderedItemsInTargetDirectory({ + items: targetItemDirectoryItems, + targetItemUid, + draggedItemUid + }); + + if (reorderedItems?.length) { + await dispatch(updateItemsSequences({ itemsToResequence: reorderedItems })); + } + }; + + return new Promise(async (resolve, reject) => { + try { + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType }); + if (!newPathname) return; + if (targetItemPathname?.startsWith(draggedItemPathname)) return; + if (newPathname !== draggedItemPathname) { + await handleMoveToNewLocation({ targetItem, targetItemDirectoryItems, draggedItem, draggedItemDirectoryItems, newPathname, dropType }); + } else { + await handleReorderInSameLocation({ draggedItem, targetItemDirectoryItems, targetItem }); + } + resolve(); + } catch (error) { + console.error(error); + toast.error(error?.message); + reject(error); + } + }) +} + +export const updateItemsSequences = ({ itemsToResequence }) => (dispatch, getState) => { return new Promise((resolve, reject) => { - if (!collection) { - return reject(new Error('Collection not found')); - } + const { ipcRenderer } = window; - const collectionCopy = cloneDeep(collection); - const draggedItem = findItemInCollection(collectionCopy, draggedItemUid); - const targetItem = findItemInCollection(collectionCopy, targetItemUid); - - if (!draggedItem) { - return reject(new Error('Dragged item not found')); - } - - if (!targetItem) { - return reject(new Error('Target item not found')); - } - - const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid); - const targetItemParent = findParentItemInCollection(collectionCopy, targetItemUid); - const sameParent = draggedItemParent === targetItemParent; - - // file item dragged onto another file item and both are in the same folder - // this is also true when both items are at the root level - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && sameParent) { - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:resequence-items', itemsToResequence) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged onto another file item which is at the root level - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !targetItemParent) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged onto another file item and both are in different folders - if (isItemARequest(draggedItem) && isItemARequest(targetItem) && !sameParent) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItemParent, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, targetItemParent.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // file item dragged into its own folder - if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) { - return resolve(); - } - - // file item dragged into another folder - if (isItemARequest(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) { - const draggedItemPathname = draggedItem.pathname; - moveCollectionItem(collectionCopy, draggedItem, targetItem); - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(targetItem, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, targetItem.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - - // end of the file drags, now let's handle folder drags - // folder drags are simpler since we don't allow ordering of folders - - // folder dragged into its own folder - if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent === targetItem) { - return resolve(); - } - - // folder dragged into a file which is at the same level - // this is also true when both items are at the root level - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && sameParent) { - return resolve(); - } - - // folder dragged into a file which is a child of the folder - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && draggedItem === targetItemParent) { - return resolve(); - } - - // folder dragged into a file which is at the root level - if (isItemAFolder(draggedItem) && isItemARequest(targetItem) && !targetItemParent) { - const draggedItemPathname = draggedItem.pathname; - - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname) - .then(resolve) - .catch((error) => reject(error)); - } - - // folder dragged into another folder - if (isItemAFolder(draggedItem) && isItemAFolder(targetItem) && draggedItemParent !== targetItem) { - const draggedItemPathname = draggedItem.pathname; - - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, targetItem.pathname) - .then(resolve) - .catch((error) => reject(error)); - } + ipcRenderer.invoke('renderer:resequence-items', itemsToResequence) + .then(resolve) + .catch(reject); }); -}; - -export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => { - const state = getState(); - const collection = findCollectionByUid(state.collections.collections, collectionUid); - - return new Promise((resolve, reject) => { - if (!collection) { - return reject(new Error('Collection not found')); - } - - const collectionCopy = cloneDeep(collection); - const draggedItem = findItemInCollection(collectionCopy, draggedItemUid); - if (!draggedItem) { - return reject(new Error('Dragged item not found')); - } - - const draggedItemParent = findParentItemInCollection(collectionCopy, draggedItemUid); - // file item is already at the root level - if (!draggedItemParent) { - return resolve(); - } - - const draggedItemPathname = draggedItem.pathname; - moveCollectionItemToRootOfCollection(collectionCopy, draggedItem); - - if (isItemAFolder(draggedItem)) { - return ipcRenderer - .invoke('renderer:move-folder-item', draggedItemPathname, collectionCopy.pathname) - .then(resolve) - .catch((error) => reject(error)); - } else { - const itemsToResequence = getItemsToResequence(draggedItemParent, collectionCopy); - const itemsToResequence2 = getItemsToResequence(collectionCopy, collectionCopy); - - return ipcRenderer - .invoke('renderer:move-file-item', draggedItemPathname, collectionCopy.pathname) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence)) - .then(() => ipcRenderer.invoke('renderer:resequence-items', itemsToResequence2)) - .then(resolve) - .catch((error) => reject(error)); - } - }); -}; +} export const newHttpRequest = (params) => (dispatch, getState) => { const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params; @@ -823,8 +810,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) ); - const requestItems = filter(collection.items, (i) => i.type !== 'folder'); - item.seq = requestItems.length + 1; + const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i)); + item.seq = items.length + 1; if (!reqWithSameNameExists) { const fullName = path.join(collection.pathname, resolvedFilename); @@ -852,8 +839,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { currentItem.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) ); - const requestItems = filter(currentItem.items, (i) => i.type !== 'folder'); - item.seq = requestItems.length + 1; + const items = filter(currentItem.items, (i) => isItemAFolder(i) || isItemARequest(i)); + item.seq = items.length + 1; if (!reqWithSameNameExists) { const fullName = path.join(currentItem.pathname, resolvedFilename); const { ipcRenderer } = window; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 5e8275ba1..d3098a936 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1719,6 +1719,9 @@ export const collectionsSlice = createSlice({ folderItem.name = file?.data?.meta?.name; } folderItem.root = file.data; + if (file?.data?.meta?.seq) { + folderItem.seq = file.data?.meta?.seq; + } } return; } @@ -1798,6 +1801,7 @@ export const collectionsSlice = createSlice({ uid: uuid(), pathname: currentPath, name: dir?.meta?.name || directoryName, + seq: dir?.meta?.seq || 1, filename: directoryName, collapsed: true, type: 'folder', @@ -1829,6 +1833,9 @@ export const collectionsSlice = createSlice({ if (file?.data?.meta?.name) { folderItem.name = file?.data?.meta?.name; } + if (file?.data?.meta?.seq) { + folderItem.seq = file?.data?.meta?.seq; + } folderItem.root = file.data; } return; diff --git a/packages/bruno-app/src/selectors/tab.js b/packages/bruno-app/src/selectors/tab.js new file mode 100644 index 000000000..76aa67365 --- /dev/null +++ b/packages/bruno-app/src/selectors/tab.js @@ -0,0 +1,9 @@ +import { createSelector } from '@reduxjs/toolkit'; + +export const isTabForItemActive = ({ itemUid }) => createSelector([ + (state) => state.tabs?.activeTabUid +], (activeTabUid) => activeTabUid === itemUid); + +export const isTabForItemPresent = ({ itemUid }) => createSelector([ + (state) => state.tabs.tabs, +], (tabs) => tabs.some((tab) => tab.uid === itemUid)); \ No newline at end of file diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 861290981..04ee6134e 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -281,6 +281,12 @@ const darkTheme = { color: 'rgb(52 51 49)' }, + dragAndDrop: { + border: '#666666', + borderStyle: '2px solid', + hoverBg: 'rgba(102, 102, 102, 0.08)', + transition: 'all 0.1s ease' + }, infoTip: { bg: '#1f1f1f', border: '#333333', diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index 6ce9fa583..e95b0e45e 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -282,6 +282,12 @@ const lightTheme = { color: 'rgb(152 151 149)' }, + dragAndDrop: { + border: '#8b8b8b', // Using the same gray as focusBorder from input + borderStyle: '2px solid', + hoverBg: 'rgba(139, 139, 139, 0.05)', // Matching the border color with reduced opacity + transition: 'all 0.1s ease' + }, infoTip: { bg: 'white', border: '#e0e0e0', diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index e258c80ba..add047119 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -98,6 +98,14 @@ export const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; +export const findParentItemInCollectionByPathname = (collection, pathname) => { + let flattenedItems = flattenItems(collection.items); + + return find(flattenedItems, (item) => { + return item.items && find(item.items, (i) => i.pathname === pathname); + }); +}; + export const findItemInCollection = (collection, itemUid) => { let flattenedItems = flattenItems(collection.items); @@ -150,90 +158,6 @@ export const getItemsLoadStats = (folder) => { }; } -export const moveCollectionItem = (collection, draggedItem, targetItem) => { - let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); - - if (draggedItemParent) { - draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq); - draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); - draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename); - } else { - collection.items = sortBy(collection.items, (item) => item.seq); - collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid); - } - - if (targetItem.type === 'folder') { - targetItem.items = sortBy(targetItem.items || [], (item) => item.seq); - targetItem.items.push(draggedItem); - draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename); - } else { - let targetItemParent = findParentItemInCollection(collection, targetItem.uid); - - if (targetItemParent) { - targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq); - let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid); - targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem); - draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename); - } else { - collection.items = sortBy(collection.items, (item) => item.seq); - let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid); - collection.items.splice(targetItemIndex + 1, 0, draggedItem); - draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); - } - } -}; - -export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => { - let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); - - // If the dragged item is already at the root of the collection, do nothing - if (!draggedItemParent) { - return; - } - - draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq); - draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); - collection.items = sortBy(collection.items, (item) => item.seq); - collection.items.push(draggedItem); - if (draggedItem.type == 'folder') { - draggedItem.pathname = path.join(collection.pathname, draggedItem.name); - } else { - draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); - } -}; - -export const getItemsToResequence = (parent, collection) => { - let itemsToResequence = []; - - if (!parent) { - let index = 1; - each(collection.items, (item) => { - if (isItemARequest(item)) { - itemsToResequence.push({ - pathname: item.pathname, - seq: index++ - }); - } - }); - return itemsToResequence; - } - - if (parent.items && parent.items.length) { - let index = 1; - each(parent.items, (item) => { - if (isItemARequest(item)) { - itemsToResequence.push({ - pathname: item.pathname, - seq: index++ - }); - } - }); - return itemsToResequence; - } - - return itemsToResequence; -}; - export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => { const copyHeaders = (headers) => { return map(headers, (header) => { @@ -502,6 +426,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} if (meta?.name) { di.root.meta = {}; di.root.meta.name = meta?.name; + di.root.meta.seq = meta?.seq; } if (!Object.keys(di.root.request)?.length) { delete di.root.request; @@ -1086,3 +1011,62 @@ export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }); return credentialsVariables; }; + + +// item sequence utils - START + +export const resetSequencesInFolder = (folderItems) => { + const items = folderItems; + const sortedItems = items.sort((a, b) => a.seq - b.seq); + return sortedItems.map((item, index) => { + item.seq = index + 1; + return item; + }); +}; + +export const isItemBetweenSequences = (itemSequence, sourceItemSequence, targetItemSequence) => { + if (targetItemSequence > sourceItemSequence) { + return itemSequence > sourceItemSequence && itemSequence < targetItemSequence; + } + return itemSequence < sourceItemSequence && itemSequence >= targetItemSequence; +}; + +export const calculateNewSequence = (isDraggedItem, targetSequence, draggedSequence) => { + if (!isDraggedItem) { + return null; + } + return targetSequence > draggedSequence ? targetSequence - 1 : targetSequence; +}; + +export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, draggedItemUid }) => { + const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items)); + const targetItem = findItem(itemsWithFixedSequences, targetItemUid); + const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid); + const targetSequence = targetItem?.seq; + const draggedSequence = draggedItem?.seq; + itemsWithFixedSequences?.forEach(item => { + const isDraggedItem = item?.uid === draggedItemUid; + const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + if (isBetween) { + item.seq += targetSequence > draggedSequence ? -1 : 1; + } + const newSequence = calculateNewSequence(isDraggedItem, targetSequence, draggedSequence); + if (newSequence !== null) { + item.seq = newSequence; + } + }); + // only return items that have been reordered + return itemsWithFixedSequences.filter(item => + items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq + ); +}; + +export const getReorderedItemsInSourceDirectory = ({ items }) => { + const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items)); + return itemsWithFixedSequences.filter(item => + items?.find(originalItem => originalItem?.uid === item?.uid)?.seq !== item?.seq + ); +}; + +// item sequence utils - END + diff --git a/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js b/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js new file mode 100644 index 000000000..adfb5dab9 --- /dev/null +++ b/packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js @@ -0,0 +1,126 @@ +import { resetSequencesInFolder, isItemBetweenSequences } from 'utils/collections/index'; + +describe('resetSequencesInFolder', () => { + it('should fix the sequences in the folder 1', () => { + const folder = { + items: [ + { uid: '1', seq: 1 }, + { uid: '2', seq: 3 }, + { uid: '3', seq: 6 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '1', seq: 1 }, + { uid: '2', seq: 2 }, + { uid: '3', seq: 3 }, + ]); + }); + + + it('should fix the sequences in the folder 2', () => { + const folder = { + items: [ + { uid: '1', seq: 3 }, + { uid: '2', seq: 1 }, + { uid: '3', seq: 2 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '2', seq: 1 }, + { uid: '3', seq: 2 }, + { uid: '1', seq: 3 }, + ]); + }); + + it('should fix the sequences in the folder with missing sequences', () => { + const folder = { + items: [ + { uid: '1', seq: 1 }, + { uid: '2', type: 'folder' }, + { uid: '3', type: 'folder' }, + { uid: '4', seq: 7 }, + ] + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '1', seq: 1 }, + { uid: '2', seq: 2, type: 'folder' }, + { uid: '3', seq: 3, type: 'folder' }, + { uid: '4', seq: 4 }, + ]); + }); + + it('should fix the sequences in the folder with same sequences', () => { + const folder = { + items: [ + { uid: '1', seq: 2 }, + { uid: '2', seq: 2 }, + { uid: '3', seq: 3 }, + { uid: '4', seq: 1 }, + ], + }; + + const fixedFolder = resetSequencesInFolder(folder.items); + expect(fixedFolder).toEqual([ + { uid: '4', seq: 1 }, + { uid: '1', seq: 2 }, + { uid: '2', seq: 3 }, + { uid: '3', seq: 4 }, + ]); + }); +}); + +describe('isItemBetweenSequences', () => { + it('should return true if the item is between the sequences 1', () => { + const item = { uid: '1', seq: 2 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 2', () => { + const item = { uid: '1', seq: 2 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 3', () => { + const item = { uid: '1', seq: 4 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return true if the item is between the sequences 4', () => { + const item = { uid: '1', seq: 1 }; + const draggedSequence = 5; + const targetSequence = 1; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(true); + }); + + it('should return false if the item is between the sequences 1', () => { + const item = { uid: '1', seq: 1 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(false); + }); + + it('should return false if the item is between the sequences 2', () => { + const item = { uid: '1', seq: 5 }; + const draggedSequence = 1; + const targetSequence = 5; + const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence); + expect(result).toBe(false); + }); +}); diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 31b2f3929..6eb832c24 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -252,7 +252,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { const requestMap = {}; const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] - each(item, (i) => { + each(item, (i, index) => { if (isItemAFolder(i)) { const baseFolderName = i.name || 'Untitled Folder'; let folderName = baseFolderName; @@ -268,6 +268,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { name: folderName, type: 'folder', items: [], + seq: index + 1, root: { docs: i.description || '', meta: { @@ -332,6 +333,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => { uid: uuid(), name: requestName, type: 'http-request', + seq: index + 1, request: { url: url, method: i?.request?.method?.toUpperCase(), 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 7eac3906c..f8f52538e 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 @@ -73,92 +73,93 @@ const expectedOutput = { "version": "1", "items": [ { - "uid": "mockeduuidvalue123456", - "name": "folder", - "type": "folder", - "items": [ - { - "uid": "mockeduuidvalue123456", - "name": "request", - "type": "http-request", - "request": { - "url": "https://usebruno.com", - "method": "GET", - "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": "" - }, - "seq": 1 - } - ], - "root": { - "docs": "", - "meta": { - "name": "folder" - }, - "request": { - "auth": { - "mode": "none", - "basic": null, - "bearer": null, - "awsv4": null, - "apikey": null, - "oauth2": null, - "digest": null - }, - "headers": [], - "script": {}, - "tests": "", - "vars": {} - } - } + "uid": "mockeduuidvalue123456", + "name": "folder", + "type": "folder", + "seq": 1, + "items": [ + { + "uid": "mockeduuidvalue123456", + "name": "request", + "type": "http-request", + "seq": 1, + "request": { + "url": "https://usebruno.com", + "method": "GET", + "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": "" + } + } + ], + "root": { + "docs": "", + "meta": { + "name": "folder" + }, + "request": { + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "awsv4": null, + "apikey": null, + "oauth2": null, + "digest": null + }, + "headers": [], + "script": {}, + "tests": "", + "vars": {} + } + } }, { - "uid": "mockeduuidvalue123456", - "name": "request", - "type": "http-request", - "request": { - "url": "https://usebruno.com", - "method": "GET", - "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": "" - }, - "seq": 1 + "uid": "mockeduuidvalue123456", + "name": "request", + "type": "http-request", + "seq": 2, + "request": { + "url": "https://usebruno.com", + "method": "GET", + "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": "" + }, } ], "environments": [], diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 3ee646e81..8cd3db22d 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -220,7 +220,6 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread } } - // Is this a folder.bru file? if (path.basename(pathname) === 'folder.bru') { const file = { meta: { @@ -327,16 +326,25 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { } let name = path.basename(pathname); + let seq = 1; + const folderBruFilePath = path.join(pathname, `folder.bru`); + + if (fs.existsSync(folderBruFilePath)) { + let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); + let folderBruData = await collectionBruToJson(folderBruFileContent); + name = folderBruData?.meta?.name || name; + seq = folderBruData?.meta?.seq || seq; + } const directory = { meta: { collectionUid, pathname, - name + name, + seq } }; - win.webContents.send('main:collection-tree-updated', 'addDir', directory); }; diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index d41c980d7..946d95519 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -29,9 +29,11 @@ const collectionBruToJson = async (data, parsed = false) => { // add meta if it exists // this is only for folder bru file // in the future, all of this will be replaced by standard bru lang - if (json.meta) { + const sequence = _.get(json, 'meta.seq'); + if (json?.meta) { transformedJson.meta = { - name: json.meta.name + name: json.meta.name, + seq: !isNaN(sequence) ? Number(sequence) : 1 }; } @@ -61,9 +63,11 @@ const jsonToCollectionBru = async (json, isFolder) => { // add meta if it exists // this is only for folder bru file // in the future, all of this will be replaced by standard bru lang + const sequence = _.get(json, 'meta.seq'); if (json?.meta) { collectionBruJson.meta = { - name: json.meta.name + name: json.meta.name, + seq: !isNaN(sequence) ? Number(sequence) : 1 }; } diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 0aaf87d55..97742928c 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const fs = require('fs'); +const fsPromises = require('fs/promises'); const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); @@ -22,7 +23,9 @@ const { hasSubDirectories, getCollectionStats, sizeInMB, - safeWriteFileSync + safeWriteFileSync, + copyPath, + removePath } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -32,11 +35,10 @@ const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); const interpolateVars = require('./network/interpolate-vars'); -const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection'); +const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection'); const { getProcessEnvVars } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); const { getCertsAndProxyConfig } = require('./network'); -const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); @@ -192,12 +194,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:save-folder-root', async (event, folder) => { try { - const { name: folderName, root: folderRoot, pathname: folderPathname } = folder; + const { name: folderName, root: folderRoot = {}, pathname: folderPathname } = folder; const folderBruFilePath = path.join(folderPathname, 'folder.bru'); - folderRoot.meta = { - name: folderName - }; + if (!folderRoot.meta) { + folderRoot.meta = { + name: folderName, + seq: 1 + }; + } const content = await jsonToCollectionBru( folderRoot, @@ -376,14 +381,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent.meta.name = newName; } else { - folderBruFileJsonContent = {}; + folderBruFileJsonContent = { + meta: { + name: newName, + seq: 1 + } + }; } - - folderBruFileJsonContent.meta = { - name: newName, - }; - + const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); @@ -425,14 +432,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent.meta.name = newName; } else { - folderBruFileJsonContent = {}; + folderBruFileJsonContent = { + meta: { + name: newName, + seq: 1 + } + }; } - folderBruFileJsonContent.meta = { - name: newName, - }; - const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); @@ -512,6 +521,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection let data = { meta: { name: folderName, + seq: 1 } }; const content = await jsonToCollectionBru(data, true); // isFolder flag @@ -598,6 +608,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (item?.root?.meta?.name) { const folderBruFilePath = path.join(folderPath, 'folder.bru'); + item.root.meta.seq = item.seq; const folderContent = await jsonToCollectionBru( item.root, true // isFolder @@ -731,17 +742,42 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => { try { - for await (let item of itemsToResequence) { - const bru = fs.readFileSync(item.pathname, 'utf8'); - const jsonData = await bruToJsonViaWorker(bru); - - if (jsonData.seq !== item.seq) { - jsonData.seq = item.seq; - const content = await jsonToBruViaWorker(jsonData); - await writeFile(item.pathname, content); + for (let item of itemsToResequence) { + if (item?.type === 'folder') { + const folderRootPath = path.join(item.pathname, 'folder.bru'); + let folderBruJsonData = { + meta: { + name: path.basename(item?.pathname), + seq: item?.seq || 1 + } + }; + if (fs.existsSync(folderRootPath)) { + const bru = fs.readFileSync(folderRootPath, 'utf8'); + folderBruJsonData = await collectionBruToJson(bru); + if (!folderBruJsonData?.meta) { + folderBruJsonData.meta = { + name: path.basename(item?.pathname), + seq: item?.seq || 1 + }; + } + if (folderBruJsonData?.meta?.seq === item.seq) { + continue; + } + folderBruJsonData.meta.seq = item.seq; + } + const content = await jsonToCollectionBru(folderBruJsonData); + await writeFile(folderRootPath, content); + } else { + if (fs.existsSync(item.pathname)) { + const itemToSave = transformRequestToSaveToFilesystem(item); + const content = await jsonToBruViaWorker(itemToSave); + await writeFile(item.pathname, content); + } } } + return true; } catch (error) { + console.error('Error in resequence-items:', error); return Promise.reject(error); } }); @@ -760,6 +796,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => { + try { + if (fs.existsSync(targetDirname)) { + await copyPath(sourcePathname, targetDirname); + await removePath(sourcePathname); + } + } catch (error) { + return Promise.reject(error); + } + }); + ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => { try { const folderName = path.basename(folderPath); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8fddd2d98..28a49e80f 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1255,6 +1255,7 @@ const registerNetworkIpc = (mainWindow) => { folderUid }); } catch (error) { + console.log("error", error); deleteCancelToken(cancelTokenUid); mainWindow.webContents.send('main:run-folder-event', { type: 'testrun-ended', diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index d6fed9da6..94fa30ec8 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -1,4 +1,4 @@ -const { get, each, find, compact, filter } = require('lodash'); +const { get, each, find, compact, isString, filter } = require('lodash'); const fs = require('fs'); const { getRequestUid } = require('../cache/requestUids'); const { uuid } = require('./common'); @@ -205,6 +205,14 @@ const findParentItemInCollection = (collection, itemUid) => { }); }; +const findParentItemInCollectionByPathname = (collection, pathname) => { + let flattenedItems = flattenItems(collection.items); + + return find(flattenedItems, (item) => { + return item.items && find(item.items, (i) => i.pathname === pathname); + }); +}; + const getTreePathFromCollectionToItem = (collection, _item) => { let path = []; let item = findItemInCollection(collection, _item.uid); @@ -272,12 +280,73 @@ const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; +const replaceTabsWithSpaces = (str, numSpaces = 2) => { + if (!str || !str.length || !isString(str)) { + return ''; + } + + return str.replaceAll('\t', ' '.repeat(numSpaces)); +}; + +const transformRequestToSaveToFilesystem = (item) => { + const _item = item.draft ? item.draft : item; + const itemToSave = { + uid: _item.uid, + type: _item.type, + name: _item.name, + seq: _item.seq, + request: { + method: _item.request.method, + url: _item.request.url, + params: [], + headers: [], + auth: _item.request.auth, + body: _item.request.body, + script: _item.request.script, + vars: _item.request.vars, + assertions: _item.request.assertions, + tests: _item.request.tests, + docs: _item.request.docs + } + }; + + each(_item.request.params, (param) => { + itemToSave.request.params.push({ + uid: param.uid, + name: param.name, + value: param.value, + description: param.description, + type: param.type, + enabled: param.enabled + }); + }); + + each(_item.request.headers, (header) => { + itemToSave.request.headers.push({ + uid: header.uid, + name: header.name, + value: header.value, + description: header.description, + enabled: header.enabled + }); + }); + + if (itemToSave.request.body.mode === 'json') { + itemToSave.request.body = { + ...itemToSave.request.body, + json: replaceTabsWithSpaces(itemToSave.request.body.json) + }; + } + + return itemToSave; +} + const sortCollection = (collection) => { const items = collection.items || []; let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + folderItems = folderItems.sort((a, b) => a.seq - b.seq); requestItems = requestItems.sort((a, b) => a.seq - b.seq); collection.items = folderItems.concat(requestItems); @@ -292,7 +361,7 @@ const sortFolder = (folder = {}) => { let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + folderItems = folderItems.sort((a, b) => a.seq - b.seq); requestItems = requestItems.sort((a, b) => a.seq - b.seq); folder.items = folderItems.concat(requestItems); @@ -410,11 +479,13 @@ module.exports = { findItemByPathname, findItemInCollectionByPathname, findParentItemInCollection, + findParentItemInCollectionByPathname, parseBruFileMeta, + hydrateRequestWithUuid, + transformRequestToSaveToFilesystem, sortCollection, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, - getFormattedCollectionOauth2Credentials, - hydrateRequestWithUuid + getFormattedCollectionOauth2Credentials }; \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index a95c116eb..f00734c7e 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -282,6 +282,48 @@ function safeWriteFileSync(filePath, data) { fs.writeFileSync(safePath, data); } +// Recursively copies a source to a destination . +const copyPath = async (source, destination) => { + let targetPath = `${destination}/${path.basename(source)}`; + + const targetPathExists = await fsPromises.access(targetPath).then(() => true).catch(() => false); + if (targetPathExists) { + throw new Error(`Cannot copy, ${path.basename(source)} already exists in ${path.basename(destination)}`); + } + + const copy = async (source, destination) => { + const stat = await fsPromises.lstat(source); + if (stat.isDirectory()) { + await fsPromises.mkdir(destination, { recursive: true }); + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const srcPath = path.join(source, entry); + const destPath = path.join(destination, entry); + await copy(srcPath, destPath); + } + } else { + await fsPromises.copyFile(source, destination); + } + } + + await copy(source, targetPath); +} + +// Recursively removes a source . +const removePath = async (source) => { + const stat = await fsPromises.lstat(source); + if (stat.isDirectory()) { + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const entryPath = path.join(source, entry); + await removePath(entryPath); + } + await fsPromises.rmdir(source); + } else { + await fsPromises.unlink(source); + } +} + module.exports = { isValidPathname, exists, @@ -308,5 +350,7 @@ module.exports = { getCollectionStats, sizeInMB, safeWriteFile, - safeWriteFileSync + safeWriteFileSync, + copyPath, + removePath }; diff --git a/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js b/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js new file mode 100644 index 000000000..60add1b57 --- /dev/null +++ b/packages/bruno-electron/src/utils/tests/filesystem/index.spec.js @@ -0,0 +1,116 @@ +const path = require('path'); +const fs = require('fs/promises'); +const os = require('os'); +const { copyPath, removePath } = require('../../filesystem'); +const { initialCollectionStructure, finalCollectionStructure } = require('../fixtures/filesystem/copypath-removepath'); + +describe('File System Operations', () => { + let tempDir; + + beforeAll(async () => { + // Create a temporary directory for each test + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bruno-test-')); + await createFilesAndFolders(tempDir, initialCollectionStructure); + const result = await verifyFilesAndFolders(tempDir, initialCollectionStructure); + expect(result).toBe(true); + }); + + afterAll(async () => { + // clean up after each test + await fs.rm(tempDir, { recursive: true, force: true }); + // confirm the temp directory is deleted + expect(await fs.access(tempDir).then(() => true).catch(() => false)).toBe(false); + }); + + describe('copyPath and removePath', () => { + it('should move files and folder items multiple times', async () => { + + { + const sourcePath = path.join(tempDir, 'folder_1', 'file_2.bru'); + const destDir = path.join(tempDir, 'folder_1', 'folder_1_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_2'); + const destDir = path.join(tempDir, 'folder_1', 'folder_1_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'file_2_2.bru'); + const destDir = path.join(tempDir, 'folder_1'); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + { + const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'folder_2_1'); + const destDir = path.join(tempDir); + await copyPath(sourcePath, destDir); + await removePath(sourcePath); + } + + const result = await verifyFilesAndFolders(tempDir, finalCollectionStructure); + expect(result).toBe(true); + }); + + + it('should throw an error move file/folder if the destination has the same filename', async () => { + { + const sourcePath = path.join(tempDir, 'folder_1', 'file_dup.bru'); + const destDir = path.join(tempDir, 'folder_1'); + await expect(copyPath(sourcePath, destDir)).rejects.toThrow(); + } + }); + + }); +}); + + +// create folders and files recursively based on the defined json structure +const createFilesAndFolders = async (dir, filesAndFolders) => { + for (const item of filesAndFolders) { + const itemPath = path.join(dir, item.name); + if (item.type === 'folder') { + await fs.mkdir(itemPath, { recursive: true }); + await createFilesAndFolders(itemPath, item.files); + } else { + await fs.writeFile(itemPath, item.content); + } + } +} + +// if a file/folder doesnt exist, return false +// should only contain files and folders that are defined in the json structure +const verifyFilesAndFolders = async (dir, filesAndFolders) => { + const verify = async (dir, filesAndFolders) => { + const files = await fs.readdir(dir); + if (files.length !== filesAndFolders.length) { + return false; + } + for (const file of files) { + const itemPath = path.join(dir, file); + const item = filesAndFolders.find(f => f.name === file); + if (!item) { + return false; + } + if (item.type === 'folder') { + return await verify(itemPath, item.files); + } else { + return await fs.readFile(itemPath, 'utf8').then(content => content === item.content); + } + } + return true; + } + + try { + const verified = await verify(dir, filesAndFolders); + return verified; + } catch (error) { + console.error(error); + return false; + } +} \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js b/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js new file mode 100644 index 000000000..ea08f8d25 --- /dev/null +++ b/packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js @@ -0,0 +1,155 @@ +const initialCollectionStructure = [ + { + "name": "folder_1", + "type": "folder", + "files": [ + { + "name": "file_1.bru", + "type": "file", + "content": "file_1_content" + }, + { + "name": "file_2.bru", + "type": "file", + "content": "file_2_content" + }, + { + "name": "folder_1_1", + "type": "folder", + "files": [ + { + "name": "file_1_1.bru", + "type": "file", + "content": "file_1_1_content" + }, + { + "name": "file_1_2.bru", + "type": "file", + "content": "file_1_2_content" + } + ] + }, + { + "name": "file_1_3.bru", + "type": "file", + "content": "file_1_3_content" + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } + ], + }, + { + "name": "folder_2", + "type": "folder", + "files": [ + { + "name": "file_2_1.bru", + "type": "file", + "content": "file_2_1_content" + }, + { + "name": "file_2_2.bru", + "type": "file", + "content": "file_2_2_content" + }, + { + "name": "folder_2_1", + "type": "folder", + "files": [ + { + "name": "file_2_1_1.bru", + "type": "file", + "content": "file_2_1_1_content" + } + ] + } + ] + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } +]; + +const finalCollectionStructure = [ + { + "name": "folder_1", + "type": "folder", + "files": [ + { + "name": "file_1.bru", + "type": "file", + "content": "file_1_content" + }, + { + "name": "folder_1_1", + "type": "folder", + "files": [ + { + "name": "file_1_1.bru", + "type": "file", + "content": "file_1_1_content" + }, + { + "name": "file_1_2.bru", + "type": "file", + "content": "file_1_2_content" + }, + { + "name": "file_2.bru", + "type": "file", + "content": "file_2_content" + }, + { + "name": "folder_2", + "type": "folder", + "files": [ + { + "name": "file_2_1.bru", + "type": "file", + "content": "file_2_1_content" + } + ] + } + ] + }, + { + "name": "file_1_3.bru", + "type": "file", + "content": "file_1_3_content" + }, + { + "name": "file_2_2.bru", + "type": "file", + "content": "file_2_2_content" + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } + ], + }, + { + "name": "folder_2_1", + "type": "folder", + "files": [ + { + "name": "file_2_1_1.bru", + "type": "file", + "content": "file_2_1_1_content" + } + ] + }, + { + "name": "file_dup.bru", + "type": "file", + "content": "file_dup_content" + } +]; + +module.exports = { initialCollectionStructure, finalCollectionStructure }; \ No newline at end of file diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3914e6bfa..af4b13434 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -340,7 +340,8 @@ const folderRootSchema = Yup.object({ .nullable(), docs: Yup.string().nullable(), meta: Yup.object({ - name: Yup.string().nullable() + name: Yup.string().nullable(), + seq: Yup.number().min(1).nullable() }) .noUnknown(true) .strict() From 9e2982101288e77f627034e7f8fbd29f07e78f2a Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Tue, 6 May 2025 17:56:34 +0530 Subject: [PATCH 785/904] feat: extend support for more auth in folder level --- .../components/FolderSettings/Auth/index.js | 91 +++++++++++++++++++ .../FolderSettings/AuthMode/index.js | 72 +++++++++++++++ .../RequestPane/Auth/BasicAuth/index.js | 9 +- .../RequestPane/Auth/BearerAuth/index.js | 12 ++- .../src/components/RequestPane/Auth/index.js | 16 +++- .../CollectionItem/GenerateCodeItem/index.js | 67 +++++++++++--- .../ReduxStore/slices/collections/index.js | 21 +++++ 7 files changed, 267 insertions(+), 21 deletions(-) diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 360d5c64f..27cd03380 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -9,6 +9,13 @@ import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/Passwo import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index'; import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index'; import AuthMode from '../AuthMode'; +import BasicAuth from 'components/RequestPane/Auth/BasicAuth'; +import BearerAuth from 'components/RequestPane/Auth/BearerAuth'; +import DigestAuth from 'components/RequestPane/Auth/DigestAuth'; +import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth'; +import WsseAuth from 'components/RequestPane/Auth/WsseAuth'; +import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth'; +import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth'; const GrantTypeComponentMap = ({ collection, folder }) => { const dispatch = useDispatch(); @@ -43,6 +50,83 @@ const Auth = ({ collection, folder }) => { const getAuthView = () => { switch (authMode) { + case 'basic': { + return ( + handleSave()} + /> + ); + } + case 'bearer': { + return ( + handleSave()} + /> + ); + } + case 'digest': { + return ( + handleSave()} + /> + ); + } + case 'ntlm': { + return ( + handleSave()} + /> + ); + } + case 'wsse': { + return ( + handleSave()} + /> + ); + } + case 'apikey': { + return ( + handleSave()} + /> + ); + } + case 'awsv4': { + return ( + handleSave()} + /> + ); + } case 'oauth2': { return ( <> @@ -56,6 +140,13 @@ const Auth = ({ collection, folder }) => { ); } + case 'inherit': { + return ( +
+ Authentication settings will be inherited from the collection. +
+ ); + } case 'none': { return null; } diff --git a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js index e6e48f110..36377973a 100644 --- a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js +++ b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js @@ -35,6 +35,51 @@ const AuthMode = ({ collection, folder }) => {
} placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + onModeChange('awsv4'); + }} + > + AWS Sig v4 +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('basic'); + }} + > + Basic Auth +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('bearer'); + }} + > + Bearer Token +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('digest'); + }} + > + Digest Auth +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('ntlm'); + }} + > + NTLM Auth +
{ @@ -44,6 +89,33 @@ const AuthMode = ({ collection, folder }) => { > OAuth 2.0
+
{ + dropdownTippyRef.current.hide(); + onModeChange('wsse'); + }} + > + WSSE Auth +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('apikey'); + }} + > + API Key +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('inherit'); + }} + > + Inherit +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js index 8582a53cd..ef714f528 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js @@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -const BasicAuth = ({ item, collection }) => { +const BasicAuth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {}); + const basicAuth = get(request, 'auth.basic', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleUsernameChange = (username) => { dispatch( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js index bef4d062a..c8ba9d1c6 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js @@ -7,16 +7,18 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -const BearerAuth = ({ item, collection }) => { +const BearerAuth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const bearerToken = item.draft - ? get(item, 'draft.request.auth.bearer.token', '') - : get(item, 'request.auth.bearer.token', ''); + // Use the request prop directly like OAuth2ClientCredentials does + const bearerToken = get(request, 'auth.bearer.token', ''); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleTokenChange = (token) => { dispatch( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 4cb8897d3..a1469675a 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -7,6 +7,8 @@ import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; import WsseAuth from './WsseAuth'; import NTLMAuth from './NTLMAuth'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import ApiKeyAuth from './ApiKeyAuth'; import StyledWrapper from './StyledWrapper'; @@ -27,6 +29,16 @@ const getTreePathFromCollectionToItem = (collection, _item) => { const Auth = ({ item, collection }) => { const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); const requestTreePath = getTreePathFromCollectionToItem(collection, item); + + // Create a request object to pass to the auth components + const request = item.draft + ? get(item, 'draft.request', {}) + : get(item, 'request', {}); + + // Save function for request level + const save = () => { + return saveRequest(item.uid, collection.uid); + }; const getEffectiveAuthSource = () => { if (authMode !== 'inherit') return null; @@ -62,10 +74,10 @@ const Auth = ({ item, collection }) => { return ; } case 'basic': { - return ; + return ; } case 'bearer': { - return ; + return ; } case 'digest': { return ; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index 42f0bc8ca..5c5640ca0 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -4,12 +4,60 @@ import CodeView from './CodeView'; import StyledWrapper from './StyledWrapper'; import { isValidUrl } from 'utils/url'; import { get } from 'lodash'; -import { findEnvironmentInCollection } from 'utils/collections'; +import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections'; import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index'; import { getLanguages } from 'utils/codegenerator/targets'; import { useSelector } from 'react-redux'; import { getGlobalEnvironmentVariables } from 'utils/collections/index'; +const getTreePathFromCollectionToItem = (collection, _itemUid) => { + let path = []; + let item = findItemInCollection(collection, _itemUid); + while (item) { + path.unshift(item); + item = findParentItemInCollection(collection, item?.uid); + } + return path; +}; + +// Function to resolve inherited auth +const resolveInheritedAuth = (item, collection) => { + const request = item.draft?.request || item.request; + const authMode = request?.auth?.mode; + + // If auth is not inherit or no auth defined, return the request as is + if (!authMode || authMode !== 'inherit') { + return { + ...request + }; + } + + // Get the tree path from collection to item + const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid); + + // Default to collection auth + const collectionAuth = get(collection, 'root.request.auth'); + let effectiveAuth = collectionAuth; + let source = 'collection'; + + // Check folders in reverse to find the closest auth configuration + for (let i of [...requestTreePath].reverse()) { + if (i.type === 'folder') { + const folderAuth = get(i, 'root.request.auth'); + if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') { + effectiveAuth = folderAuth; + source = 'folder'; + break; + } + } + } + + return { + ...request, + auth: effectiveAuth + }; +}; + const GenerateCodeItem = ({ collectionUid, item, onClose }) => { const languages = getLanguages(); @@ -46,6 +94,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => { get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params') ); + // Resolve auth inheritance + const resolvedRequest = resolveInheritedAuth(item, collection); + const [selectedLanguage, setSelectedLanguage] = useState(languages[0]); return ( @@ -94,16 +145,10 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => { language={selectedLanguage} item={{ ...item, - request: - item.request.url !== '' - ? { - ...item.request, - url: finalUrl - } - : { - ...item.draft.request, - url: finalUrl - } + request: { + ...resolvedRequest, + url: finalUrl + } }} /> ) : ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index d3098a936..95287000a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1593,6 +1593,27 @@ export const collectionsSlice = createSlice({ case 'oauth2': set(folder, 'root.request.auth.oauth2', action.payload.content); break; + case 'basic': + set(folder, 'root.request.auth.basic', action.payload.content); + break; + case 'bearer': + set(folder, 'root.request.auth.bearer', action.payload.content); + break; + case 'digest': + set(folder, 'root.request.auth.digest', action.payload.content); + break; + case 'ntlm': + set(folder, 'root.request.auth.ntlm', action.payload.content); + break; + case 'apikey': + set(folder, 'root.request.auth.apikey', action.payload.content); + break; + case 'awsv4': + set(folder, 'root.request.auth.awsv4', action.payload.content); + break; + case 'wsse': + set(folder, 'root.request.auth.wsse', action.payload.content); + break; } } }, From 0d7c94e7e9a59d81988543e96cea44e6b8935d49 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Tue, 6 May 2025 18:41:40 +0530 Subject: [PATCH 786/904] add: auth for other --- .../RequestPane/Auth/ApiKeyAuth/index.js | 62 ++++++++++--------- .../RequestPane/Auth/AwsV4Auth/index.js | 9 ++- .../RequestPane/Auth/DigestAuth/index.js | 10 +-- .../RequestPane/Auth/NTLMAuth/index.js | 10 +-- .../RequestPane/Auth/WsseAuth/index.js | 12 +++- .../src/components/RequestPane/Auth/index.js | 12 ++-- 6 files changed, 67 insertions(+), 48 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js index 22a16563e..acf706c5a 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js @@ -10,16 +10,19 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection import StyledWrapper from './StyledWrapper'; import { humanizeRequestAPIKeyPlacement } from 'utils/collections'; -const ApiKeyAuth = ({ item, collection }) => { +const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {}); + const apikeyAuth = get(request, 'auth.apikey', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const Icon = forwardRef((props, ref) => { return ( @@ -60,6 +63,30 @@ const ApiKeyAuth = ({ item, collection }) => { return ( +
+
Add To
+ } placement="bottom-end"> +
{ + dropdownTippyRef?.current?.hide(); + handleAuthChange('placement', 'header'); + }} + > + Header +
+
{ + dropdownTippyRef?.current?.hide(); + handleAuthChange('placement', 'queryParam'); + }} + > + Query Param +
+
+
+
{ onChange={(val) => handleAuthChange('key', val)} onRun={handleRun} collection={collection} + item={item} />
-
+
{ onChange={(val) => handleAuthChange('value', val)} onRun={handleRun} collection={collection} + item={item} + isSecret={true} />
- - -
- } placement="bottom-end"> -
{ - dropdownTippyRef.current.hide(); - handleAuthChange('placement', 'header'); - }} - > - Header -
-
{ - dropdownTippyRef.current.hide(); - handleAuthChange('placement', 'queryparams'); - }} - > - Query Params -
-
-
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js index a44cecc1b..75469d784 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js @@ -8,14 +8,17 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection import StyledWrapper from './StyledWrapper'; import { update } from 'lodash'; -const AwsV4Auth = ({ onTokenChange, item, collection }) => { +const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {}); + const awsv4Auth = get(request, 'auth.awsv4', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleAccessKeyIdChange = (accessKeyId) => { dispatch( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js index e91ed8d1f..50b92f669 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js @@ -3,18 +3,20 @@ import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -const DigestAuth = ({ item, collection }) => { +const DigestAuth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {}); + const digestAuth = get(request, 'auth.digest', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleUsernameChange = (username) => { dispatch( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js index 65e756041..1164fb903 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js @@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -const NTLMAuth = ({ item, collection }) => { +const NTLMAuth = ({ item, collection, request, save, updateAuth }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {}); + const ntlmAuth = get(request, 'auth.ntlm', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleUsernameChange = (username) => { dispatch( @@ -26,7 +29,6 @@ const NTLMAuth = ({ item, collection }) => { username: username, password: ntlmAuth.password, domain: ntlmAuth.domain - } }) ); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js index 76a20e6f6..ae201370e 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js @@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -const WsseAuth = ({ item, collection }) => { +const WsseAuth = ({ item, collection, updateAuth, request, save }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {}); + const wsseAuth = get(request, 'auth.wsse', {}); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleSave = () => { + save(); + }; const handleUserChange = (username) => { dispatch( @@ -55,6 +58,7 @@ const WsseAuth = ({ item, collection }) => { onChange={(val) => handleUserChange(val)} onRun={handleRun} collection={collection} + item={item} />
@@ -67,6 +71,8 @@ const WsseAuth = ({ item, collection }) => { onChange={(val) => handlePasswordChange(val)} onRun={handleRun} collection={collection} + item={item} + isSecret={true} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index a1469675a..8ca23ab8d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -71,7 +71,7 @@ const Auth = ({ item, collection }) => { const getAuthView = () => { switch (authMode) { case 'awsv4': { - return ; + return ; } case 'basic': { return ; @@ -80,19 +80,19 @@ const Auth = ({ item, collection }) => { return ; } case 'digest': { - return ; + return ; } case 'ntlm': { - return ; + return ; } case 'oauth2': { - return ; + return ; } case 'wsse': { - return ; + return ; } case 'apikey': { - return ; + return ; } case 'inherit': { const source = getEffectiveAuthSource(); From 2ee7ce5829201f408db8bcfbe3cd2a2ddcb5127b Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 6 May 2025 22:20:59 +0530 Subject: [PATCH 787/904] persist request/folder uids after request/folder resequencing and ui updates (#4611) * move file/folder uids to new paths * drag file/folder preview ui updates, can item be dropped ui hint check --------- Co-authored-by: lohit --- .../StyledWrapper.js | 9 ++++ .../CollectionItemDragPreview/index.js | 49 +++++++++++++++++++ .../CollectionItem/StyledWrapper.js | 1 + .../Collection/CollectionItem/index.js | 31 ++++++++++-- .../Collections/Collection/StyledWrapper.js | 3 +- .../Sidebar/Collections/Collection/index.js | 21 +++++--- .../ReduxStore/slices/collections/actions.js | 19 +------ .../ReduxStore/slices/collections/index.js | 2 +- .../bruno-app/src/utils/collections/index.js | 15 ++++++ packages/bruno-electron/src/app/watcher.js | 3 +- packages/bruno-electron/src/ipc/collection.js | 10 +++- .../bruno-electron/src/utils/filesystem.js | 22 ++++++++- 12 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js new file mode 100644 index 000000000..62f53069e --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .drag-preview { + background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js new file mode 100644 index 000000000..1ad4065a8 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js @@ -0,0 +1,49 @@ +import { useDragLayer } from 'react-dnd'; +import { + IconFile, + IconFolder, +} from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; + +function getItemStyles({ x, y }) { + if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' }; + const transform = `translate(${x}px, ${y}px)`; + + return { + position: 'fixed', + pointerEvents: 'none', + top: 0, + transform, + WebkitTransform: transform, + zIndex: 100, + }; +} + +export const CollectionItemDragPreview = () => { + const { + item, + isDragging, + clientOffset + } = useDragLayer((monitor) => ({ + item: monitor.getItem(), + isDragging: monitor.isDragging(), + clientOffset: monitor.getClientOffset(), + })); + if (!isDragging) return null; + const { x, y } = clientOffset || {}; + const shouldShowFolderIcon = !item.type || item.type === 'folder'; + return ( + +
+
+ {shouldShowFolderIcon ? ( + + ) : ( + + )} + {item.name} +
+
+
+ ); +}; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js index d47c820c3..4e3525df9 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js @@ -1,6 +1,7 @@ import styled from 'styled-components'; const Wrapper = styled.div` + position: relative; .menu-icon { color: ${(props) => props.theme.sidebar.dropdownIcon.color}; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 92e025405..232d35e7a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -1,4 +1,5 @@ -import React, { useState, useRef, forwardRef } from 'react'; +import React, { useState, useRef, forwardRef, useEffect } from 'react'; +import { getEmptyImage } from 'react-dnd-html5-backend'; import range from 'lodash/range'; import filter from 'lodash/filter'; import classnames from 'classnames'; @@ -28,6 +29,7 @@ import CollectionItemIcon from './CollectionItemIcon'; import { scrollToTheActiveTab } from 'utils/tabs'; import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab'; import { isEqual } from 'lodash'; +import { calculateDraggedItemNewPathname } from 'utils/collections/index'; const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => { const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid }); @@ -56,9 +58,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside' - const [{ isDragging }, drag] = useDrag({ + const [{ isDragging }, drag, dragPreview] = useDrag({ type: `collection-item-${collectionUid}`, - item: item, + item, collect: (monitor) => ({ isDragging: monitor.isDragging() }), @@ -67,6 +69,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) } }); + useEffect(() => { + dragPreview(getEmptyImage(), { captureDraggingState: true }); + }, []); + const determineDropType = (monitor) => { const hoverBoundingRect = ref.current?.getBoundingClientRect(); const clientOffset = monitor.getClientOffset(); @@ -83,6 +89,20 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) } }; + const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => { + const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; + const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; + + if (draggedItemUid === targetItemUid) return false; + + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname }); + if (!newPathname) return false; + + if (targetItemPathname?.startsWith(draggedItemPathname)) return false; + + return true; + }; + const [{ isOver, canDrop }, drop] = useDrop({ accept: `collection-item-${collectionUid}`, hover: (draggedItem, monitor) => { @@ -92,7 +112,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) if (draggedItemUid === targetItemUid) return; const dropType = determineDropType(monitor); - setDropType(dropType); + + const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType }); + + setDropType(_canItemBeDropped ? dropType : null); }, drop: async (draggedItem, monitor) => { const { uid: targetItemUid } = item; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index 0378d9ad9..b47881fad 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -13,7 +13,8 @@ const Wrapper = styled.div` } &.item-hovered { - background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; + border-bottom: 2px solid transparent; .collection-actions { .dropdown { div[aria-expanded='false'] { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index aebea5092..16fa52b21 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -1,4 +1,5 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react'; +import { getEmptyImage } from 'react-dnd-html5-backend'; import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; @@ -7,7 +8,7 @@ import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; @@ -19,9 +20,10 @@ import { isItemAFolder, isItemARequest } from 'utils/collections'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; import CloneCollection from './CloneCollection'; -import { areItemsLoading, findItemInCollection } from 'utils/collections'; +import { areItemsLoading } from 'utils/collections'; import { scrollToTheActiveTab } from 'utils/tabs'; import ShareCollection from 'components/ShareCollection/index'; +import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -127,8 +129,8 @@ const Collection = ({ collection, searchText }) => { const isCollectionItem = (itemType) => { return itemType.startsWith('collection-item'); }; - - const [{ isDragging }, drag] = useDrag({ + + const [{ isDragging }, drag, dragPreview] = useDrag({ type: "collection", item: collection, collect: (monitor) => ({ @@ -157,7 +159,9 @@ const Collection = ({ collection, searchText }) => { }), }); - drag(drop(collectionRef)); + useEffect(() => { + dragPreview(getEmptyImage(), { captureDraggingState: true }); + }, []); if (searchText && searchText.length) { if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) { @@ -193,8 +197,12 @@ const Collection = ({ collection, searchText }) => { {showCloneCollectionModalOpen && ( setShowCloneCollectionModalOpen(false)} /> )} +
{ + collectionRef.current = node; + drag(drop(node)); + }} >
{
-
{!collectionIsCollapsed ? (
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 86ad618aa..8c1878342 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -44,7 +44,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; -import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory } from 'utils/collections/index'; +import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory, calculateDraggedItemNewPathname } from 'utils/collections/index'; import { sanitizeName } from 'utils/common/regex'; import { safeParseJSON, safeStringifyJSON } from 'utils/common/index'; @@ -649,21 +649,6 @@ export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, co const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection; const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items); - const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType }) => { - const { pathname: targetItemPathname } = targetItem; - const { filename: draggedItemFilename } = draggedItem; - const targetItemDirname = path.dirname(targetItemPathname); - const isTargetTheCollection = targetItemPathname === collection.pathname; - const isTargetItemAFolder = isItemAFolder(targetItem); - - if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) { - return path.join(targetItemPathname, draggedItemFilename) - } else if (dropType === 'adjacent') { - return path.join(targetItemDirname, draggedItemFilename) - } - return null; - }; - const handleMoveToNewLocation = async ({ draggedItem, draggedItemDirectoryItems, targetItem, targetItemDirectoryItems, newPathname, dropType }) => { const { uid: targetItemUid } = targetItem; const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem; @@ -725,7 +710,7 @@ export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, co return new Promise(async (resolve, reject) => { try { - const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType }); + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname: collection.pathname }); if (!newPathname) return; if (targetItemPathname?.startsWith(draggedItemPathname)) return; if (newPathname !== draggedItemPathname) { diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index d3098a936..3dfa6d052 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1798,7 +1798,7 @@ export const collectionsSlice = createSlice({ currentPath = path.join(currentPath, directoryName); if (!childItem) { childItem = { - uid: uuid(), + uid: dir?.meta?.uid || uuid(), pathname: currentPath, name: dir?.meta?.name || directoryName, seq: dir?.meta?.seq || 1, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index add047119..61ce02f50 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1068,5 +1068,20 @@ export const getReorderedItemsInSourceDirectory = ({ items }) => { ); }; +export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => { + const { pathname: targetItemPathname } = targetItem; + const { filename: draggedItemFilename } = draggedItem; + const targetItemDirname = path.dirname(targetItemPathname); + const isTargetTheCollection = targetItemPathname === collectionPathname; + const isTargetItemAFolder = isItemAFolder(targetItem); + + if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) { + return path.join(targetItemPathname, draggedItemFilename) + } else if (dropType === 'adjacent') { + return path.join(targetItemDirname, draggedItemFilename) + } + return null; +}; + // item sequence utils - END diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 8cd3db22d..a2f91d953 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -341,7 +341,8 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { collectionUid, pathname, name, - seq + seq, + uid: getRequestUid(pathname) } }; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 97742928c..443c5e956 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -25,7 +25,8 @@ const { sizeInMB, safeWriteFileSync, copyPath, - removePath + removePath, + getPaths } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -799,8 +800,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => { try { if (fs.existsSync(targetDirname)) { + const sourceDirname = path.dirname(sourcePathname); + const pathnamesBefore = await getPaths(sourcePathname); + const pathnamesAfter = pathnamesBefore?.map(p => p?.replace(sourceDirname, targetDirname)); await copyPath(sourcePathname, targetDirname); await removePath(sourcePathname); + // move the request uids of the previous file/folders to the new file/folder items + pathnamesAfter?.forEach((_, index) => { + moveRequestUid(pathnamesBefore[index], pathnamesAfter[index]); + }); } } catch (error) { return Promise.reject(error); diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index f00734c7e..7593d79ed 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -324,6 +324,25 @@ const removePath = async (source) => { } } +// Recursively gets paths. +const getPaths = async (source) => { + let paths = []; + const _getPaths = async (source) => { + const stat = await fsPromises.lstat(source); + paths.push(source); + if (stat.isDirectory()) { + const entries = await fsPromises.readdir(source); + for (const entry of entries) { + const entryPath = path.join(source, entry); + await _getPaths(entryPath); + } + } + } + await _getPaths(source); + return paths; +} + + module.exports = { isValidPathname, exists, @@ -352,5 +371,6 @@ module.exports = { safeWriteFile, safeWriteFileSync, copyPath, - removePath + removePath, + getPaths }; From f2eaa79318c67a38800ea9ff112300b81d86c4e9 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 7 May 2025 15:36:21 +0530 Subject: [PATCH 788/904] Feat: add openapi to bruno import in cli --- package-lock.json | 2 + packages/bruno-cli/package.json | 2 + packages/bruno-cli/readme.md | 38 ++++ packages/bruno-cli/src/commands/import.js | 230 +++++++++++++++++++++ packages/bruno-cli/src/utils/collection.js | 137 +++++++++++- packages/bruno-cli/src/utils/filesystem.js | 44 +++- 6 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-cli/src/commands/import.js diff --git a/package-lock.json b/package-lock.json index 08f122dc9..5e41191ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25222,6 +25222,7 @@ "dependencies": { "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", + "@usebruno/converters": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/requests": "^0.1.0", @@ -25237,6 +25238,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "iconv-lite": "^0.6.3", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 7347f78fb..e1b74e191 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -52,6 +52,7 @@ "@usebruno/lang": "0.12.0", "@usebruno/vm2": "^3.9.13", "@usebruno/requests": "^0.1.0", + "@usebruno/converters": "^0.1.0", "aws4-axios": "^3.3.0", "axios": "^1.8.3", "axios-ntlm": "^1.4.2", @@ -63,6 +64,7 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "iconv-lite": "^0.6.3", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md index 616f1b385..c1d38f457 100644 --- a/packages/bruno-cli/readme.md +++ b/packages/bruno-cli/readme.md @@ -58,6 +58,44 @@ If you need to limit the trusted CA to a specified set when validating the reque bru run request.bru --cacert myCustomCA.pem --ignore-truststore ``` +## Importing Collections + +You can import collections from other formats, such as OpenAPI, using the import command: + +```bash +bru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API" +``` + +You can also use the shorter form with aliases: + +```bash +bru import openapi -s api.yml -o ~/Desktop/my-collection -n "My API" +``` + +This creates a Bruno collection directory that can be opened in Bruno. + +You can also import directly from a URL: + +```bash +bru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API" +``` + +You can also export the collection as a JSON file: + +```bash +bru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API" +``` + +Import Options: + +| Option | Details | +| ------------------------- | -------------------------------------------------- | +| --source, -s | Path to the source file or URL (required) | +| --output, -o | Path to the output directory | +| --output-file, -f | Path to the output JSON file | +| --collection-name, -n | Name for the imported collection | +| --insecure | Skip SSL certificate validation when fetching from URLs | + ## Command Line Options | Option | Details | diff --git a/packages/bruno-cli/src/commands/import.js b/packages/bruno-cli/src/commands/import.js new file mode 100644 index 000000000..dd12a8bc3 --- /dev/null +++ b/packages/bruno-cli/src/commands/import.js @@ -0,0 +1,230 @@ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const jsyaml = require('js-yaml'); +const axios = require('axios'); +const { openApiToBruno } = require('@usebruno/converters'); +const { exists, isDirectory, sanitizeName } = require('../utils/filesystem'); +const { createCollectionFromBrunoObject } = require('../utils/collection'); + +const command = 'import '; +const desc = 'Import a collection from other formats'; + +const builder = (yargs) => { + yargs + .positional('type', { + describe: 'Type of collection to import', + type: 'string', + choices: ['openapi'] + }) + .option('source', { + alias: 's', + describe: 'Path to the source file or URL', + type: 'string', + demandOption: true + }) + .option('output', { + alias: 'o', + describe: 'Path to the output directory', + type: 'string', + conflicts: 'output-file' + }) + .option('output-file', { + alias: 'f', + describe: 'Path to the output JSON file', + type: 'string', + conflicts: 'output' + }) + .option('collection-name', { + alias: 'n', + describe: 'Name for the imported collection', + type: 'string' + }) + .option('insecure', { + type: 'boolean', + describe: 'Skip SSL certificate verification when fetching from URLs', + default: false + }) + .example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"') + .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"') + .example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"') + .example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop') + .example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"') + .example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"'); +}; + +const isUrl = (str) => { + try { + return Boolean(new URL(str)); + } catch (error) { + return false; + } +}; + +const readOpenApiFile = async (source, options = {}) => { + try { + let content; + + if (isUrl(source)) { + // Handle URL input + console.log(chalk.yellow(`Fetching specification from URL: ${source}`)); + try { + const axiosOptions = { + timeout: 30000, // 30 second timeout + maxContentLength: 10 * 1024 * 1024, + validateStatus: status => status >= 200 && status < 300 + }; + + // Skip SSL certificate validation if insecure flag is set + if (options.insecure) { + console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.')); + axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false }); + } + + const response = await axios.get(source, axiosOptions); + content = response.data; + } catch (error) { + if (error.code === 'ECONNABORTED') { + throw new Error('Request timed out. The server took too long to respond.'); + } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || + error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`); + } else if (error.response) { + throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + throw new Error(`No response received from server. Check the URL and your network connection.`); + } else { + throw new Error(`Error fetching URL: ${error.message}`); + } + } + + // If response is already an object, return it directly + if (typeof content === 'object' && content !== null) { + return content; + } + } else { + // Handle file input + if (!await exists(source)) { + throw new Error(`File does not exist: ${source}`); + } + content = fs.readFileSync(source, 'utf8'); + } + + // If content is a string, try to parse as JSON or YAML + if (typeof content === 'string') { + try { + return JSON.parse(content); + } catch (jsonError) { + try { + return jsyaml.load(content); + } catch (yamlError) { + throw new Error('Failed to parse content as JSON or YAML'); + } + } + } + + return content; + } catch (error) { + // Let the specific error handling from above propagate + throw error; + } +}; + +const handler = async (argv) => { + try { + const { type, source, output, outputFile, collectionName, insecure } = argv; + + if (!type || type !== 'openapi') { + console.error(chalk.red('Only OpenAPI import is supported currently')); + process.exit(1); + } + + if (!source) { + console.error(chalk.red('Source file or URL is required')); + process.exit(1); + } + + if (!output && !outputFile) { + console.error(chalk.red('Either --output or --output-file is required')); + process.exit(1); + } + + console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`)); + + const openApiSpec = await readOpenApiFile(source, { insecure }); + + if (!openApiSpec) { + console.error(chalk.red('Failed to parse OpenAPI specification')); + process.exit(1); + } + + console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...')); + + // Convert OpenAPI to Bruno format + let brunoCollection = openApiToBruno(openApiSpec); + + // Override collection name if provided + if (collectionName) { + brunoCollection.name = collectionName; + } + + if (outputFile) { + // Save as JSON file + const outputPath = path.resolve(outputFile); + fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2)); + console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`)); + } else if (output) { + const resolvedOutput = path.resolve(output); + + // Check if output is an existing directory + const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput); + + // Determine the final output directory + let outputDir; + if (isOutputDirectory) { + // If output is an existing directory, use collection name to create a subdirectory + const dirName = sanitizeName(brunoCollection.name); + outputDir = path.join(resolvedOutput, dirName); + + // Check if this subfolder already exists + if (await exists(outputDir)) { + const dirContents = fs.readdirSync(outputDir); + if (dirContents.length > 0) { + console.error(chalk.red(`Output directory is not empty: ${outputDir}`)); + process.exit(1); + } + } else { + // Create the subfolder + fs.mkdirSync(outputDir, { recursive: true }); + } + } else { + // If output doesn't exist or is not a directory, use it directly + outputDir = resolvedOutput; + + // Check if parent directory exists + const parentDir = path.dirname(outputDir); + if (!await exists(parentDir)) { + console.error(chalk.red(`Parent directory does not exist: ${parentDir}`)); + process.exit(1); + } + + fs.mkdirSync(outputDir, { recursive: true }); + } + + await createCollectionFromBrunoObject(brunoCollection, outputDir); + console.log(chalk.green(`Bruno collection created at ${outputDir}`)); + } + } catch (error) { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } +}; + +module.exports = { + command, + desc, + builder, + handler, + isUrl, + readOpenApiFile +}; \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 64e17cb39..ec2705af3 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -1,5 +1,9 @@ const { get, each, find, compact } = require('lodash'); const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang'); +const { sanitizeName } = require('./filesystem'); const mergeHeaders = (collection, request, requestTreePath) => { let headers = new Map(); @@ -200,10 +204,141 @@ const getTreePathFromCollectionToItem = (collection, _item) => { return path; }; +/** + * Safe write file implementation to handle errors + * @param {string} filePath - Path to write file + * @param {string} content - Content to write + */ +const safeWriteFileSync = (filePath, content) => { + try { + fs.writeFileSync(filePath, content, { encoding: 'utf8' }); + } catch (error) { + console.error(`Error writing file ${filePath}:`, error); + } +}; + +/** + * Creates a Bruno collection directory structure from a Bruno collection object + * + * @param {Object} collection - The Bruno collection object + * @param {string} dirPath - The output directory path + */ +const createCollectionFromBrunoObject = async (collection, dirPath) => { + // Create bruno.json + const brunoConfig = { + version: '1', + name: collection.name, + type: 'collection', + ignore: ['node_modules', '.git'] + }; + + fs.writeFileSync( + path.join(dirPath, 'bruno.json'), + JSON.stringify(brunoConfig, null, 2) + ); + + // Create collection.bru if root exists + if (collection.root) { + const collectionContent = await jsonToCollectionBru(collection.root); + fs.writeFileSync(path.join(dirPath, 'collection.bru'), collectionContent); + } + + // Process environments + if (collection.environments && collection.environments.length) { + const envDirPath = path.join(dirPath, 'environments'); + fs.mkdirSync(envDirPath, { recursive: true }); + + for (const env of collection.environments) { + const content = await envJsonToBruV2(env); + const filename = sanitizeName(`${env.name}.bru`); + fs.writeFileSync(path.join(envDirPath, filename), content); + } + } + + // Process collection items + await processCollectionItems(collection.items, dirPath); + + return dirPath; +}; + +/** + * Recursively processes collection items to create files and folders + * + * @param {Array} items - Collection items + * @param {string} currentPath - Current directory path + */ +const processCollectionItems = async (items = [], currentPath) => { + for (const item of items) { + if (item.type === 'folder') { + // Create folder + let sanitizedFolderName = sanitizeName(item?.filename || item?.name); + const folderPath = path.join(currentPath, sanitizedFolderName); + fs.mkdirSync(folderPath, { recursive: true }); + + // Create folder.bru file if root exists + if (item?.root?.meta?.name) { + const folderBruFilePath = path.join(folderPath, 'folder.bru'); + if (item.seq) { + item.root.meta.seq = item.seq; + } + const folderContent = await jsonToCollectionBru( + item.root, + true + ); + safeWriteFileSync(folderBruFilePath, folderContent); + } + + // Process folder items recursively + if (item.items && item.items.length) { + await processCollectionItems(item.items, folderPath); + } + } else if (['http-request', 'graphql-request'].includes(item.type)) { + // Create request file + let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`); + if (!sanitizedFilename.endsWith('.bru')) { + sanitizedFilename += '.bru'; + } + + // Convert JSON to BRU format based on the item type + let type = item.type === 'http-request' ? 'http' : 'graphql'; + const bruJson = { + meta: { + name: item.name, + type: type, + seq: typeof item.seq === 'number' ? item.seq : 1 + }, + http: { + method: (item.request?.method || 'GET').toLowerCase(), + url: item.request?.url || '', + auth: item.request?.auth?.mode || 'none', + body: item.request?.body?.mode || 'none' + }, + params: item.request?.params || [], + headers: item.request?.headers || [], + auth: item.request?.auth || {}, + body: item.request?.body || {}, + script: item.request?.script || {}, + vars: { + req: item.request?.vars?.req || [], + res: item.request?.vars?.res || [] + }, + assertions: item.request?.assertions || [], + tests: item.request?.tests || '', + docs: item.request?.docs || '' + }; + + // Convert to BRU format and write to file + const content = await jsonToBruV2(bruJson); + safeWriteFileSync(path.join(currentPath, sanitizedFilename), content); + } + } +}; + module.exports = { mergeHeaders, mergeVars, mergeScripts, findItemInCollection, - getTreePathFromCollectionToItem + getTreePathFromCollectionToItem, + createCollectionFromBrunoObject } \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/filesystem.js b/packages/bruno-cli/src/utils/filesystem.js index c3438cebc..46aa6c797 100644 --- a/packages/bruno-cli/src/utils/filesystem.js +++ b/packages/bruno-cli/src/utils/filesystem.js @@ -118,6 +118,46 @@ const getSubDirectories = (dir) => { } }; +/** + * Sanitizes a filename to make it safe for filesystem operations + * + * @param {string} name - The name to sanitize + * @returns {string} - The sanitized name + */ +const sanitizeName = (name) => { + if (!name) return ''; + + const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; + return name + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) +}; + +/** + * Validates if a name is valid for the filesystem + * + * @param {string} name - The name to validate + * @returns {boolean} - True if the name is valid, false otherwise + */ +const validateName = (name) => { + if (!name) return false; + + const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; + const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start + const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters + const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed + + if (name.length > 255) return false; // max name length + if (reservedDeviceNames.test(name)) return false; // windows reserved names + + return ( + firstCharacter.test(name) && + middleCharacters.test(name) && + lastCharacter.test(name) + ); +}; + module.exports = { exists, isSymbolicLink, @@ -131,5 +171,7 @@ module.exports = { searchForFiles, searchForBruFiles, stripExtension, - getSubDirectories + getSubDirectories, + sanitizeName, + validateName }; From 4af0bb394398e954c997293251e325f84e2e799b Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Wed, 7 May 2025 16:59:35 +0530 Subject: [PATCH 789/904] Fix: ResponseSize component logic to handle size when it's undefined (#4613) * Fix: ResponseSize component logic to handle size when it's undefined * Improved check for valid response size --------- Co-authored-by: Sreelakshmi Jayarajan --- .../src/components/ResponsePane/ResponseSize/index.js | 8 +++++++- packages/bruno-app/src/components/ResponsePane/index.js | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js index b956b0813..b1cff2157 100644 --- a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js @@ -2,14 +2,20 @@ import React from 'react'; import StyledWrapper from './StyledWrapper'; const ResponseSize = ({ size }) => { + + if (!Number.isFinite(size)) { + return null; + } + let sizeToDisplay = ''; + // If size is greater than 1024 bytes, format as KB if (size > 1024) { - // size is greater than 1kb let kb = Math.floor(size / 1024); let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100); sizeToDisplay = kb + '.' + decimal + 'KB'; } else { + // If size is less than or equal to 1024 bytes, display as bytes (B) sizeToDisplay = size + 'B'; } diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index ebacf05c5..1fb120ae9 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -48,6 +48,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { }; const response = item.response || {}; + const responseSize = response.size || 0; const getTabPanel = (tab) => { switch (tab) { @@ -156,7 +157,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { - + ) : null}
From 2852c07ec7c547fd386414cb836852ace045ce7f Mon Sep 17 00:00:00 2001 From: Pooja Belaramani <109731557+poojabela@users.noreply.github.com> Date: Wed, 7 May 2025 17:44:29 +0530 Subject: [PATCH 790/904] feat: support tv4 as a inbuilt lib (#4589) --- packages/bruno-js/package.json | 1 + .../bruno-js/src/runtime/script-runtime.js | 3 ++ packages/bruno-js/src/runtime/test-runtime.js | 2 + .../bruno-js/src/sandbox/bundle-libraries.js | 5 ++- .../scripting/inbuilt modules/tv4/folder.bru | 3 ++ .../scripting/inbuilt modules/tv4/tv4.bru | 39 +++++++++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru create mode 100644 packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index d9b4e16e9..c040244a3 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -35,6 +35,7 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", + "tv4": "^1.3.0", "uuid": "^9.0.0", "xml2js": "^0.6.2" }, diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2a8d02a87..bbb0476f5 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -30,6 +30,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const tv4 = require('tv4'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); class ScriptRuntime { @@ -151,6 +152,7 @@ class ScriptRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault @@ -285,6 +287,7 @@ class ScriptRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index e2d1f4865..f1a5ef572 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -32,6 +32,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const tv4 = require('tv4'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); const getResultsSummary = (results) => { @@ -209,6 +210,7 @@ class TestRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + tv4, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-js/src/sandbox/bundle-libraries.js b/packages/bruno-js/src/sandbox/bundle-libraries.js index 30d6c7c03..1545ef5cd 100644 --- a/packages/bruno-js/src/sandbox/bundle-libraries.js +++ b/packages/bruno-js/src/sandbox/bundle-libraries.js @@ -12,6 +12,7 @@ const bundleLibraries = async () => { import btoa from "btoa"; import atob from "atob"; import * as CryptoJS from "@usebruno/crypto-js"; + import tv4 from "tv4"; globalThis.expect = expect; globalThis.assert = assert; globalThis.moment = moment; @@ -19,6 +20,7 @@ const bundleLibraries = async () => { globalThis.atob = atob; globalThis.Buffer = Buffer; globalThis.CryptoJS = CryptoJS; + globalThis.tv4 = tv4; globalThis.requireObject = { ...(globalThis.requireObject || {}), 'chai': { expect, assert }, @@ -26,7 +28,8 @@ const bundleLibraries = async () => { 'buffer': { Buffer }, 'btoa': btoa, 'atob': atob, - 'crypto-js': CryptoJS + 'crypto-js': CryptoJS, + 'tv4': tv4 }; `; diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru new file mode 100644 index 000000000..4fd4bb1d2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru @@ -0,0 +1,3 @@ +meta { + name: tv4 +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru new file mode 100644 index 000000000..820a7a3b8 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru @@ -0,0 +1,39 @@ +meta { + name: tv4 + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: inherit +} + +body:json { + { + "name": "John", + "age": 30 + } +} + +tests { + const tv4 = require("tv4") + + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' } + } + }; + + let responseData = res.getBody(); + + let isValid = tv4.validate(responseData, schema); + + test("Response body matches expected schema", function () { + expect(isValid, tv4.error ? tv4.error.message : "").to.be.true; + }); + +} From fc422853ef6e0a10f595d8c3832821bcaf303a73 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 7 May 2025 19:19:40 +0530 Subject: [PATCH 791/904] update T_RunnerResults type to include summary prop (#4617) Co-authored-by: lohit --- .../bruno-common/src/runner/reports/html/generate-report.ts | 5 ++--- packages/bruno-common/src/runner/types/index.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-common/src/runner/reports/html/generate-report.ts b/packages/bruno-common/src/runner/reports/html/generate-report.ts index 378a75197..7309c483e 100644 --- a/packages/bruno-common/src/runner/reports/html/generate-report.ts +++ b/packages/bruno-common/src/runner/reports/html/generate-report.ts @@ -1,6 +1,5 @@ import { T_RunnerResults } from "../../types"; import { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from "../../utils"; -import { getRunnerSummary } from "../../runner-summary"; import htmlTemplateString from "./template"; const generateHtmlReport = ({ @@ -8,7 +7,7 @@ const generateHtmlReport = ({ }: { runnerResults: T_RunnerResults[] }): string => { - const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results }) => { + const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results, summary }) => { return { iterationIndex, results: results.map((result) => { @@ -29,7 +28,7 @@ const generateHtmlReport = ({ } } }), - summary: getRunnerSummary(results) + summary } }); const htmlString = htmlTemplateString(encodeBase64(JSON.stringify(resultsWithSummaryAndCleanData))); diff --git a/packages/bruno-common/src/runner/types/index.ts b/packages/bruno-common/src/runner/types/index.ts index d9c36e32c..33840f0ad 100644 --- a/packages/bruno-common/src/runner/types/index.ts +++ b/packages/bruno-common/src/runner/types/index.ts @@ -96,6 +96,7 @@ export type T_RunnerResults = { iterationIndex: number; iterationData?: any; // todo - csv/json row data results: T_RunnerRequestExecutionResult[]; + summary: T_RunSummary; } // run summary type From da172ff9b511feb276dceae62e26c0ad91ded7e7 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 7 May 2025 22:12:57 +0530 Subject: [PATCH 792/904] fix sanitize name function tests --- packages/bruno-app/src/utils/common/regex.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bruno-app/src/utils/common/regex.spec.js b/packages/bruno-app/src/utils/common/regex.spec.js index e7a8b8d36..3994a2b2d 100644 --- a/packages/bruno-app/src/utils/common/regex.spec.js +++ b/packages/bruno-app/src/utils/common/regex.spec.js @@ -23,8 +23,8 @@ describe('regex validators', () => { }); it('should remove trailing periods', () => { - expect(sanitizeName('.file')).toBe('file'); - expect(sanitizeName('.file.')).toBe('file'); + expect(sanitizeName('.file')).toBe('.file'); + expect(sanitizeName('.file.')).toBe('.file'); expect(sanitizeName('file.')).toBe('file'); expect(sanitizeName('file.name.')).toBe('file.name'); expect(sanitizeName('hello world.')).toBe('hello world'); @@ -83,11 +83,11 @@ describe('regex validators', () => { it('should handle filenames with multiple consecutive periods (only remove trailing)', () => { expect(sanitizeName('file.name...')).toBe('file.name'); - expect(sanitizeName('...file')).toBe('file'); + expect(sanitizeName('...file')).toBe('...file'); expect(sanitizeName('file.name... ')).toBe('file.name'); - expect(sanitizeName(' ...file')).toBe('file'); - expect(sanitizeName(' ...file ')).toBe('file'); - expect(sanitizeName(' ...file.... ')).toBe('file'); + expect(sanitizeName(' ...file')).toBe('...file'); + expect(sanitizeName(' ...file ')).toBe('...file'); + expect(sanitizeName(' ...file.... ')).toBe('...file'); }); it('should handle very long filenames', () => { From fc553e10094acf737c1560a1471fdeea652f0963 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 7 May 2025 22:24:52 +0530 Subject: [PATCH 793/904] fix sanitize name function tests in bruno-electron --- packages/bruno-electron/src/utils/filesystem.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/utils/filesystem.test.js b/packages/bruno-electron/src/utils/filesystem.test.js index a6e2db53a..a0f0f018d 100644 --- a/packages/bruno-electron/src/utils/filesystem.test.js +++ b/packages/bruno-electron/src/utils/filesystem.test.js @@ -2,9 +2,13 @@ const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = r describe('sanitizeName', () => { it('should replace invalid characters with hyphens', () => { - const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'; - const expectedOutput = '----------------------------------------'; - expect(sanitizeName(input)).toEqual(expectedOutput); + expect(sanitizeName( + 'valid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + )).toEqual('valid----------------------------------------'); + + expect(sanitizeName( + '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1Fvalid<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F' + )).toEqual('valid----------------------------------------'); }); it('should not modify valid directory names', () => { From 967170a7b29f05a68e5baca61182274f768c6cff Mon Sep 17 00:00:00 2001 From: lohit Date: Thu, 8 May 2025 18:09:55 +0530 Subject: [PATCH 794/904] `eslint` for `bruno-app` and `bruno-electron` packages (#4622) Co-authored-by: lohit --- .husky/pre-commit | 3 + eslint.config.js | 41 + package-lock.json | 1985 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 9 +- 4 files changed, 2016 insertions(+), 22 deletions(-) create mode 100755 .husky/pre-commit create mode 100644 eslint.config.js diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..166ee6873 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +# .husky/pre-commit + +npx lint-staged \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..40f6c3351 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +// eslint.config.js +const { defineConfig } = require("eslint/config"); +const globals = require("globals"); + +module.exports = defineConfig([ + { + files: ["packages/bruno-app/**/*.{js,jsx,ts}"], + ignores: ["**/*.config.js"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.jest, + global: false, + require: false, + Buffer: false, + process: false + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + rules: { + "no-undef": "error", + }, + }, + { + files: ["packages/bruno-electron/**/*.{js}"], + ignores: ["**/*.config.js"], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + }, + rules: { + "no-undef": "error", + }, + } +]); diff --git a/package-lock.json b/package-lock.json index 08f122dc9..ee2147dd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,12 @@ "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", + "eslint": "^9.26.0", "fs-extra": "^11.1.1", + "globals": "^16.1.0", "husky": "^8.0.3", "jest": "^29.2.0", + "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", @@ -54,7 +57,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1475,7 +1477,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -1506,7 +1507,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1524,7 +1524,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -1804,7 +1803,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -2353,6 +2351,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", @@ -3207,6 +3214,15 @@ } } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3757,6 +3773,292 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", "license": "MIT" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@faker-js/faker": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", @@ -3949,6 +4251,72 @@ "react-dom": "^16 || ^17 || ^18" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -5011,6 +5379,44 @@ "node": ">=10" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", + "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@module-federation/runtime": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz", @@ -7789,7 +8195,6 @@ "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": { @@ -7812,7 +8217,6 @@ "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": "*", @@ -7823,7 +8227,6 @@ "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": { @@ -8319,6 +8722,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -11068,7 +11481,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -11797,6 +12209,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -12679,7 +13098,6 @@ "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": { @@ -12751,6 +13169,19 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -12843,7 +13274,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -12851,6 +13281,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "zod": "^3.24.2" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -12865,6 +13358,216 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -12878,6 +13581,29 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -12977,6 +13703,29 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -13057,6 +13806,49 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/express-basic-auth": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", @@ -13066,6 +13858,259 @@ "basic-auth": "^2.0.1" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -13168,6 +14213,13 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -13247,6 +14299,19 @@ "integrity": "sha512-KnYitqNf/rANEhUxWzkINAaMVc7SshejwA5HEd5Wr8lEJQX1Js1LCndectS44SXTnXWK+jbHQYs4R6CaG+7Jkg==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -13360,6 +14425,27 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -13647,7 +14733,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -13662,6 +14747,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -13856,12 +14954,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -15137,6 +16239,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -16552,6 +17661,13 @@ "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", "license": "BSD-2-Clause" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -16789,6 +17905,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -16824,6 +17954,409 @@ "uc.micro": "^1.0.1" } }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-script": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", @@ -16936,6 +18469,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -16983,6 +18523,222 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -17578,6 +19334,19 @@ "node": ">=8" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -18435,6 +20204,24 @@ "node": ">=6" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -18889,6 +20676,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -19011,6 +20808,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -19034,6 +20844,16 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -20021,6 +21841,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -20341,12 +22171,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -21349,6 +23179,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -21630,6 +23467,48 @@ "dev": true, "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -23002,6 +24881,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -24163,6 +26052,19 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -24207,7 +26109,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -24845,6 +26747,16 @@ "dev": true, "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -25063,6 +26975,26 @@ "node": ">=10" } }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "packages/bruno-app": { "name": "@usebruno/app", "version": "2.0.0", @@ -26824,6 +28756,16 @@ } } }, + "packages/bruno-common/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "packages/bruno-common/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -28183,6 +30125,7 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", + "tv4": "^1.3.0", "uuid": "^9.0.0", "xml2js": "^0.6.2" }, diff --git a/package.json b/package.json index 7508cb904..30d41bb01 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,12 @@ "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", + "eslint": "^9.26.0", "fs-extra": "^11.1.1", + "globals": "^16.1.0", "husky": "^8.0.3", "jest": "^29.2.0", + "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", @@ -59,7 +62,8 @@ "test:e2e": "npx playwright test", "test:report": "npx playwright show-report", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", - "prepare": "husky install" + "prepare": "husky install", + "lint": "npx eslint ./" }, "overrides": { "rollup": "3.29.5", @@ -68,5 +72,8 @@ "json-schema-typed": "8.0.1" } } + }, + "lint-staged": { + "packages/**/*.{js,jsx,ts,tsx}": "npm run lint" } } From e9a79a32da520ba1cec36cf4faa81de4c3cde5c0 Mon Sep 17 00:00:00 2001 From: lohit Date: Thu, 8 May 2025 18:12:16 +0530 Subject: [PATCH 795/904] lint error fixes (#4623) Co-authored-by: lohit --- .../Auth/OAuth2/Oauth2TokenViewer/index.js | 2 +- .../Collection/CollectionItem/index.js | 2 +- .../ReduxStore/slices/collections/actions.js | 24 +++++++++++++++---- .../ReduxStore/slices/global-environments.js | 9 ++++++- .../src/utils/codemirror/javascript-lint.js | 2 ++ packages/bruno-app/src/utils/common/cache.js | 10 -------- packages/bruno-app/src/utils/common/index.js | 2 +- 7 files changed, 32 insertions(+), 19 deletions(-) delete mode 100644 packages/bruno-app/src/utils/common/cache.js diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js index 9439a0bea..7692b5891 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js @@ -1,6 +1,6 @@ +import { useState, useEffect, useMemo } from "react"; import { find } from "lodash"; import StyledWrapper from "./StyledWrapper"; -import { useState, useEffect } from "react"; import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons'; import { getAllVariables } from 'utils/collections/index'; import { interpolate } from '@usebruno/common'; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 232d35e7a..6c34dcaa3 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -278,7 +278,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) const viewFolderSettings = () => { if (isItemAFolder(item)) { - if (itemIsOpenedInTabs(item, tabs)) { + if (isTabForItemPresent) { dispatch(focusTab({ uid: item.uid })); return; } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 8c1878342..6aa890d8b 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -56,7 +56,7 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState) if (!collection) { return reject(new Error('Collection not found')); } - + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:rename-collection', newName, collection.pathname).then(resolve).catch(reject); }); }; @@ -333,6 +333,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) }) ); + const { ipcRenderer } = window; ipcRenderer .invoke( 'renderer:run-collection-folder', @@ -531,6 +532,8 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp set(item, 'root.meta.seq', parentFolder?.items?.length + 1); const collectionPath = path.join(parentFolder.pathname, newFilename); + + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject); return; } @@ -857,6 +860,7 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => { return reject(new Error('Collection not found')); } + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, name) .then( @@ -885,6 +889,7 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch, const sanitizedName = sanitizeName(name); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, sanitizedName, variables) .then( @@ -918,6 +923,7 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g const sanitizedName = sanitizeName(name); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables) .then( @@ -954,6 +960,7 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di const oldName = environment.name; environment.name = sanitizedName; + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName)) @@ -977,6 +984,7 @@ export const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, g return reject(new Error('Environment not found')); } + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:delete-environment', collection.pathname, environment.name) .then(resolve) @@ -1000,6 +1008,7 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di environment.variables = variables; + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environment)) @@ -1025,7 +1034,8 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g if (environmentUid && !environmentName) { return reject(new Error('Environment not found')); } - + + const { ipcRenderer } = window; ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }}); dispatch(_selectEnvironment({ environmentUid, collectionUid })); @@ -1084,11 +1094,13 @@ export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getS const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); - if (!collection) { - return reject(new Error('Collection not found')); - } return new Promise((resolve, reject) => { + if (!collection) { + return reject(new Error('Collection not found')); + } + + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid) .then(resolve) @@ -1107,6 +1119,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge brunoConfig: brunoConfig }; + const { ipcRenderer } = window; + return new Promise((resolve, reject) => { ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => { collectionSchema diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js index def88f2b6..b208f14fd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { stringifyIfNot, uuid } from 'utils/common/index'; +import { uuid } from 'utils/common/index'; import { environmentSchema } from '@usebruno/schema'; import { cloneDeep } from 'lodash'; @@ -90,6 +90,7 @@ export const { export const addGlobalEnvironment = ({ name, variables = [] }) => (dispatch, getState) => { return new Promise((resolve, reject) => { const uid = uuid(); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-global-environment', { name, uid, variables }) .then(() => dispatch(_addGlobalEnvironment({ name, uid, variables }))) @@ -104,6 +105,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => ( const globalEnvironments = state.globalEnvironments.globalEnvironments; const baseEnv = globalEnvironments?.find(env => env?.uid == baseEnvUid) const uid = uuid(); + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables }) .then(() => dispatch(_copyGlobalEnvironment({ name, uid, variables: baseEnv.variables }))) @@ -114,6 +116,7 @@ export const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => ( export const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; const state = getState(); const globalEnvironments = state.globalEnvironments.globalEnvironments; const environment = globalEnvironments?.find(env => env?.uid == environmentUid) @@ -139,6 +142,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc return reject(new Error('Environment not found')); } + const { ipcRenderer } = window; environmentSchema .validate(environment) .then(() => ipcRenderer.invoke('renderer:save-global-environment', { @@ -155,6 +159,7 @@ export const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatc export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:select-global-environment', { environmentUid }) .then(() => dispatch(_selectGlobalEnvironment({ environmentUid }))) @@ -165,6 +170,7 @@ export const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; ipcRenderer .invoke('renderer:delete-global-environment', { environmentUid }) .then(() => dispatch(_deleteGlobalEnvironment({ environmentUid }))) @@ -175,6 +181,7 @@ export const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getSta export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => { return new Promise((resolve, reject) => { + const { ipcRenderer } = window; if (!globalEnvironmentVariables) resolve(); const state = getState(); diff --git a/packages/bruno-app/src/utils/codemirror/javascript-lint.js b/packages/bruno-app/src/utils/codemirror/javascript-lint.js index 88829322e..0038406aa 100644 --- a/packages/bruno-app/src/utils/codemirror/javascript-lint.js +++ b/packages/bruno-app/src/utils/codemirror/javascript-lint.js @@ -5,6 +5,8 @@ * Copyright (C) 2017 by Marijn Haverbeke and others */ +import { JSHINT } from 'jshint'; + let CodeMirror; const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; diff --git a/packages/bruno-app/src/utils/common/cache.js b/packages/bruno-app/src/utils/common/cache.js deleted file mode 100644 index d8cee9e50..000000000 --- a/packages/bruno-app/src/utils/common/cache.js +++ /dev/null @@ -1,10 +0,0 @@ -class Cache { - get(key) { - return window.localStorage.getItem(key); - } - set(key, val) { - window.localStorage.setItem(key, val); - } -} - -module.exports = new Cache(); diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 8bafbb8f9..47055cbb7 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -53,7 +53,7 @@ export const safeStringifyJSON = (obj, indent = false) => { export const convertToCodeMirrorJson = (obj) => { try { - return JSON5.stringify(obj).slice(1, -1); + return JSON.stringify(obj, null, 2).slice(1, -1); } catch (e) { return obj; } From dfa951e574c94903a4c0b42232aae266ee8d934a Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Thu, 8 May 2025 21:51:21 +0530 Subject: [PATCH 796/904] 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") + }) +} From a125781312201c613698276c2aa822e237aa620e Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Thu, 8 May 2025 21:52:58 +0530 Subject: [PATCH 797/904] feat/replace unsupported characters in env key during pm import (#4618) --------- Co-authored-by: sanjai0py --- packages/bruno-converters/src/constants/index.js | 3 +++ packages/bruno-converters/src/constants/regex.js | 1 + .../src/postman/postman-env-to-bruno-env.js | 6 +++++- packages/bruno-converters/src/postman/postman-to-bruno.js | 3 ++- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-converters/src/constants/index.js create mode 100644 packages/bruno-converters/src/constants/regex.js diff --git a/packages/bruno-converters/src/constants/index.js b/packages/bruno-converters/src/constants/index.js new file mode 100644 index 000000000..723a09fea --- /dev/null +++ b/packages/bruno-converters/src/constants/index.js @@ -0,0 +1,3 @@ +import { invalidVariableCharacterRegex } from './regex'; + +export { invalidVariableCharacterRegex }; diff --git a/packages/bruno-converters/src/constants/regex.js b/packages/bruno-converters/src/constants/regex.js new file mode 100644 index 000000000..989dbc1c3 --- /dev/null +++ b/packages/bruno-converters/src/constants/regex.js @@ -0,0 +1 @@ +export const invalidVariableCharacterRegex = /[^\w-.]/g; \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js b/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js index c61c98d71..52d60b08b 100644 --- a/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js +++ b/packages/bruno-converters/src/postman/postman-env-to-bruno-env.js @@ -1,4 +1,7 @@ import each from 'lodash/each'; +import { invalidVariableCharacterRegex } from '../constants'; +import { uuid } from '../common'; + const isSecret = (type) => { return type === 'secret'; }; @@ -8,7 +11,8 @@ const importPostmanEnvironmentVariables = (brunoEnvironment, values) => { each(values, (i) => { const brunoEnvironmentVariable = { - name: i.key, + uid: uuid(), + name: i.key.replace(invalidVariableCharacterRegex, '_'), value: i.value, enabled: i.enabled, secret: isSecret(i.type) diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index c14e12172..8ae8e195e 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -2,6 +2,7 @@ import get from 'lodash/get'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; import each from 'lodash/each'; import postmanTranslation from './postman-translations'; +import { invalidVariableCharacterRegex } from '../constants/index'; const parseGraphQLRequest = (graphqlSource) => { try { @@ -120,7 +121,7 @@ const importScriptsFromEvents = (events, requestObject) => { const importCollectionLevelVariables = (variables, requestObject) => { const vars = variables.map((v) => ({ uid: uuid(), - name: v.key, + name: v.key.replace(invalidVariableCharacterRegex, '_'), value: v.value, enabled: true })); From c47bc86d37799fee8a9b2cb5e21e82cfadadf1bc Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 8 May 2025 21:53:14 +0530 Subject: [PATCH 798/904] fix: Improve Variable Highlighting in Code Generation and Environment Views (#4593) --- .../EnvironmentDetails/EnvironmentVariables/index.js | 11 +++++++++-- .../CollectionItem/GenerateCodeItem/CodeView/index.js | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index ab2ea7691..c777aa85f 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; @@ -13,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex'; import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { Tooltip } from 'react-tooltip'; +import { getGlobalEnvironmentVariables } from 'utils/collections'; const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const addButtonRef = useRef(null); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); + + let _collection = cloneDeep(collection); + + const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); + _collection.globalEnvironmentVariables = globalEnvironmentVariables; const formik = useFormik({ enableReinitialize: true, @@ -160,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
{ Date: Fri, 9 May 2025 00:07:58 +0530 Subject: [PATCH 799/904] fix tests for postmanToBrunoEnvironment function --- .../tests/postman/postman-env-to-bruno-env.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js b/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js index 665a52e75..6101548bd 100644 --- a/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js +++ b/packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js @@ -32,12 +32,14 @@ describe('postmanToBrunoEnvironment Function', () => { value: 'value1', enabled: true, secret: false, + uid: "mockeduuidvalue123456", }, { name: 'var2', value: 'value2', enabled: false, secret: true, + uid: "mockeduuidvalue123456", }, ], }; From e4f48e81fc54970ce5bfe3a60a2edd5c1f75e497 Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Fri, 9 May 2025 14:16:29 +0530 Subject: [PATCH 800/904] feat: add setBody test script to bruno-tests collection (#4415) --- .../scripting/api/res/setBody/array.bru | 45 +++++++++++++++++++ .../scripting/api/res/setBody/boolean.bru | 33 ++++++++++++++ .../scripting/api/res/setBody/folder.bru | 3 ++ .../scripting/api/res/setBody/null.bru | 33 ++++++++++++++ .../scripting/api/res/setBody/number.bru | 33 ++++++++++++++ .../scripting/api/res/setBody/object.bru | 35 +++++++++++++++ .../scripting/api/res/setBody/string.bru | 33 ++++++++++++++ .../scripting/api/res/setBody/undefined.bru | 36 +++++++++++++++ 8 files changed, 251 insertions(+) create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/array.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/null.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/number.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/object.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/string.bru create mode 100644 packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru new file mode 100644 index 000000000..7e3492abe --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/array.bru @@ -0,0 +1,45 @@ +meta { + name: array + type: http + seq: 6 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + const obj = { + hello : "hello from post-res" + } + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON + res.setBody(["hello",1, null, undefined, true, obj]) +} + +tests { + test("res.setBody(array)", function() { + const body = res.getBody(); + expect(body.length).to.eql(6); + expect(body[0]).to.eql("hello") + expect(body[1]).to.eql(1) + expect(body[2]).to.be.null + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON + expect(body[3]).to.be.undefined; + expect(body[4]).to.eql(true) + expect(body[5].hello).to.eql("hello from post-res") + + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru new file mode 100644 index 000000000..28513d679 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru @@ -0,0 +1,33 @@ +meta { + name: boolean + type: http + seq: 7 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(true) +} + +tests { + test("res.setBody(boolean)", function() { + const body = res.getBody(); + expect(body).to.be.true; + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru new file mode 100644 index 000000000..4e66417c7 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru @@ -0,0 +1,3 @@ +meta { + name: setBody +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru new file mode 100644 index 000000000..8d8d2dcf5 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/null.bru @@ -0,0 +1,33 @@ +meta { + name: null + type: http + seq: 6 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(null) +} + +tests { + test("res.setBody(null)", function() { + const body = res.getBody(); + expect(body).to.be.null; + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru new file mode 100644 index 000000000..a90aec802 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/number.bru @@ -0,0 +1,33 @@ +meta { + name: number + type: http + seq: 3 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody(2) +} + +tests { + test("res.setBody(number)", function() { + const body = res.getBody(); + expect(body).to.eql(2); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru new file mode 100644 index 000000000..02ad78a2e --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/object.bru @@ -0,0 +1,35 @@ +meta { + name: object + type: http + seq: 1 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody({ + hello : "hello from post-res" + }) +} + +tests { + test("res.setBody(object)", function() { + const body = res.getBody(); + expect(body.hello).to.eql("hello from post-res"); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru new file mode 100644 index 000000000..a87e57bd4 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/string.bru @@ -0,0 +1,33 @@ +meta { + name: string + type: http + seq: 4 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + res.setBody("hello from post-res") +} + +tests { + test("res.setBody(string)", function() { + const body = res.getBody(); + expect(body).to.eql("hello from post-res"); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru b/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru new file mode 100644 index 000000000..0dc918f1f --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru @@ -0,0 +1,36 @@ +meta { + name: undefined + type: http + seq: 7 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:post-response { + // if undefined is not passed to res.setBody() the test fails in only safe-mode, needs to check, undefined is not a valid JSON + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON + res.setBody(undefined) +} + +tests { + test("res.setBody(undefined)", function() { + const body = res.getBody(); + // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON + expect(body).to.be.undefined; + }); + +} From 0cdcb83a7ad83d256403a047347e2c933c64e12a Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Fri, 9 May 2025 15:48:49 +0530 Subject: [PATCH 801/904] Adding build instructions for new packages --- contributing.md | 2 ++ docs/contributing/contributing_hi.md | 2 ++ docs/contributing/contributing_ja.md | 2 ++ docs/contributing/contributing_kr.md | 2 ++ docs/contributing/contributing_nl.md | 2 ++ docs/contributing/contributing_sk.md | 2 ++ 6 files changed, 12 insertions(+) diff --git a/contributing.md b/contributing.md index 14e561b0b..13bbec333 100644 --- a/contributing.md +++ b/contributing.md @@ -62,6 +62,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # bundle js sandbox libraries npm run sandbox:bundle-libraries --workspace=packages/bruno-js diff --git a/docs/contributing/contributing_hi.md b/docs/contributing/contributing_hi.md index 6af26f1d2..ab79de31b 100644 --- a/docs/contributing/contributing_hi.md +++ b/docs/contributing/contributing_hi.md @@ -40,6 +40,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # Next.js ऐप चलाएँ (टर्मिनल 1 पर) npm run dev:web diff --git a/docs/contributing/contributing_ja.md b/docs/contributing/contributing_ja.md index 19da3dfda..dba6d6435 100644 --- a/docs/contributing/contributing_ja.md +++ b/docs/contributing/contributing_ja.md @@ -40,6 +40,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # run next app (terminal 1) npm run dev:web diff --git a/docs/contributing/contributing_kr.md b/docs/contributing/contributing_kr.md index f5a100510..b0f655990 100644 --- a/docs/contributing/contributing_kr.md +++ b/docs/contributing/contributing_kr.md @@ -40,6 +40,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # next 앱 실행 (1번 터미널) npm run dev:web diff --git a/docs/contributing/contributing_nl.md b/docs/contributing/contributing_nl.md index 707e342c4..f8e34f4ca 100644 --- a/docs/contributing/contributing_nl.md +++ b/docs/contributing/contributing_nl.md @@ -40,6 +40,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # draai next app (terminal 1) npm run dev:web diff --git a/docs/contributing/contributing_sk.md b/docs/contributing/contributing_sk.md index 10d2ce0f5..69dcb6740 100644 --- a/docs/contributing/contributing_sk.md +++ b/docs/contributing/contributing_sk.md @@ -42,6 +42,8 @@ npm i --legacy-peer-deps npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests # spustite ďalšiu aplikáciu (terminál 1) npm run dev:web From fb7d247fa7f11252ef541e982cfce05c654aece4 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 9 May 2025 17:37:16 +0530 Subject: [PATCH 802/904] fix: add missing deps --- package-lock.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b45baf9c..8bf023cfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8290,7 +8290,6 @@ "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": { @@ -8313,7 +8312,6 @@ "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": "*", @@ -8324,7 +8322,6 @@ "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": { @@ -13266,7 +13263,6 @@ "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": { @@ -14733,6 +14729,15 @@ "dev": true, "license": "ISC" }, + "node_modules/flow-parser": { + "version": "0.269.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.269.1.tgz", + "integrity": "sha512-2Yr0kqvT7RwaGL192nT78O5AWJeECQjl0NEzBkMsx8OJt63BvNl5yvSIbE4qZ1VDSjEkhbUgaWYdwX354bVNjw==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -26469,7 +26474,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", From 3dfb158382aad2d2a5b47f509862e944cc12d0c1 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 9 May 2025 17:56:32 +0530 Subject: [PATCH 803/904] feat: add missing translations --- .../src/postman/postman-translations.js | 5 ++ .../src/utils/jscode-shift-translator.js | 31 ++++++- .../transpiler-tests/combined.test.js | 81 ++++++++++++++++++- .../legacy-tests-syntax.test.js | 4 +- .../transpiler-tests/response.test.js | 73 ++++++++++++++++- .../transpiler-tests/variables.test.js | 27 ++++++- 6 files changed, 211 insertions(+), 10 deletions(-) diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 23195b5b8..0f9941e4f 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -20,6 +20,11 @@ const replacements = { 'pm\\.response\\.text\\(\\)': 'JSON.stringify(res.getBody())', 'pm\\.expect\\.fail\\(': 'expect.fail(', 'pm\\.response\\.responseTime': 'res.getResponseTime()', + 'pm\\.globals\\.set\\(': 'bru.setGlobalEnvVar(', + 'pm\\.globals\\.get\\(': 'bru.getGlobalEnvVar(', + 'pm\\.response\\.headers\\.get\\(': 'res.getHeader(', + 'pm\\.response\\.to\\.have\\.body\\(': 'expect(res.getBody()).to.equal(', + 'pm\\.response\\.to\\.have\\.header\\(': 'expect(Object.keys(res.getHeaders())).to.include(', 'pm\\.environment\\.name': 'bru.getEnvName()', 'pm\\.response\\.status': 'res.statusText', 'pm\\.response\\.headers': 'res.getHeaders()', diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 924f4eb1d..04555b072 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -38,6 +38,10 @@ function getMemberExpressionString(node) { // Simple 1:1 translations for straightforward replacements const simpleTranslations = { + // Global Variables + 'pm.globals.get': 'bru.getGlobalEnvVar', + 'pm.globals.set': 'bru.setGlobalEnvVar', + // Environment variables 'pm.environment.get': 'bru.getEnvVar', 'pm.environment.set': 'bru.setEnvVar', @@ -124,7 +128,12 @@ const complexTransformations = [ return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]); } }, - + { + pattern: 'pm.response.headers.get', + transform: (path, j) => { + return j.callExpression(j.identifier('res.getHeader'), path.parent.value.arguments); + } + }, // Handle pm.response.to.have.status { pattern: 'pm.response.to.have.status', @@ -191,6 +200,24 @@ const complexTransformations = [ } }, + // handle pm.response.to.have.body to expect(res.getBody()).to.equal(arg) + { + pattern: 'pm.response.to.have.body', + transform: (path, j) => { + const callExpr = path.parent.value; + + const args = callExpr.arguments; + + return j.callExpression( + j.memberExpression( + j.callExpression(j.identifier('expect'), [j.identifier('res.getBody()')]), + j.identifier('to.equal') + ), + args + ); + + } + }, // Handle pm.execution.setNextRequest(null) { @@ -225,7 +252,7 @@ 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']); +const varInitsToReplace = new Set(['pm', 'postman', 'pm.request','pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution', 'pm.globals']); /** * Process all transformations (both simple and complex) in the AST in a single pass 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 index 8d3508e05..4916a10c0 100644 --- 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 @@ -411,8 +411,85 @@ describe('Combined API Features Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - const globals = pm.globals; - const key = globals.get("key"); + const key = bru.getGlobalEnvVar("key"); `); }) + + it('should handle pm.response.to.have.body integrated with other assertions', () => { + const code = ` + pm.test("Response validation", function() { + pm.response.to.have.status(200); + pm.response.to.have.body({"success": true}); + pm.response.to.have.header("Content-Type", "application/json"); + }); + `; + const translatedCode = translateCode(code); + + const expectedOutput = ` + test("Response validation", function() { + expect(res.getStatus()).to.equal(200); + expect(res.getBody()).to.equal({"success": true}); + expect(res.getHeaders()).to.have.property("Content-Type".toLowerCase(), "application/json"); + }); + `; + expect(translatedCode).toBe(expectedOutput); + }); + + it('should handle pm.response.to.have.body with dynamic content', () => { + const code = ` + const expectedResponse = { + id: pm.environment.get("userId"), + token: pm.variables.get("authToken"), + timestamp: new Date().getTime() + }; + + pm.test("Dynamic response validation", function() { + pm.response.to.have.body(expectedResponse); + }); + `; + const translatedCode = translateCode(code); + + const expectedOutput = ` + const expectedResponse = { + id: bru.getEnvVar("userId"), + token: bru.getVar("authToken"), + timestamp: new Date().getTime() + }; + + test("Dynamic response validation", function() { + expect(res.getBody()).to.equal(expectedResponse); + }); + ` + expect(translatedCode).toBe(expectedOutput); + }); + + it('should handle pm.response.to.have.body in control structures', () => { + const code = ` + const jsonData = pm.response.json(); + + if (jsonData.status === "success") { + pm.response.to.have.body({ + status: "success", + data: jsonData.data + }); + } else { + pm.expect(jsonData.error).to.exist; + } + `; + const translatedCode = translateCode(code); + + const expectedOutput = ` + const jsonData = res.getBody(); + + if (jsonData.status === "success") { + expect(res.getBody()).to.equal({ + status: "success", + data: jsonData.data + }); + } else { + expect(jsonData.error).to.exist; + } + `; + expect(translatedCode).toBe(expectedOutput); + }); }); \ 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 index e548aa03c..34c6f32a6 100644 --- 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 @@ -27,7 +27,7 @@ describe('Legacy Tests[] Syntax Translation', () => { 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; + expect(Boolean(res.getHeader("Content-Type") === "application/json")).to.be.true; });`); }); @@ -273,7 +273,7 @@ describe('Legacy Tests[] Syntax Translation', () => { 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('expect(Boolean(res.getHeader("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;'); 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 index 7fd4d902b..3a1e45dbc 100644 --- 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 @@ -32,6 +32,12 @@ describe('Response Translation', () => { expect(translatedCode).toBe('console.log("Status text:", res.statusText);'); }); + it('should translate pm.response.headers', () => { + const code = 'console.log("Headers:", pm.response.headers);'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('console.log("Headers:", res.getHeaders());'); + }); + // Complex response transformations it('should transform pm.response.to.have.status', () => { const code = 'pm.response.to.have.status(201);'; @@ -178,6 +184,17 @@ describe('Response Translation', () => { `); }); + it('should translate response.headers', () => { + const code = ` + const resp = pm.response; + const headers = resp.headers; + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const headers = res.getHeaders(); + `); + }); + it('should translate pm.response.statusText', () => { const code = ` const resp = pm.response; @@ -296,8 +313,8 @@ describe('Response Translation', () => { 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('const contentType = res.getHeader(\'Content-Type\');'); + expect(translatedCode).toContain('const contentLength = res.getHeader(\'Content-Length\');'); expect(translatedCode).toContain('console.log("contentType", contentType);'); expect(translatedCode).toContain('console.log("contentLength", contentLength);'); expect(translatedCode).not.toContain('pm.test') @@ -340,7 +357,7 @@ describe('Response Translation', () => { 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('if (res.getHeader(\'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) {'); @@ -486,4 +503,54 @@ describe('Response Translation', () => { expect(translatedCode).toContain('checkHeaderPresent("Authorization");'); expect(translatedCode).toContain('validateHeader("Content-Type", "application/json");'); }); + + it('should transform pm.response.to.have.body with string literal', () => { + const code = 'pm.response.to.have.body("Expected response body");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getBody()).to.equal("Expected response body");'); + }); + + it('should transform pm.response.to.have.body with variable parameter', () => { + const code = ` + const expectedBody = {"status": "success", "data": [1, 2, 3]}; + pm.response.to.have.body(expectedBody); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const expectedBody = {"status": "success", "data": [1, 2, 3]};'); + expect(translatedCode).toContain('expect(res.getBody()).to.equal(expectedBody);'); + }); + + it('should transform pm.response.to.have.body with JSON object', () => { + const code = `pm.response.to.have.body({"status": "success", "message": "Operation completed"});`; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('expect(res.getBody()).to.equal({"status": "success", "message": "Operation completed"});'); + }); + + it('should transform pm.response.to.have.body inside test function', () => { + const code = ` + pm.test("Response body validation", function() { + const expectedResponse = {"result": true}; + pm.response.to.have.body(expectedResponse); + }); + `; + const translatedCode = translateCode(code); + const expectedOutput = ` + test("Response body validation", function() { + const expectedResponse = {"result": true}; + expect(res.getBody()).to.equal(expectedResponse); + }); + ` + expect(translatedCode).toBe(expectedOutput); + }); + + it('should transform pm.response.to.have.body with response alias', () => { + const code = ` + const resp = pm.response; + resp.to.have.body({"status": "ok"}); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + expect(res.getBody()).to.equal({"status": "ok"}); + `); + }); }); \ 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 index b704e4a7e..b4439f826 100644 --- 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 @@ -45,6 +45,18 @@ describe('Variables Translation', () => { expect(translatedCode).toBe('bru.deleteVar("tempVar");'); }); + it('should handle pm.globals.get', () => { + const code = 'pm.globals.get("test");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getGlobalEnvVar("test");'); + }); + + it('should handle pm.globals.set', () => { + const code = 'pm.globals.set("test", "value");'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setGlobalEnvVar("test", "value");'); + }); + // Alias tests for variables it('should handle variables aliases', () => { const code = ` @@ -79,6 +91,19 @@ describe('Variables Translation', () => { `); }); + it('should handle pm.globals aliases', () => { + const code = ` + const globals = pm.globals; + const get = globals.get("test"); + const set = globals.set("test", "value"); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + const get = bru.getGlobalEnvVar("test"); + const set = bru.setGlobalEnvVar("test", "value"); + `); + }) + // Combined tests it('should handle conditional expressions with variable calls', () => { const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";'; @@ -124,5 +149,5 @@ describe('Variables Translation', () => { 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 From 1daeb8fe93df96a22bd0797e6a2eef1a969ee55e Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 9 May 2025 19:12:52 +0530 Subject: [PATCH 804/904] fix: regex tranasaltion for to.have.headers --- packages/bruno-converters/src/postman/postman-translations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 0f9941e4f..cb0656e38 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -24,7 +24,7 @@ const replacements = { 'pm\\.globals\\.get\\(': 'bru.getGlobalEnvVar(', 'pm\\.response\\.headers\\.get\\(': 'res.getHeader(', 'pm\\.response\\.to\\.have\\.body\\(': 'expect(res.getBody()).to.equal(', - 'pm\\.response\\.to\\.have\\.header\\(': 'expect(Object.keys(res.getHeaders())).to.include(', + 'pm\\.response\\.to\\.have\\.header\\(': 'expect(res.getHeaders()).to.have.property(', 'pm\\.environment\\.name': 'bru.getEnvName()', 'pm\\.response\\.status': 'res.statusText', 'pm\\.response\\.headers': 'res.getHeaders()', From a9982d6e28dcfbf2c8efa317905fb4b06470c409 Mon Sep 17 00:00:00 2001 From: lohit Date: Sat, 10 May 2025 15:11:26 +0530 Subject: [PATCH 805/904] removed unused collectionPathnmae prop to components (#4640) Co-authored-by: lohit --- .../CollectionSettings/Overview/Info/index.js | 2 +- .../src/components/RequestTabs/RequestTab/index.js | 3 +-- .../bruno-app/src/components/RequestTabs/index.js | 2 +- .../CollectionItem/CloneCollectionItem/index.js | 3 +-- .../CollectionItem/RenameCollectionItem/index.js | 3 +-- .../Collections/Collection/CollectionItem/index.js | 14 +++++++------- .../Sidebar/Collections/Collection/index.js | 12 ++++++------ 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index e08866ccf..ebe29a21a 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -72,7 +72,7 @@ const Info = ({ collection }) => {
- {showShareCollectionModal && } + {showShareCollectionModal && }
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 182b7ff7e..816b00e25 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -263,14 +263,13 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col return ( {showAddNewRequestModal && ( - setShowAddNewRequestModal(false)} /> + setShowAddNewRequestModal(false)} /> )} {showCloneRequestModal && ( setShowCloneRequestModal(false)} /> )} diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index 1e1503a85..d50d528b3 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -79,7 +79,7 @@ const RequestTabs = () => { return ( {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {collectionRequestTabs && collectionRequestTabs.length ? ( <> diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 31a58c2dd..3a4e2e9c8 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -15,7 +15,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { +const CloneCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -172,7 +172,6 @@ const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 583a914b0..6b0cb086a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -16,7 +16,7 @@ import Portal from 'components/Portal'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; -const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => { +const RenameCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); @@ -191,7 +191,6 @@ const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose ) : (
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 6c34dcaa3..0f59f4a69 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -7,7 +7,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; -import { handleCollectionItemDrop, moveItem, sendRequest, showInFolder, updateItemsSequences } from 'providers/ReduxStore/slices/collections/actions'; +import { handleCollectionItemDrop, sendRequest, showInFolder } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -17,7 +17,7 @@ import CloneCollectionItem from './CloneCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem'; import RunCollectionItem from './RunCollectionItem'; import GenerateCodeItem from './GenerateCodeItem'; -import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs'; +import { isItemARequest, isItemAFolder } from 'utils/tabs'; import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search'; import { getDefaultRequestPaneTab } from 'utils/collections'; import { hideHomePage } from 'providers/ReduxStore/slices/app'; @@ -295,19 +295,19 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) return ( {renameItemModalOpen && ( - setRenameItemModalOpen(false)} /> + setRenameItemModalOpen(false)} /> )} {cloneItemModalOpen && ( - setCloneItemModalOpen(false)} /> + setCloneItemModalOpen(false)} /> )} {deleteItemModalOpen && ( - setDeleteItemModalOpen(false)} /> + setDeleteItemModalOpen(false)} /> )} {newRequestModalOpen && ( - setNewRequestModalOpen(false)} /> + setNewRequestModalOpen(false)} /> )} {newFolderModalOpen && ( - setNewFolderModalOpen(false)} /> + setNewFolderModalOpen(false)} /> )} {runCollectionModalOpen && ( setRunCollectionModalOpen(false)} /> diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 16fa52b21..995be522c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -183,19 +183,19 @@ const Collection = ({ collection, searchText }) => { return ( - {showNewRequestModal && setShowNewRequestModal(false)} />} - {showNewFolderModal && setShowNewFolderModal(false)} />} + {showNewRequestModal && setShowNewRequestModal(false)} />} + {showNewFolderModal && setShowNewFolderModal(false)} />} {showRenameCollectionModal && ( - setShowRenameCollectionModal(false)} /> + setShowRenameCollectionModal(false)} /> )} {showRemoveCollectionModal && ( - setShowRemoveCollectionModal(false)} /> + setShowRemoveCollectionModal(false)} /> )} {showShareCollectionModal && ( - setShowShareCollectionModal(false)} /> + setShowShareCollectionModal(false)} /> )} {showCloneCollectionModalOpen && ( - setShowCloneCollectionModalOpen(false)} /> + setShowCloneCollectionModalOpen(false)} /> )}
Date: Sun, 11 May 2025 23:08:44 +0530 Subject: [PATCH 806/904] refactored getCollectionJsonFromPathname function and added tests --- packages/bruno-cli/src/commands/run.js | 120 ++--------- packages/bruno-cli/src/utils/collection.js | 109 ++++++++++ .../collection-json-from-pathname.spec.js | 192 ++++++++++++++++++ .../collection/bruno.json | 31 +++ .../collection/collection.bru | 38 ++++ .../collection/folder_1/folder.bru | 4 + .../collection/folder_1/folder_1/folder.bru | 4 + .../folder_1/folder_1/request_1.bru | 27 +++ .../folder_1/folder_1/request_2.bru | 27 +++ .../folder_1/folder_1/request_3.bru | 27 +++ .../collection/folder_1/folder_2/folder.bru | 4 + .../folder_1/folder_2/request_1.bru | 27 +++ .../folder_1/folder_2/request_2.bru | 27 +++ .../folder_1/folder_2/request_3.bru | 27 +++ .../collection/folder_1/request_1.bru | 27 +++ .../collection/folder_1/request_2.bru | 27 +++ .../collection/folder_1/request_3.bru | 27 +++ .../collection/folder_2/folder.bru | 4 + .../collection/folder_2/request_1.bru | 27 +++ .../collection/folder_2/request_2.bru | 27 +++ .../collection/folder_2/request_3.bru | 27 +++ .../collection/request_1.bru | 27 +++ .../collection/request_2.bru | 58 ++++++ .../collection/request_3.bru | 27 +++ 24 files changed, 836 insertions(+), 106 deletions(-) create mode 100644 packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/bruno.json create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/collection.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/folder.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/folder.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/folder.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_2.bru create mode 100644 packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 9801494f8..25bc1d1cd 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -12,7 +12,7 @@ const { rpad } = require('../utils/common'); const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); const { dotenvToJson } = require('@usebruno/lang'); const constants = require('../constants'); -const { findItemInCollection, getAllRequestsInFolder } = require('../utils/collection'); +const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname } = require('../utils/collection'); const command = 'run [filename]'; const desc = 'Run a request'; @@ -76,87 +76,6 @@ const printRunSummary = (results) => { } }; -const createCollectionFromPath = (collectionPath) => { - const environmentsPath = path.join(collectionPath, `environments`); - const getFilesInOrder = (collectionPath) => { - let collection = { - pathname: collectionPath - }; - const traverse = (currentPath) => { - const filesInCurrentDir = fs.readdirSync(currentPath); - - if (currentPath.includes('node_modules')) { - return; - } - const currentDirItems = []; - for (const file of filesInCurrentDir) { - const filePath = path.join(currentPath, file); - const stats = fs.lstatSync(filePath); - if ( - stats.isDirectory() && - filePath !== environmentsPath && - !filePath.startsWith('.git') && - !filePath.startsWith('node_modules') - ) { - let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) } - const folderBruJson = getFolderRoot(filePath); - if (folderBruJson) { - folderItem.root = folderBruJson; - folderItem.seq = folderBruJson.meta.seq; - } - currentDirItems.push(folderItem); - } - } - - for (const file of filesInCurrentDir) { - if (['collection.bru', 'folder.bru'].includes(file)) { - continue; - } - const filePath = path.join(currentPath, file); - const stats = fs.lstatSync(filePath); - - if (!stats.isDirectory() && path.extname(filePath) === '.bru') { - const bruContent = fs.readFileSync(filePath, 'utf8'); - const requestItem = bruToJson(bruContent); - currentDirItems.push({ - name: file, - pathname: filePath, - ...requestItem - }); - } - } - const sortedFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder')?.sort((a, b) => a.seq - b.seq); - const sortedRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder')?.sort((a, b) => a.seq - b.seq); - return sortedFolderItems?.concat(sortedRequestItems); - }; - collection.items = traverse(collectionPath); - return collection; - }; - return getFilesInOrder(collectionPath); -}; - -const getCollectionRoot = (dir) => { - const collectionRootPath = path.join(dir, 'collection.bru'); - const exists = fs.existsSync(collectionRootPath); - if (!exists) { - return {}; - } - - const content = fs.readFileSync(collectionRootPath, 'utf8'); - return collectionBruToJson(content); -}; - -const getFolderRoot = (dir) => { - const folderRootPath = path.join(dir, 'folder.bru'); - const exists = fs.existsSync(folderRootPath); - if (!exists) { - return null; - } - - const content = fs.readFileSync(folderRootPath, 'utf8'); - return collectionBruToJson(content); -}; - const getJsSandboxRuntime = (sandbox) => { return sandbox === 'safe' ? 'quickjs' : 'vm2'; }; @@ -249,7 +168,10 @@ const builder = async (yargs) => { type:"number", description: "Delay between each requests (in miliseconds)" }) - + .option('collection-pathname', { + type: "string", + description: "Collection root pathname" + }) .example('$0 run request.bru', 'Run a request') .example('$0 run request.bru --env local', 'Run a request with the environment set to local') .example('$0 run folder', 'Run all requests in a folder') @@ -315,29 +237,15 @@ const handler = async function (argv) { reporterSkipAllHeaders, reporterSkipHeaders, clientCertConfig, - delay + delay, + collectionPathname } = argv; - const collectionPath = process.cwd(); + const collectionPath = collectionPathname || process.cwd(); - // todo - // right now, bru must be run from the root of the collection - // will add support in the future to run it from anywhere inside the collection - const brunoJsonPath = path.join(collectionPath, 'bruno.json'); - const brunoJsonExists = await exists(brunoJsonPath); - if (!brunoJsonExists) { - console.error(chalk.red(`You can run only at the root of a collection`)); - process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); - } + filename = path.join(collectionPath, filename); - const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8'); - const brunoConfig = JSON.parse(brunoConfigFile); - const collectionRoot = getCollectionRoot(collectionPath); - let collection = createCollectionFromPath(collectionPath); - collection = { - brunoConfig, - root: collectionRoot, - ...collection - } + let collection = createCollectionJsonFromPathname(collectionPath); + const { root: collectionRoot, brunoConfig } = collection; if (clientCertConfig) { try { @@ -358,10 +266,10 @@ const handler = async function (argv) { } if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) { - if (brunoConfig.clientCertificates) { - brunoConfig.clientCertificates.certs.push(...clientCertConfigJson.certs); + if (clientCertificates) { + clientCertificates.certs.push(...clientCertConfigJson.certs); } else { - brunoConfig.clientCertificates = { certs: clientCertConfigJson.certs }; + clientCertificates = { certs: clientCertConfigJson.certs }; } console.log(chalk.green(`Client certificates has been added`)); } else { diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index a300e415f..3616c1f87 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -4,6 +4,112 @@ const fs = require('fs'); const path = require('path'); const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang'); const { sanitizeName } = require('./filesystem'); +const { bruToJson, collectionBruToJson } = require('./bru'); +const constants = require('../constants'); +const chalk = require('chalk'); + +const createCollectionJsonFromPathname = (collectionPath) => { + const environmentsPath = path.join(collectionPath, `environments`); + + // get the collection bruno json config [/bruno.json] + const brunoConfig = getCollectionBrunoJsonConfig(collectionPath); + + // get the collection root [/collection.bru] + const collectionRoot = getCollectionRoot(collectionPath); + + // get the collection items recursively + const traverse = (currentPath) => { + const filesInCurrentDir = fs.readdirSync(currentPath); + if (currentPath.includes('node_modules')) { + return; + } + const currentDirItems = []; + for (const file of filesInCurrentDir) { + const filePath = path.join(currentPath, file); + const stats = fs.lstatSync(filePath); + if (stats.isDirectory()) { + if (filePath === environmentsPath) continue; + if (filePath.startsWith('.git') || filePath.startsWith('node_modules')) continue; + + // get the folder root + let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) } + const folderBruJson = getFolderRoot(filePath); + if (folderBruJson) { + folderItem.root = folderBruJson; + folderItem.seq = folderBruJson.meta.seq; + } + currentDirItems.push(folderItem); + } + else { + if (['collection.bru', 'folder.bru'].includes(file)) continue; + if (path.extname(filePath) !== '.bru') continue; + + // get the request item + const bruContent = fs.readFileSync(filePath, 'utf8'); + const requestItem = bruToJson(bruContent); + currentDirItems.push({ + name: file, + pathname: filePath, + ...requestItem + }); + } + } + let currentDirFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder'); + let sortedFolderItems = currentDirFolderItems?.sort((a, b) => a.seq - b.seq); + + let currentDirRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder'); + let sortedRequestItems = currentDirRequestItems?.sort((a, b) => a.seq - b.seq); + + return sortedFolderItems?.concat(sortedRequestItems); + }; + let collectionItems = traverse(collectionPath); + + let collection = { + brunoConfig, + root: collectionRoot, + pathname: collectionPath, + items: collectionItems + } + + return collection; +}; + +const getCollectionBrunoJsonConfig = (dir) => { + // right now, bru must be run from the root of the collection + // will add support in the future to run it from anywhere inside the collection + const brunoJsonPath = path.join(dir, 'bruno.json'); + const brunoJsonExists = fs.existsSync(brunoJsonPath); + if (!brunoJsonExists) { + console.error(chalk.red(`You can run only at the root of a collection`)); + process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); + } + + const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8'); + const brunoConfig = JSON.parse(brunoConfigFile); + return brunoConfig; +} + +const getCollectionRoot = (dir) => { + const collectionRootPath = path.join(dir, 'collection.bru'); + const exists = fs.existsSync(collectionRootPath); + if (!exists) { + return {}; + } + + const content = fs.readFileSync(collectionRootPath, 'utf8'); + return collectionBruToJson(content); +}; + +const getFolderRoot = (dir) => { + const folderRootPath = path.join(dir, 'folder.bru'); + const exists = fs.existsSync(folderRootPath); + if (!exists) { + return null; + } + + const content = fs.readFileSync(folderRootPath, 'utf8'); + return collectionBruToJson(content); +}; const mergeHeaders = (collection, request, requestTreePath) => { let headers = new Map(); @@ -355,7 +461,10 @@ const processCollectionItems = async (items = [], currentPath) => { } }; + + module.exports = { + createCollectionJsonFromPathname, mergeHeaders, mergeVars, mergeScripts, diff --git a/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js b/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js new file mode 100644 index 000000000..0886b0beb --- /dev/null +++ b/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js @@ -0,0 +1,192 @@ +const path = require("node:path"); +const { describe, it, expect } = require('@jest/globals'); +const constants = require('../../src/constants'); +const { createCollectionJsonFromPathname } = require('../../src/utils/collection'); + +describe('create collection json from pathname', () => { + it("should throw an error when the pathname is not a valid bruno collection root", () => { + const invalidCollectionPathname = path.join(__dirname, './fixtures/collection-invalid'); + jest.spyOn(console, 'error').mockImplementation(() => { }); + let mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(code); }); + try { createCollectionJsonFromPathname(invalidCollectionPathname); } catch {} + expect(mockProcessExit).toHaveBeenCalledWith(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); + jest.restoreAllMocks(); + }) + + it("creates a bruno collection json from the collection bru files", () => { + const collectionPathname = path.join(__dirname, './fixtures/collection-json-from-pathname/collection'); + const outputCollectionJson = createCollectionJsonFromPathname(collectionPathname); + + let c = outputCollectionJson; + expect(c).toBeDefined(); + + /* + collection bruno.json + */ + + expect(c).toHaveProperty('brunoConfig.version', "1"); + expect(c).toHaveProperty('brunoConfig.name', 'collection'); + expect(c).toHaveProperty('brunoConfig.type', 'collection'); + expect(c).toHaveProperty('brunoConfig.ignore', ["node_modules", ".git"]); + expect(c).toHaveProperty('brunoConfig.proxy.enabled', false); + expect(c).toHaveProperty('brunoConfig.proxy.protocol', 'http'); + expect(c).toHaveProperty('brunoConfig.proxy.hostname', ''); + expect(c).toHaveProperty('brunoConfig.proxy.port', 3000); + expect(c).toHaveProperty('brunoConfig.proxy.auth.enabled', false); + expect(c).toHaveProperty('brunoConfig.proxy.auth.username', ''); + expect(c).toHaveProperty('brunoConfig.proxy.auth.password', ''); + expect(c).toHaveProperty('brunoConfig.proxy.bypassProxy', ''); + expect(c).toHaveProperty('brunoConfig.scripts.moduleWhitelist', ['crypto', 'buffer']); + expect(c).toHaveProperty('brunoConfig.scripts.filesystemAccess.allow', true); + expect(c).toHaveProperty('brunoConfig.clientCertificates.enabled', true); + expect(c).toHaveProperty('brunoConfig.clientCertificates.certs', []); + + /* + collection pathname + */ + + expect(c).toHaveProperty('pathname', collectionPathname); + + + /* + collection root + */ + + // headers + expect(c).toHaveProperty('root.request.headers[0].name', 'collection_header'); + expect(c).toHaveProperty('root.request.headers[0].value', 'collection_header_value'); + expect(c).toHaveProperty('root.request.headers[0].enabled', true); + // auth + expect(c).toHaveProperty('root.request.auth.mode', 'basic'); + expect(c).toHaveProperty('root.request.auth.basic.username', 'username'); + expect(c).toHaveProperty('root.request.auth.basic.password', 'password'); + // pre-request scripts + expect(c).toHaveProperty('root.request.script.req', 'const collectionPreRequestScript = true;'); + // collection root - post-response scripts + expect(c).toHaveProperty('root.request.script.res', 'const collectionPostResponseScript = true;'); + // pre-request vars + expect(c).toHaveProperty('root.request.vars.req[0].name', 'collection_pre_var'); + expect(c).toHaveProperty('root.request.vars.req[0].value', 'collection_pre_var_value'); + expect(c).toHaveProperty('root.request.vars.req[0].enabled', true); + // post-response vars + expect(c).toHaveProperty('root.request.vars.res[0].name', 'collection_post_var'); + expect(c).toHaveProperty('root.request.vars.res[0].value', 'collection_post_var_value'); + expect(c).toHaveProperty('root.request.vars.res[0].enabled', true); + // tests + expect(c).toHaveProperty('root.request.tests', 'test(\"collection level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); + + + /* + collection items names and sequences + */ + + // /folder_2 + expect(c).toHaveProperty('items[0].type', 'folder'); + expect(c).toHaveProperty('items[0].name', 'folder_2'); + expect(c).toHaveProperty('items[0].seq', 1); + + // /folder_2/request_1 + expect(c).toHaveProperty('items[0].items[0].name', 'request_1'); + expect(c).toHaveProperty('items[0].items[0].seq', 1); + + // /folder_2/request_3 + expect(c).toHaveProperty('items[0].items[1].name', 'request_3'); + expect(c).toHaveProperty('items[0].items[1].seq', 2); + + // /folder_2/request_2 + expect(c).toHaveProperty('items[0].items[2].name', 'request_2'); + expect(c).toHaveProperty('items[0].items[2].seq', 3); + + // /folder_1 + expect(c).toHaveProperty('items[1].type', 'folder'); + expect(c).toHaveProperty('items[1].name', 'folder_1'); + expect(c).toHaveProperty('items[1].seq', 5); + + // /folder_1/folder_2 + expect(c).toHaveProperty('items[1].items[0].name', 'folder_2'); + expect(c).toHaveProperty('items[1].items[0].seq', 1); + + // /folder_1/folder_2/request_3 + expect(c).toHaveProperty('items[1].items[0].items[0].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[0].items[0].seq', 1); + + // /folder_1/folder_2/request_1 + expect(c).toHaveProperty('items[1].items[0].items[1].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[0].items[1].seq', 2); + + // /folder_1/folder_2/request_2 + expect(c).toHaveProperty('items[1].items[0].items[2].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[0].items[2].seq', 3); + + // /folder_1/folder_1 + expect(c).toHaveProperty('items[1].items[1].name', 'folder_1'); + expect(c).toHaveProperty('items[1].items[1].seq', 2); + + // /folder_1/folder_1/request_3 + expect(c).toHaveProperty('items[1].items[1].items[0].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[1].items[0].seq', 1); + + // /folder_1/folder_1/request_2 + expect(c).toHaveProperty('items[1].items[1].items[1].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[1].items[1].seq', 2); + + // /folder_1/folder_1/request_1 + expect(c).toHaveProperty('items[1].items[1].items[2].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[1].items[2].seq', 3); + + // /folder_1/request_1 + expect(c).toHaveProperty('items[1].items[2].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[2].seq', 3); + + // /folder_1/request_3 + expect(c).toHaveProperty('items[1].items[3].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[3].seq', 4); + + // /folder_1/request_2 + expect(c).toHaveProperty('items[1].items[4].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[4].seq', 5); + + // /request_2 + expect(c).toHaveProperty('items[2].name', 'request_3'); + expect(c).toHaveProperty('items[2].seq', 2); + + // /request_3 + expect(c).toHaveProperty('items[3].name', 'request_1'); + expect(c).toHaveProperty('items[3].seq', 3); + + // /request_4 + expect(c).toHaveProperty('items[4].name', 'request_2'); + expect(c).toHaveProperty('items[4].seq', 4); + + + /* + collection request item - /request_4 + */ + + // /request_4 + // headers + expect(c).toHaveProperty('items[4].request.headers[0].name', 'request_header'); + expect(c).toHaveProperty('items[4].request.headers[0].value', 'request_header_value'); + expect(c).toHaveProperty('items[4].request.headers[0].enabled', true); + // auth + expect(c).toHaveProperty('items[4].request.auth.mode', 'basic'); + expect(c).toHaveProperty('items[4].request.auth.basic.username', 'username'); + expect(c).toHaveProperty('items[4].request.auth.basic.password', 'password'); + // pre-request scripts + expect(c).toHaveProperty('items[4].request.script.req', 'const requestPreRequestScript = true;'); + // request items[4] - post-response scripts + expect(c).toHaveProperty('items[4].request.script.res', 'const requestPostResponseScript = true;'); + // pre-request vars + expect(c).toHaveProperty('items[4].request.vars.req[0].name', 'request_pre_var'); + expect(c).toHaveProperty('items[4].request.vars.req[0].value', 'request_pre_var_value'); + expect(c).toHaveProperty('items[4].request.vars.req[0].enabled', true); + // post-response vars + expect(c).toHaveProperty('items[4].request.vars.res[0].name', 'request_post_var'); + expect(c).toHaveProperty('items[4].request.vars.res[0].value', 'request_post_var_value'); + expect(c).toHaveProperty('items[4].request.vars.res[0].enabled', true); + // tests + expect(c).toHaveProperty('items[4].request.tests', 'test(\"request level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); + + + }); +}); \ No newline at end of file diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/bruno.json b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/bruno.json new file mode 100644 index 000000000..366f84472 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/bruno.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "name": "collection", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ], + "proxy": { + "enabled": false, + "protocol": "http", + "hostname": "", + "port": 3000, + "auth": { + "enabled": false, + "username": "", + "password": "" + }, + "bypassProxy": "" + }, + "scripts": { + "moduleWhitelist": ["crypto", "buffer"], + "filesystemAccess": { + "allow": true + } + }, + "clientCertificates": { + "enabled": true, + "certs": [] + } +} \ No newline at end of file diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/collection.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/collection.bru new file mode 100644 index 000000000..bdfbfc430 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/collection.bru @@ -0,0 +1,38 @@ +headers { + collection_header: collection_header_value +} + +auth { + mode: basic +} + +auth:basic { + username: username + password: password +} + +vars:pre-request { + collection_pre_var: collection_pre_var_value +} + +vars:post-response { + collection_post_var: collection_post_var_value +} + +script:pre-request { + const collectionPreRequestScript = true; +} + +script:post-response { + const collectionPostResponseScript = true; +} + +tests { + test("collection level script", function() { + expect("test").to.equal("test"); + }); +} + +docs { + # docs +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder.bru new file mode 100644 index 000000000..fb30da65c --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder.bru @@ -0,0 +1,4 @@ +meta { + name: folder_1 + seq: 5 +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/folder.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/folder.bru new file mode 100644 index 000000000..2b5e3cd2f --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/folder.bru @@ -0,0 +1,4 @@ +meta { + name: folder_1 + seq: 2 +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru new file mode 100644 index 000000000..9bc174417 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru @@ -0,0 +1,27 @@ +meta { + name: request_1 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru new file mode 100644 index 000000000..552eee1a7 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru @@ -0,0 +1,27 @@ +meta { + name: request_2 + type: http + seq: 2 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru new file mode 100644 index 000000000..104477365 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru @@ -0,0 +1,27 @@ +meta { + name: request_3 + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/folder.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/folder.bru new file mode 100644 index 000000000..674476e89 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/folder.bru @@ -0,0 +1,4 @@ +meta { + name: folder_2 + seq: 1 +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru new file mode 100644 index 000000000..57e9d65aa --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru @@ -0,0 +1,27 @@ +meta { + name: request_1 + type: http + seq: 2 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru new file mode 100644 index 000000000..af5720319 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru @@ -0,0 +1,27 @@ +meta { + name: request_2 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru new file mode 100644 index 000000000..104477365 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru @@ -0,0 +1,27 @@ +meta { + name: request_3 + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru new file mode 100644 index 000000000..9bc174417 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru @@ -0,0 +1,27 @@ +meta { + name: request_1 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru new file mode 100644 index 000000000..f2628dc4a --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru @@ -0,0 +1,27 @@ +meta { + name: request_2 + type: http + seq: 5 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru new file mode 100644 index 000000000..11f5b150d --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru @@ -0,0 +1,27 @@ +meta { + name: request_3 + type: http + seq: 4 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/folder.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/folder.bru new file mode 100644 index 000000000..674476e89 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/folder.bru @@ -0,0 +1,4 @@ +meta { + name: folder_2 + seq: 1 +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru new file mode 100644 index 000000000..1eae2ee54 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru @@ -0,0 +1,27 @@ +meta { + name: request_1 + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru new file mode 100644 index 000000000..af5720319 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru @@ -0,0 +1,27 @@ +meta { + name: request_2 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru new file mode 100644 index 000000000..f1382aa81 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru @@ -0,0 +1,27 @@ +meta { + name: request_3 + type: http + seq: 2 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru new file mode 100644 index 000000000..9bc174417 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru @@ -0,0 +1,27 @@ +meta { + name: request_1 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_2.bru new file mode 100644 index 000000000..1aef2b30e --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_2.bru @@ -0,0 +1,58 @@ +meta { + name: request_2 + type: http + seq: 4 +} + +post { + url: https://echo.usebruno.com/:request_path_param?request_query_param=request_query_param_value + body: text + auth: basic +} + +params:query { + request_query_param: request_query_param_value +} + +params:path { + request_path_param: request_path_param_value +} + +headers { + request_header: request_header_value +} + +auth:basic { + username: username + password: password +} + +body:text { + ping +} + +vars:pre-request { + request_pre_var: request_pre_var_value +} + +vars:post-response { + request_post_var: request_post_var_value +} + +assert { + res.status: eq 200 +} + +script:pre-request { + const requestPreRequestScript = true; +} + +script:post-response { + const requestPostResponseScript = true; +} + +tests { + test("request level script", function() { + expect("test").to.equal("test"); + }); +} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru new file mode 100644 index 000000000..f1382aa81 --- /dev/null +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru @@ -0,0 +1,27 @@ +meta { + name: request_3 + type: http + seq: 2 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { + ping +} + +assert { + res.status: eq 200 +} + +tests { + test("should return plain text", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql("ping"); + }); + +} From 6455b007420918294df942a5ae435edd8d7e7232 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Mon, 14 Apr 2025 13:33:22 +0530 Subject: [PATCH 807/904] Removed old Playwright setup --- package-lock.json | 64 ---------------------- package.json | 3 -- playwright.config.js | 110 -------------------------------------- tests/home.spec.js | 48 ----------------- tests/pages/home.page.js | 86 ----------------------------- tests/utils/data-faker.js | 6 --- 6 files changed, 317 deletions(-) delete mode 100644 playwright.config.js delete mode 100644 tests/home.spec.js delete mode 100644 tests/pages/home.page.js delete mode 100644 tests/utils/data-faker.js diff --git a/package-lock.json b/package-lock.json index 8bf023cfd..1fa51c2c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "devDependencies": { "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@playwright/test": "^1.27.1", "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", @@ -6104,22 +6103,6 @@ "node": ">=14" } }, - "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -21299,53 +21282,6 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, - "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 30d41bb01..4bd03c657 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "devDependencies": { "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@playwright/test": "^1.27.1", "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "concurrently": "^8.2.2", @@ -59,8 +58,6 @@ "build:electron:snap": "./scripts/build-electron.sh snap", "watch:common": "npm run watch --workspace=packages/bruno-common", "test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js", - "test:e2e": "npx playwright test", - "test:report": "npx playwright show-report", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "prepare": "husky install", "lint": "npx eslint ./" diff --git a/playwright.config.js b/playwright.config.js deleted file mode 100644 index 63afd88d4..000000000 --- a/playwright.config.js +++ /dev/null @@ -1,110 +0,0 @@ -// @ts-check -const { devices } = require('@playwright/test'); - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -process.env.PLAYWRIGHT = "1"; - -/** - * @see https://playwright.dev/docs/test-configuration - * @type {import('@playwright/test').PlaywrightTestConfig} - */ -const config = { - testDir: './tests', - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000 - }, - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'retain-on-failure', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, - - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, - }, - - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, - ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev:web', - port: 3000, - }, -}; - -module.exports = config; diff --git a/tests/home.spec.js b/tests/home.spec.js deleted file mode 100644 index 0ab8a5652..000000000 --- a/tests/home.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -const { test, expect } = require('@playwright/test'); -const { HomePage } = require('../tests/pages/home.page'); -const { faker } = require('./utils/data-faker'); - -test.describe('bruno e2e test', () => { - let homePage; - - test.beforeEach(async ({ page }) => { - homePage = new HomePage(page); - - await homePage.open(); - await expect(page).toHaveURL('/'); - await expect(page).toHaveTitle(/bruno/); - }); - - test('user should be able to create new collection & new request', async () => { - await homePage.createNewCollection(faker.randomWords); - await expect(homePage.createNewCollectionSuccessToast).toBeVisible(); - - // using fake data to simulate negative case - await homePage.createNewRequest(faker.randomVerb, faker.randomHttpMethod, faker.randomUrl); - await expect(homePage.networkErrorToast).toBeVisible(); - - // using real data to simulate positive case - await homePage.createNewRequest('Single User', 'GET', 'https://reqres.in/api/users/2'); - await expect(homePage.statusRequestSuccess).toBeVisible(); - }); - - test('user should be able to load & use sample collection', async () => { - await homePage.loadSampleCollection(); - await expect(homePage.loadSampleCollectionSuccessToast).toBeVisible(); - - await homePage.getUsers(); - await expect(homePage.statusRequestSuccess).toBeVisible(); - - await homePage.getSingleUser(); - await expect(homePage.statusRequestSuccess).toBeVisible(); - - await homePage.getUserNotFound(); - await expect(homePage.statusRequestNotFound).toBeVisible(); - - await homePage.createUser(); - await expect(homePage.statusRequestCreated).toBeVisible(); - - await homePage.updateUser(); - await expect(homePage.statusRequestSuccess).toBeVisible(); - }); -}); diff --git a/tests/pages/home.page.js b/tests/pages/home.page.js deleted file mode 100644 index 4aff24ce1..000000000 --- a/tests/pages/home.page.js +++ /dev/null @@ -1,86 +0,0 @@ -exports.HomePage = class HomePage { - constructor(page) { - this.page = page; - - // welcome - this.createCollectionSelector = page.locator('#create-collection'); - this.addCollectionSelector = page.locator('#add-collection'); - this.importCollectionSelector = page.locator('#import-collection'); - this.loadSampleCollectionSelector = page.locator('#load-sample-collection'); - - // sample collection - this.loadSampleCollectionSuccessToast = page.getByText('Sample Collection loaded successfully'); - this.sampleCollectionSelector = page.locator('#sidebar-collection-name'); - this.getUsersSelector = page.getByText('Users'); - this.getSingleUserSelector = page.getByText('Single User'); - this.getUserNotFoundSelector = page.getByText('User Not Found'); - this.postCreateSelector = page.getByText('Create'); - this.putUpdateSelector = page.getByText('Update'); - - // request panel - this.sendRequestButton = page.locator('#send-request'); - this.statusRequestSuccess = page.getByText('200 OK'); - this.statusRequestNotFound = page.getByText('404 Not Found'); - this.statusRequestCreated = page.getByText('201 Created'); - - // create collection - this.collectionNameField = page.locator('#collection-name'); - this.submitButton = page.locator(`button[type='submit']`); - this.createNewCollectionSuccessToast = page.getByText('Collection created'); - this.createNewTab = page.locator('#create-new-tab'); - this.requestNameField = page.locator('input[name="requestName"]'); - this.methodName = page.locator('#create-new-request-method').first(); - this.requestUrlField = page.locator('#request-url'); - this.networkErrorToast = page.getByText('Network Error'); - } - - async open() { - await this.page.goto('/'); - } - - async loadSampleCollection() { - await this.loadSampleCollectionSelector.click(); - } - - async getUsers() { - await this.sampleCollectionSelector.click(); - await this.getUsersSelector.click(); - await this.sendRequestButton.click(); - } - - async getSingleUser() { - await this.getSingleUserSelector.click(); - await this.sendRequestButton.click(); - } - - async getUserNotFound() { - await this.getUserNotFoundSelector.click(); - await this.sendRequestButton.click(); - } - - async createUser() { - await this.postCreateSelector.click(); - await this.sendRequestButton.click(); - } - - async updateUser() { - await this.putUpdateSelector.click(); - await this.sendRequestButton.click(); - } - - async createNewCollection(collectionName) { - await this.createCollectionSelector.click(); - await this.collectionNameField.fill(collectionName); - await this.submitButton.click(); - } - - async createNewRequest(name, method, endpoint) { - await this.createNewTab.click(); - await this.requestNameField.fill(name); - await this.methodName.click(); - await this.page.click(`text=${method}`); - await this.requestUrlField.fill(endpoint); - await this.submitButton.click(); - await this.sendRequestButton.click(); - } -}; diff --git a/tests/utils/data-faker.js b/tests/utils/data-faker.js deleted file mode 100644 index 2674b6244..000000000 --- a/tests/utils/data-faker.js +++ /dev/null @@ -1,6 +0,0 @@ -const { faker } = require('@faker-js/faker'); - -export let randomWords = faker.random.words(); -export let randomVerb = faker.hacker.verb(); -export let randomHttpMethod = faker.internet.httpMethod(); -export let randomUrl = faker.internet.url(); From 277697031784f4856d4ca63a963b855a3fc3034a Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 12 May 2025 16:57:27 +0530 Subject: [PATCH 808/904] revert collection-pathname param --- packages/bruno-cli/src/commands/run.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 25bc1d1cd..ce06a31de 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -168,10 +168,6 @@ const builder = async (yargs) => { type:"number", description: "Delay between each requests (in miliseconds)" }) - .option('collection-pathname', { - type: "string", - description: "Collection root pathname" - }) .example('$0 run request.bru', 'Run a request') .example('$0 run request.bru --env local', 'Run a request with the environment set to local') .example('$0 run folder', 'Run all requests in a folder') @@ -237,12 +233,9 @@ const handler = async function (argv) { reporterSkipAllHeaders, reporterSkipHeaders, clientCertConfig, - delay, - collectionPathname + delay } = argv; - const collectionPath = collectionPathname || process.cwd(); - - filename = path.join(collectionPath, filename); + const collectionPath = process.cwd(); let collection = createCollectionJsonFromPathname(collectionPath); const { root: collectionRoot, brunoConfig } = collection; From 27f11ab583dcb771e16b36c7c53f123311992c96 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 12 May 2025 17:19:13 +0530 Subject: [PATCH 809/904] updates --- .../collection-json-from-pathname.spec.js | 300 ++++++++---------- .../folder_1/folder_1/request_1.bru | 18 +- .../folder_1/folder_1/request_2.bru | 18 +- .../folder_1/folder_1/request_3.bru | 18 +- .../folder_1/folder_2/request_1.bru | 18 +- .../folder_1/folder_2/request_2.bru | 18 +- .../folder_1/folder_2/request_3.bru | 18 +- .../collection/folder_1/request_1.bru | 18 +- .../collection/folder_1/request_2.bru | 18 +- .../collection/folder_1/request_3.bru | 18 +- .../collection/folder_2/request_1.bru | 18 +- .../collection/folder_2/request_2.bru | 18 +- .../collection/folder_2/request_3.bru | 18 +- .../collection/request_1.bru | 18 +- .../collection/request_3.bru | 18 +- 15 files changed, 154 insertions(+), 398 deletions(-) diff --git a/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js b/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js index 0886b0beb..8cab346cd 100644 --- a/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js +++ b/packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js @@ -4,189 +4,169 @@ const constants = require('../../src/constants'); const { createCollectionJsonFromPathname } = require('../../src/utils/collection'); describe('create collection json from pathname', () => { - it("should throw an error when the pathname is not a valid bruno collection root", () => { - const invalidCollectionPathname = path.join(__dirname, './fixtures/collection-invalid'); - jest.spyOn(console, 'error').mockImplementation(() => { }); - let mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(code); }); - try { createCollectionJsonFromPathname(invalidCollectionPathname); } catch {} - expect(mockProcessExit).toHaveBeenCalledWith(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); - jest.restoreAllMocks(); - }) + it("should throw an error when the pathname is not a valid bruno collection root", () => { + const invalidCollectionPathname = path.join(__dirname, './fixtures/collection-invalid'); + jest.spyOn(console, 'error').mockImplementation(() => { }); + let mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(code); }); + try { createCollectionJsonFromPathname(invalidCollectionPathname); } catch { } + expect(mockProcessExit).toHaveBeenCalledWith(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); + jest.restoreAllMocks(); + }) - it("creates a bruno collection json from the collection bru files", () => { - const collectionPathname = path.join(__dirname, './fixtures/collection-json-from-pathname/collection'); - const outputCollectionJson = createCollectionJsonFromPathname(collectionPathname); + it("creates a bruno collection json from the collection bru files", () => { + const collectionPathname = path.join(__dirname, './fixtures/collection-json-from-pathname/collection'); + const outputCollectionJson = createCollectionJsonFromPathname(collectionPathname); - let c = outputCollectionJson; - expect(c).toBeDefined(); - - /* - collection bruno.json - */ + let c = outputCollectionJson; + expect(c).toBeDefined(); - expect(c).toHaveProperty('brunoConfig.version', "1"); - expect(c).toHaveProperty('brunoConfig.name', 'collection'); - expect(c).toHaveProperty('brunoConfig.type', 'collection'); - expect(c).toHaveProperty('brunoConfig.ignore', ["node_modules", ".git"]); - expect(c).toHaveProperty('brunoConfig.proxy.enabled', false); - expect(c).toHaveProperty('brunoConfig.proxy.protocol', 'http'); - expect(c).toHaveProperty('brunoConfig.proxy.hostname', ''); - expect(c).toHaveProperty('brunoConfig.proxy.port', 3000); - expect(c).toHaveProperty('brunoConfig.proxy.auth.enabled', false); - expect(c).toHaveProperty('brunoConfig.proxy.auth.username', ''); - expect(c).toHaveProperty('brunoConfig.proxy.auth.password', ''); - expect(c).toHaveProperty('brunoConfig.proxy.bypassProxy', ''); - expect(c).toHaveProperty('brunoConfig.scripts.moduleWhitelist', ['crypto', 'buffer']); - expect(c).toHaveProperty('brunoConfig.scripts.filesystemAccess.allow', true); - expect(c).toHaveProperty('brunoConfig.clientCertificates.enabled', true); - expect(c).toHaveProperty('brunoConfig.clientCertificates.certs', []); + /* collection bruno.json */ + expect(c).toHaveProperty('brunoConfig.version', "1"); + expect(c).toHaveProperty('brunoConfig.name', 'collection'); + expect(c).toHaveProperty('brunoConfig.type', 'collection'); + expect(c).toHaveProperty('brunoConfig.ignore', ["node_modules", ".git"]); + expect(c).toHaveProperty('brunoConfig.proxy.enabled', false); + expect(c).toHaveProperty('brunoConfig.proxy.protocol', 'http'); + expect(c).toHaveProperty('brunoConfig.proxy.hostname', ''); + expect(c).toHaveProperty('brunoConfig.proxy.port', 3000); + expect(c).toHaveProperty('brunoConfig.proxy.auth.enabled', false); + expect(c).toHaveProperty('brunoConfig.proxy.auth.username', ''); + expect(c).toHaveProperty('brunoConfig.proxy.auth.password', ''); + expect(c).toHaveProperty('brunoConfig.proxy.bypassProxy', ''); + expect(c).toHaveProperty('brunoConfig.scripts.moduleWhitelist', ['crypto', 'buffer']); + expect(c).toHaveProperty('brunoConfig.scripts.filesystemAccess.allow', true); + expect(c).toHaveProperty('brunoConfig.clientCertificates.enabled', true); + expect(c).toHaveProperty('brunoConfig.clientCertificates.certs', []); - /* - collection pathname - */ + /* collection pathname */ + expect(c).toHaveProperty('pathname', collectionPathname); - expect(c).toHaveProperty('pathname', collectionPathname); + /* collection root */ + // headers + expect(c).toHaveProperty('root.request.headers[0].name', 'collection_header'); + expect(c).toHaveProperty('root.request.headers[0].value', 'collection_header_value'); + expect(c).toHaveProperty('root.request.headers[0].enabled', true); + // auth + expect(c).toHaveProperty('root.request.auth.mode', 'basic'); + expect(c).toHaveProperty('root.request.auth.basic.username', 'username'); + expect(c).toHaveProperty('root.request.auth.basic.password', 'password'); + // pre-request scripts + expect(c).toHaveProperty('root.request.script.req', 'const collectionPreRequestScript = true;'); + // collection root - post-response scripts + expect(c).toHaveProperty('root.request.script.res', 'const collectionPostResponseScript = true;'); + // pre-request vars + expect(c).toHaveProperty('root.request.vars.req[0].name', 'collection_pre_var'); + expect(c).toHaveProperty('root.request.vars.req[0].value', 'collection_pre_var_value'); + expect(c).toHaveProperty('root.request.vars.req[0].enabled', true); + // post-response vars + expect(c).toHaveProperty('root.request.vars.res[0].name', 'collection_post_var'); + expect(c).toHaveProperty('root.request.vars.res[0].value', 'collection_post_var_value'); + expect(c).toHaveProperty('root.request.vars.res[0].enabled', true); + // tests + expect(c).toHaveProperty('root.request.tests', 'test(\"collection level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); - - /* - collection root - */ + /* collection items names and sequences */ + // /folder_2 + expect(c).toHaveProperty('items[0].type', 'folder'); + expect(c).toHaveProperty('items[0].name', 'folder_2'); + expect(c).toHaveProperty('items[0].seq', 1); - // headers - expect(c).toHaveProperty('root.request.headers[0].name', 'collection_header'); - expect(c).toHaveProperty('root.request.headers[0].value', 'collection_header_value'); - expect(c).toHaveProperty('root.request.headers[0].enabled', true); - // auth - expect(c).toHaveProperty('root.request.auth.mode', 'basic'); - expect(c).toHaveProperty('root.request.auth.basic.username', 'username'); - expect(c).toHaveProperty('root.request.auth.basic.password', 'password'); - // pre-request scripts - expect(c).toHaveProperty('root.request.script.req', 'const collectionPreRequestScript = true;'); - // collection root - post-response scripts - expect(c).toHaveProperty('root.request.script.res', 'const collectionPostResponseScript = true;'); - // pre-request vars - expect(c).toHaveProperty('root.request.vars.req[0].name', 'collection_pre_var'); - expect(c).toHaveProperty('root.request.vars.req[0].value', 'collection_pre_var_value'); - expect(c).toHaveProperty('root.request.vars.req[0].enabled', true); - // post-response vars - expect(c).toHaveProperty('root.request.vars.res[0].name', 'collection_post_var'); - expect(c).toHaveProperty('root.request.vars.res[0].value', 'collection_post_var_value'); - expect(c).toHaveProperty('root.request.vars.res[0].enabled', true); - // tests - expect(c).toHaveProperty('root.request.tests', 'test(\"collection level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); + // /folder_2/request_1 + expect(c).toHaveProperty('items[0].items[0].name', 'request_1'); + expect(c).toHaveProperty('items[0].items[0].seq', 1); + // /folder_2/request_3 + expect(c).toHaveProperty('items[0].items[1].name', 'request_3'); + expect(c).toHaveProperty('items[0].items[1].seq', 2); - /* - collection items names and sequences - */ + // /folder_2/request_2 + expect(c).toHaveProperty('items[0].items[2].name', 'request_2'); + expect(c).toHaveProperty('items[0].items[2].seq', 3); - // /folder_2 - expect(c).toHaveProperty('items[0].type', 'folder'); - expect(c).toHaveProperty('items[0].name', 'folder_2'); - expect(c).toHaveProperty('items[0].seq', 1); + // /folder_1 + expect(c).toHaveProperty('items[1].type', 'folder'); + expect(c).toHaveProperty('items[1].name', 'folder_1'); + expect(c).toHaveProperty('items[1].seq', 5); - // /folder_2/request_1 - expect(c).toHaveProperty('items[0].items[0].name', 'request_1'); - expect(c).toHaveProperty('items[0].items[0].seq', 1); + // /folder_1/folder_2 + expect(c).toHaveProperty('items[1].items[0].name', 'folder_2'); + expect(c).toHaveProperty('items[1].items[0].seq', 1); - // /folder_2/request_3 - expect(c).toHaveProperty('items[0].items[1].name', 'request_3'); - expect(c).toHaveProperty('items[0].items[1].seq', 2); + // /folder_1/folder_2/request_3 + expect(c).toHaveProperty('items[1].items[0].items[0].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[0].items[0].seq', 1); - // /folder_2/request_2 - expect(c).toHaveProperty('items[0].items[2].name', 'request_2'); - expect(c).toHaveProperty('items[0].items[2].seq', 3); + // /folder_1/folder_2/request_1 + expect(c).toHaveProperty('items[1].items[0].items[1].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[0].items[1].seq', 2); - // /folder_1 - expect(c).toHaveProperty('items[1].type', 'folder'); - expect(c).toHaveProperty('items[1].name', 'folder_1'); - expect(c).toHaveProperty('items[1].seq', 5); - - // /folder_1/folder_2 - expect(c).toHaveProperty('items[1].items[0].name', 'folder_2'); - expect(c).toHaveProperty('items[1].items[0].seq', 1); + // /folder_1/folder_2/request_2 + expect(c).toHaveProperty('items[1].items[0].items[2].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[0].items[2].seq', 3); - // /folder_1/folder_2/request_3 - expect(c).toHaveProperty('items[1].items[0].items[0].name', 'request_3'); - expect(c).toHaveProperty('items[1].items[0].items[0].seq', 1); + // /folder_1/folder_1 + expect(c).toHaveProperty('items[1].items[1].name', 'folder_1'); + expect(c).toHaveProperty('items[1].items[1].seq', 2); - // /folder_1/folder_2/request_1 - expect(c).toHaveProperty('items[1].items[0].items[1].name', 'request_1'); - expect(c).toHaveProperty('items[1].items[0].items[1].seq', 2); + // /folder_1/folder_1/request_3 + expect(c).toHaveProperty('items[1].items[1].items[0].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[1].items[0].seq', 1); - // /folder_1/folder_2/request_2 - expect(c).toHaveProperty('items[1].items[0].items[2].name', 'request_2'); - expect(c).toHaveProperty('items[1].items[0].items[2].seq', 3); + // /folder_1/folder_1/request_2 + expect(c).toHaveProperty('items[1].items[1].items[1].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[1].items[1].seq', 2); - // /folder_1/folder_1 - expect(c).toHaveProperty('items[1].items[1].name', 'folder_1'); - expect(c).toHaveProperty('items[1].items[1].seq', 2); + // /folder_1/folder_1/request_1 + expect(c).toHaveProperty('items[1].items[1].items[2].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[1].items[2].seq', 3); - // /folder_1/folder_1/request_3 - expect(c).toHaveProperty('items[1].items[1].items[0].name', 'request_3'); - expect(c).toHaveProperty('items[1].items[1].items[0].seq', 1); + // /folder_1/request_1 + expect(c).toHaveProperty('items[1].items[2].name', 'request_1'); + expect(c).toHaveProperty('items[1].items[2].seq', 3); - // /folder_1/folder_1/request_2 - expect(c).toHaveProperty('items[1].items[1].items[1].name', 'request_2'); - expect(c).toHaveProperty('items[1].items[1].items[1].seq', 2); + // /folder_1/request_3 + expect(c).toHaveProperty('items[1].items[3].name', 'request_3'); + expect(c).toHaveProperty('items[1].items[3].seq', 4); - // /folder_1/folder_1/request_1 - expect(c).toHaveProperty('items[1].items[1].items[2].name', 'request_1'); - expect(c).toHaveProperty('items[1].items[1].items[2].seq', 3); + // /folder_1/request_2 + expect(c).toHaveProperty('items[1].items[4].name', 'request_2'); + expect(c).toHaveProperty('items[1].items[4].seq', 5); - // /folder_1/request_1 - expect(c).toHaveProperty('items[1].items[2].name', 'request_1'); - expect(c).toHaveProperty('items[1].items[2].seq', 3); + // /request_2 + expect(c).toHaveProperty('items[2].name', 'request_3'); + expect(c).toHaveProperty('items[2].seq', 2); - // /folder_1/request_3 - expect(c).toHaveProperty('items[1].items[3].name', 'request_3'); - expect(c).toHaveProperty('items[1].items[3].seq', 4); + // /request_3 + expect(c).toHaveProperty('items[3].name', 'request_1'); + expect(c).toHaveProperty('items[3].seq', 3); - // /folder_1/request_2 - expect(c).toHaveProperty('items[1].items[4].name', 'request_2'); - expect(c).toHaveProperty('items[1].items[4].seq', 5); + // /request_4 + expect(c).toHaveProperty('items[4].name', 'request_2'); + expect(c).toHaveProperty('items[4].seq', 4); - // /request_2 - expect(c).toHaveProperty('items[2].name', 'request_3'); - expect(c).toHaveProperty('items[2].seq', 2); - - // /request_3 - expect(c).toHaveProperty('items[3].name', 'request_1'); - expect(c).toHaveProperty('items[3].seq', 3); - - // /request_4 - expect(c).toHaveProperty('items[4].name', 'request_2'); - expect(c).toHaveProperty('items[4].seq', 4); - - - /* - collection request item - /request_4 - */ - - // /request_4 - // headers - expect(c).toHaveProperty('items[4].request.headers[0].name', 'request_header'); - expect(c).toHaveProperty('items[4].request.headers[0].value', 'request_header_value'); - expect(c).toHaveProperty('items[4].request.headers[0].enabled', true); - // auth - expect(c).toHaveProperty('items[4].request.auth.mode', 'basic'); - expect(c).toHaveProperty('items[4].request.auth.basic.username', 'username'); - expect(c).toHaveProperty('items[4].request.auth.basic.password', 'password'); - // pre-request scripts - expect(c).toHaveProperty('items[4].request.script.req', 'const requestPreRequestScript = true;'); - // request items[4] - post-response scripts - expect(c).toHaveProperty('items[4].request.script.res', 'const requestPostResponseScript = true;'); - // pre-request vars - expect(c).toHaveProperty('items[4].request.vars.req[0].name', 'request_pre_var'); - expect(c).toHaveProperty('items[4].request.vars.req[0].value', 'request_pre_var_value'); - expect(c).toHaveProperty('items[4].request.vars.req[0].enabled', true); - // post-response vars - expect(c).toHaveProperty('items[4].request.vars.res[0].name', 'request_post_var'); - expect(c).toHaveProperty('items[4].request.vars.res[0].value', 'request_post_var_value'); - expect(c).toHaveProperty('items[4].request.vars.res[0].enabled', true); - // tests - expect(c).toHaveProperty('items[4].request.tests', 'test(\"request level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); - - - }); + /* collection request item - /request_4 */ + // /request_4 + // headers + expect(c).toHaveProperty('items[4].request.headers[0].name', 'request_header'); + expect(c).toHaveProperty('items[4].request.headers[0].value', 'request_header_value'); + expect(c).toHaveProperty('items[4].request.headers[0].enabled', true); + // auth + expect(c).toHaveProperty('items[4].request.auth.mode', 'basic'); + expect(c).toHaveProperty('items[4].request.auth.basic.username', 'username'); + expect(c).toHaveProperty('items[4].request.auth.basic.password', 'password'); + // pre-request scripts + expect(c).toHaveProperty('items[4].request.script.req', 'const requestPreRequestScript = true;'); + // request items[4] - post-response scripts + expect(c).toHaveProperty('items[4].request.script.res', 'const requestPostResponseScript = true;'); + // pre-request vars + expect(c).toHaveProperty('items[4].request.vars.req[0].name', 'request_pre_var'); + expect(c).toHaveProperty('items[4].request.vars.req[0].value', 'request_pre_var_value'); + expect(c).toHaveProperty('items[4].request.vars.req[0].enabled', true); + // post-response vars + expect(c).toHaveProperty('items[4].request.vars.res[0].name', 'request_post_var'); + expect(c).toHaveProperty('items[4].request.vars.res[0].value', 'request_post_var_value'); + expect(c).toHaveProperty('items[4].request.vars.res[0].enabled', true); + // tests + expect(c).toHaveProperty('items[4].request.tests', 'test(\"request level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});'); + }); }); \ No newline at end of file diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru index 9bc174417..79ff1676a 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru @@ -4,24 +4,8 @@ meta { seq: 3 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru index 552eee1a7..b0b7e046e 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru @@ -4,24 +4,8 @@ meta { seq: 2 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru index 104477365..7953c4499 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru @@ -4,24 +4,8 @@ meta { seq: 1 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru index 57e9d65aa..c93c6cb37 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru @@ -4,24 +4,8 @@ meta { seq: 2 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru index af5720319..375cc9f6d 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru @@ -4,24 +4,8 @@ meta { seq: 3 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru index 104477365..7953c4499 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru @@ -4,24 +4,8 @@ meta { seq: 1 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru index 9bc174417..79ff1676a 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru @@ -4,24 +4,8 @@ meta { seq: 3 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru index f2628dc4a..7dac68aed 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru @@ -4,24 +4,8 @@ meta { seq: 5 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru index 11f5b150d..8a818f66c 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru @@ -4,24 +4,8 @@ meta { seq: 4 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru index 1eae2ee54..b8fb205ed 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru @@ -4,24 +4,8 @@ meta { seq: 1 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru index af5720319..375cc9f6d 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru @@ -4,24 +4,8 @@ meta { seq: 3 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru index f1382aa81..a2582cf6c 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru @@ -4,24 +4,8 @@ meta { seq: 2 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru index 9bc174417..79ff1676a 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru @@ -4,24 +4,8 @@ meta { seq: 3 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} diff --git a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru index f1382aa81..a2582cf6c 100644 --- a/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru +++ b/packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru @@ -4,24 +4,8 @@ meta { seq: 2 } -post { +get { url: https://echo.usebruno.com body: text auth: none } - -body:text { - ping -} - -assert { - res.status: eq 200 -} - -tests { - test("should return plain text", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql("ping"); - }); - -} From b2756b3c63eef1ed87716dd3f1d61393ca23f780 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 12 May 2025 17:37:45 +0530 Subject: [PATCH 810/904] updates --- packages/bruno-cli/src/commands/run.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index ce06a31de..1dc688404 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -401,6 +401,7 @@ const handler = async function (argv) { console.log(chalk.yellow('Running Request \n')); const bruContent = fs.readFileSync(filename, 'utf8'); const requestItem = bruToJson(bruContent); + requestItem.pathname = path.resolve(collectionPath, filename); requestItems.push(requestItem); } From bdbcaeff67befb5ac732a7005978731e1eef1971 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 12 May 2025 20:06:26 +0530 Subject: [PATCH 811/904] updates --- package-lock.json | 7 ------- packages/bruno-cli/src/commands/run.js | 6 +++--- packages/bruno-cli/src/runner/run-single-request.js | 8 ++++---- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b267dd3f..0b45baf9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1478,7 +1477,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -1509,7 +1507,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1527,7 +1524,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -1807,7 +1803,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -11654,7 +11649,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -15026,7 +15020,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 1dc688404..0ef587a9c 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -259,10 +259,10 @@ const handler = async function (argv) { } if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) { - if (clientCertificates) { - clientCertificates.certs.push(...clientCertConfigJson.certs); + if (brunoConfig.clientCertificates) { + brunoConfig.clientCertificates.certs.push(...clientCertConfigJson.certs); } else { - clientCertificates = { certs: clientCertConfigJson.certs }; + brunoConfig.clientCertificates = { certs: clientCertConfigJson.certs }; } console.log(chalk.green(`Client certificates has been added`)); } else { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index ed29d84b0..2bf7bc435 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -81,7 +81,7 @@ const runSingleRequest = async function ( if (result?.skipRequest) { return { test: { - filename: itemPathname + filename: relativeItemPathname }, request: { method: request.method, @@ -363,7 +363,7 @@ const runSingleRequest = async function ( console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`)); return { test: { - filename: itemPathname + filename: relativeItemPathname }, request: { method: request.method, @@ -499,7 +499,7 @@ const runSingleRequest = async function ( return { test: { - filename: itemPathname + filename: relativeItemPathname }, request: { method: request.method, @@ -525,7 +525,7 @@ const runSingleRequest = async function ( console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`)); return { test: { - filename: itemPathname + filename: relativeItemPathname }, request: { method: null, From aa911f88f2afa770558997a0d2a3d37b29bf78de Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Mon, 14 Apr 2025 14:24:50 +0530 Subject: [PATCH 812/904] Playwright Codegen and CI setup - Improved the Codegen setup - Removed the app-launch related boilerplate from tests - Enable recording mode by default - Option to provide the test file name to save the recording - Added GitHub workflow to run Playwright tests with Electron in Headless mode(mocking display using `xvfb`). --- .github/workflows/playwright.yml | 44 ++++++++++++++++ .gitignore | 5 +- e2e-tests/test-app-start.spec.ts | 5 ++ package-lock.json | 86 +++++++++++++++++++++++++++++--- package.json | 6 ++- playwright.config.ts | 25 ++++++++++ playwright/codegen.ts | 13 +++++ playwright/electron.ts | 13 +++++ playwright/index.ts | 23 +++++++++ scripts/playwright-codegen.js | 17 ------- 10 files changed, 210 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 e2e-tests/test-app-start.spec.ts create mode 100644 playwright.config.ts create mode 100644 playwright/codegen.ts create mode 100644 playwright/electron.ts create mode 100644 playwright/index.ts delete mode 100644 scripts/playwright-codegen.js diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..f630b56cb --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,44 @@ +name: Playwright E2E Tests +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + e2e-test: + timeout-minutes: 60 + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: v22.11.x + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get --no-install-recommends install -y \ + libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ + xvfb + npm ci --legacy-peer-deps + sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + + - name: Build libraries + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + npm run sandbox:bundle-libraries --workspace=packages/bruno-js + npm run build:bruno-converters + npm run build:bruno-requests + + - name: Run Playwright tests + run: | + xvfb-run npm run test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index b97cd17e3..9331b13ff 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,7 @@ yarn-error.log* #dev editor bruno.iml .idea -.vscode \ No newline at end of file +.vscode + +# Playwright +/blob-report/ diff --git a/e2e-tests/test-app-start.spec.ts b/e2e-tests/test-app-start.spec.ts new file mode 100644 index 000000000..891c7ce3b --- /dev/null +++ b/e2e-tests/test-app-start.spec.ts @@ -0,0 +1,5 @@ +import { test, expect } from '../playwright'; + +test('test-app-start', async ({ page }) => { + await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1fa51c2c1..15809857d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,10 @@ "devDependencies": { "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", + "@playwright/test": "^1.51.1", "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", + "@types/node": "^22.14.1", "concurrently": "^8.2.2", "eslint": "^9.26.0", "fs-extra": "^11.1.1", @@ -33,6 +35,7 @@ "jest": "^29.2.0", "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", + "playwright": "^1.51.1", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", "rimraf": "^6.0.1", @@ -6103,6 +6106,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -8273,6 +8292,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": { @@ -8295,6 +8315,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": "*", @@ -8305,6 +8326,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": { @@ -8315,13 +8337,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/plist": { @@ -13246,6 +13268,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": { @@ -21282,6 +21305,53 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -26410,7 +26480,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", @@ -26436,9 +26506,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "devOptional": true, "license": "MIT" }, diff --git a/package.json b/package.json index 4bd03c657..8d7ee6287 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "devDependencies": { "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", + "@playwright/test": "^1.51.1", "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", + "@types/node": "^22.14.1", "concurrently": "^8.2.2", "eslint": "^9.26.0", "fs-extra": "^11.1.1", @@ -30,6 +32,7 @@ "jest": "^29.2.0", "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", + "playwright": "^1.51.1", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", "rimraf": "^6.0.1", @@ -57,7 +60,8 @@ "build:electron:rpm": "./scripts/build-electron.sh rpm", "build:electron:snap": "./scripts/build-electron.sh snap", "watch:common": "npm run watch --workspace=packages/bruno-common", - "test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js", + "test:codegen": "node playwright/codegen.ts", + "test:e2e": "playwright test", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "prepare": "husky install", "lint": "npx eslint ./" diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..b0adb030f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e-tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? undefined : 1, + reporter: 'html', + use: { + trace: 'on-first-retry' + }, + + projects: [ + { + name: 'Bruno Electron App' + } + ], + + webServer: { + command: 'npm run dev:web', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI + } +}); diff --git a/playwright/codegen.ts b/playwright/codegen.ts new file mode 100644 index 000000000..da2bfcb1f --- /dev/null +++ b/playwright/codegen.ts @@ -0,0 +1,13 @@ +const path = require('path'); +const { startApp } = require('./electron.ts'); + +async function main() { + const { app, context } = await startApp(); + let outputFile = process.argv[2]?.trim(); + if (outputFile && !/\.(ts|js)$/.test(outputFile)) { + outputFile = path.join(__dirname, '../e2e-tests/', outputFile + '.spec.ts'); + } + await context._enableRecorder({ language: 'playwright-test', mode: 'recording', outputFile }); +} + +main(); diff --git a/playwright/electron.ts b/playwright/electron.ts new file mode 100644 index 000000000..bc49363f1 --- /dev/null +++ b/playwright/electron.ts @@ -0,0 +1,13 @@ +const path = require('path'); +const { _electron: electron } = require('playwright'); + +const electronAppPath = path.join(__dirname, '../packages/bruno-electron'); + +exports.startApp = async () => { + const app = await electron.launch({ args: [electronAppPath] }); + const context = await app.context(); + + app.process().stdout.on('data', (data) => console.log(data.toString())); + app.process().stderr.on('data', (error) => console.error(error.toString())); + return { app, context }; +}; diff --git a/playwright/index.ts b/playwright/index.ts new file mode 100644 index 000000000..ca865437d --- /dev/null +++ b/playwright/index.ts @@ -0,0 +1,23 @@ +import { test as baseTest, ElectronApplication, Page } from '@playwright/test'; + +const { startApp } = require('./electron.ts'); + +export const test = baseTest.extend<{ page: Page }, { electronApp: ElectronApplication }>({ + electronApp: [ + async ({}, use) => { + const { app: electronApp, context } = await startApp(); + + await use(electronApp); + await context.close(); + await electronApp.close(); + }, + { scope: 'worker' } + ], + page: async ({ electronApp }, use) => { + const page = await electronApp.firstWindow(); + await use(page); + await page.reload(); + } +}); + +export * from '@playwright/test' diff --git a/scripts/playwright-codegen.js b/scripts/playwright-codegen.js deleted file mode 100644 index ae96c7a41..000000000 --- a/scripts/playwright-codegen.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path'); -const timer = require('node:timers/promises'); -const { _electron: electron } = require('playwright'); - -const electronAppPath = path.join(__dirname, '../packages/bruno-electron'); - -(async () => { - const browser = await electron.launch({ args: [electronAppPath] }); - const context = await browser.context(); - await context.route('**/*', (route) => route.continue()); - - while (true) { - if(browser.windows().length) break; - await timer.setTimeout(200); - } - await browser.windows()[0].pause(); -})(); From f58477931f704b8df6d0eb9d6d11d2c1db758350 Mon Sep 17 00:00:00 2001 From: Pooja Date: Mon, 12 May 2025 21:37:42 +0530 Subject: [PATCH 813/904] feat: add support for oauth2 in cli (#4578) Co-authored-by: Pooja Belaramani <109731557+poojabela@users.noreply.github.com> --- package-lock.json | 8 + .../bruno-cli/src/runner/interpolate-vars.js | 31 +++ packages/bruno-cli/src/runner/oauth2.js | 6 + .../bruno-cli/src/runner/prepare-request.js | 66 +++++- .../src/runner/run-single-request.js | 28 +++ packages/bruno-cli/src/runner/tokenStore.js | 22 ++ packages/bruno-cli/src/utils/collection.js | 25 ++- .../tests/runner/prepare-request.spec.js | 67 ++++++ packages/bruno-requests/package.json | 3 + packages/bruno-requests/rollup.config.js | 5 +- packages/bruno-requests/src/auth/index.ts | 3 +- .../bruno-requests/src/auth/oauth2-helper.ts | 199 ++++++++++++++++++ packages/bruno-requests/src/index.ts | 2 +- 13 files changed, 456 insertions(+), 9 deletions(-) create mode 100644 packages/bruno-cli/src/runner/oauth2.js create mode 100644 packages/bruno-cli/src/runner/tokenStore.js create mode 100644 packages/bruno-requests/src/auth/oauth2-helper.ts diff --git a/package-lock.json b/package-lock.json index 8bf023cfd..f6efd03a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8358,6 +8358,11 @@ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" + }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", @@ -30570,6 +30575,9 @@ "name": "@usebruno/requests", "version": "0.1.0", "license": "MIT", + "dependencies": { + "@types/qs": "^6.9.18" + }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 2d11350eb..7ec7041b5 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -156,6 +156,37 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc delete request.basicAuth; } + if (request?.oauth2?.grantType) { + switch (request.oauth2.grantType) { + case 'password': + request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; + request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || ''; + request.oauth2.username = _interpolate(request.oauth2.username) || ''; + request.oauth2.password = _interpolate(request.oauth2.password) || ''; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || ''; + request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || ''; + break; + case 'client_credentials': + request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; + request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || ''; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || ''; + request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || ''; + break; + default: + break; + } + } + if (request.awsv4config) { request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || ''; request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || ''; diff --git a/packages/bruno-cli/src/runner/oauth2.js b/packages/bruno-cli/src/runner/oauth2.js new file mode 100644 index 000000000..f5335dc55 --- /dev/null +++ b/packages/bruno-cli/src/runner/oauth2.js @@ -0,0 +1,6 @@ +const { getOAuth2Token } = require('@usebruno/requests'); +const tokenStore = require('./tokenStore'); + +module.exports = { + getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore) +}; \ No newline at end of file diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 4b0a324ef..bd63704a2 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -1,7 +1,7 @@ const { get, each, filter } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); -const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection'); +const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection'); const { createFormData } = require('../utils/form-data'); const prepareRequest = (item = {}, collection = {}) => { @@ -16,6 +16,7 @@ const prepareRequest = (item = {}, collection = {}) => { mergeHeaders(collection, request, requestTreePath); mergeScripts(collection, request, requestTreePath, scriptFlow); mergeVars(collection, request, requestTreePath); + mergeAuth(collection, request, requestTreePath); } each(get(request, 'headers', []), (h) => { @@ -73,6 +74,37 @@ const prepareRequest = (item = {}, collection = {}) => { }; } + if (collectionAuth.mode === 'oauth2') { + const grantType = get(collectionAuth, 'oauth2.grantType'); + + if (grantType === 'client_credentials') { + axiosRequest.oauth2 = { + grantType, + accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'), + clientId: get(collectionAuth, 'oauth2.clientId'), + clientSecret: get(collectionAuth, 'oauth2.clientSecret'), + scope: get(collectionAuth, 'oauth2.scope'), + credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey') + }; + } else if (grantType === 'password') { + axiosRequest.oauth2 = { + grantType, + accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'), + username: get(collectionAuth, 'oauth2.username'), + password: get(collectionAuth, 'oauth2.password'), + clientId: get(collectionAuth, 'oauth2.clientId'), + clientSecret: get(collectionAuth, 'oauth2.clientSecret'), + scope: get(collectionAuth, 'oauth2.scope'), + credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey') + }; + } + } if (collectionAuth.mode === 'awsv4') { axiosRequest.awsv4config = { accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'), @@ -169,6 +201,38 @@ const prepareRequest = (item = {}, collection = {}) => { }; } + if (request.auth.mode === 'oauth2') { + const grantType = get(request, 'auth.oauth2.grantType'); + + if (grantType === 'client_credentials') { + axiosRequest.oauth2 = { + grantType, + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope'), + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'), + tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey') + }; + } else if (grantType === 'password') { + axiosRequest.oauth2 = { + grantType, + username: get(request, 'auth.oauth2.username'), + password: get(request, 'auth.oauth2.password'), + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope'), + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'), + tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'), + tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey') + }; + } + } + if (request.auth.mode === 'apikey') { if (request.auth.apikey?.placement === 'header') { axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 2bf7bc435..cb7eb98b5 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -22,6 +22,7 @@ const path = require('path'); const { parseDataFromResponse } = require('../utils/common'); const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); const { createFormData } = require('../utils/form-data'); +const { getOAuth2Token } = require('./oauth2'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); const { addDigestInterceptor } = require('@usebruno/requests'); @@ -303,6 +304,33 @@ const runSingleRequest = async function ( } } + // Handle OAuth2 authentication + if (request.oauth2) { + try { + const token = await getOAuth2Token(request.oauth2); + if (token) { + const { tokenPlacement = 'header', tokenHeaderPrefix = 'Bearer', tokenQueryKey = 'access_token' } = request.oauth2; + + if (tokenPlacement === 'header') { + request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`; + } else if (tokenPlacement === 'url') { + try { + const url = new URL(request.url); + url.searchParams.set(tokenQueryKey, token); + request.url = url.toString(); + } catch (error) { + console.error('Error applying OAuth2 token to URL:', error.message); + } + } + } + } catch (error) { + console.error('OAuth2 token fetch error:', error.message); + } + + // Remove oauth2 config from request to prevent it from being sent + delete request.oauth2; + } + let response, responseTime; try { diff --git a/packages/bruno-cli/src/runner/tokenStore.js b/packages/bruno-cli/src/runner/tokenStore.js new file mode 100644 index 000000000..1bc5c3273 --- /dev/null +++ b/packages/bruno-cli/src/runner/tokenStore.js @@ -0,0 +1,22 @@ +// In-memory token store implementation for OAuth2 tokens +const tokenStore = { + tokens: new Map(), + + // Save a token with optional expiry information + async saveToken(serviceId, account, token) { + this.tokens.set(`${serviceId}:${account}`, token); + return true; + }, + + // Get a token + async getToken(serviceId, account) { + return this.tokens.get(`${serviceId}:${account}`); + }, + + // Delete a token + async deleteToken(serviceId, account) { + return this.tokens.delete(`${serviceId}:${account}`); + } +}; + +module.exports = tokenStore; \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 3616c1f87..649fb2a33 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -310,6 +310,24 @@ const getTreePathFromCollectionToItem = (collection, _item) => { return path; }; +const mergeAuth = (collection, request, requestTreePath) => { + let collectionAuth = collection?.root?.request?.auth || { mode: 'none' }; + let effectiveAuth = collectionAuth; + + for (let i of requestTreePath) { + if (i.type === 'folder') { + const folderAuth = i?.root?.request?.auth; + if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') { + effectiveAuth = folderAuth; + } + } + } + + if (request.auth && request.auth.mode === 'inherit') { + request.auth = effectiveAuth; + } +} + const getAllRequestsInFolder = (folderItems = [], recursive = true) => { let requests = []; @@ -461,8 +479,6 @@ const processCollectionItems = async (items = [], currentPath) => { } }; - - module.exports = { createCollectionJsonFromPathname, mergeHeaders, @@ -470,7 +486,8 @@ module.exports = { mergeScripts, findItemInCollection, getTreePathFromCollectionToItem, + createCollectionFromBrunoObject, + mergeAuth, getAllRequestsInFolder, - getAllRequestsAtFolderRoot, - createCollectionFromBrunoObject + getAllRequestsAtFolderRoot } \ No newline at end of file diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js index 4c6e86c69..d532dcff1 100644 --- a/packages/bruno-cli/tests/runner/prepare-request.spec.js +++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js @@ -150,6 +150,72 @@ describe('prepare-request: prepareRequest', () => { }); }); + describe('OAuth2 Authentication', () => { + it('If collection auth is OAuth2 with client credentials grant type', () => { + collection.root.request.auth = { + mode: 'oauth2', + oauth2: { + grantType: 'client_credentials', + accessTokenUrl: 'https://auth.example.com/token', + clientId: 'test_client_id', + clientSecret: 'test_client_secret', + scope: 'read write', + credentialsPlacement: 'header', + tokenPlacement: 'header', + tokenHeaderPrefix: 'Bearer', + tokenQueryKey: 'access_token' + } + }; + + const result = prepareRequest(item, collection); + + expect(result.oauth2).toBeDefined(); + expect(result.oauth2.grantType).toBe('client_credentials'); + expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token'); + expect(result.oauth2.clientId).toBe('test_client_id'); + expect(result.oauth2.clientSecret).toBe('test_client_secret'); + expect(result.oauth2.scope).toBe('read write'); + expect(result.oauth2.credentialsPlacement).toBe('header'); + expect(result.oauth2.tokenPlacement).toBe('header'); + expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer'); + expect(result.oauth2.tokenQueryKey).toBe('access_token'); + }); + + it('If collection auth is OAuth2 with password grant type', () => { + collection.root.request.auth = { + mode: 'oauth2', + oauth2: { + grantType: 'password', + accessTokenUrl: 'https://auth.example.com/token', + username: 'test_user', + password: 'test_password', + clientId: 'test_client_id', + clientSecret: 'test_client_secret', + scope: 'read write', + credentialsPlacement: 'body', + tokenPlacement: 'url', + tokenHeaderPrefix: 'Bearer', + tokenQueryKey: 'access_token' + } + }; + + const result = prepareRequest(item, collection); + + expect(result.oauth2).toBeDefined(); + expect(result.oauth2.grantType).toBe('password'); + expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token'); + expect(result.oauth2.username).toBe('test_user'); + expect(result.oauth2.password).toBe('test_password'); + expect(result.oauth2.clientId).toBe('test_client_id'); + expect(result.oauth2.clientSecret).toBe('test_client_secret'); + expect(result.oauth2.scope).toBe('read write'); + expect(result.oauth2.credentialsPlacement).toBe('body'); + expect(result.oauth2.tokenPlacement).toBe('url'); + expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer'); + expect(result.oauth2.tokenQueryKey).toBe('access_token'); + }); + }); + describe('AWS v4 Authentication', () => { it('If collection auth is AWS v4', () => { collection.root.request.auth = { @@ -228,6 +294,7 @@ describe('prepare-request: prepareRequest', () => { }; const result = prepareRequest(item, collection); + const expected = { username: 'testUser', password: 'testPass123' diff --git a/packages/bruno-requests/package.json b/packages/bruno-requests/package.json index f43820549..1b4fc8311 100644 --- a/packages/bruno-requests/package.json +++ b/packages/bruno-requests/package.json @@ -28,5 +28,8 @@ }, "overrides": { "rollup": "3.29.5" + }, + "dependencies": { + "@types/qs": "^6.9.18" } } diff --git a/packages/bruno-requests/rollup.config.js b/packages/bruno-requests/rollup.config.js index fa04da640..83422321d 100644 --- a/packages/bruno-requests/rollup.config.js +++ b/packages/bruno-requests/rollup.config.js @@ -31,7 +31,8 @@ module.exports = [ }), commonjs(), typescript({ tsconfig: './tsconfig.json' }), - terser() - ] + terser(), + ], + external: ['axios', 'qs'] } ]; diff --git a/packages/bruno-requests/src/auth/index.ts b/packages/bruno-requests/src/auth/index.ts index cd302427c..082ca796b 100644 --- a/packages/bruno-requests/src/auth/index.ts +++ b/packages/bruno-requests/src/auth/index.ts @@ -1 +1,2 @@ -export { addDigestInterceptor } from './digestauth-helper'; \ No newline at end of file +export { addDigestInterceptor } from './digestauth-helper'; +export { getOAuth2Token } from './oauth2-helper'; \ No newline at end of file diff --git a/packages/bruno-requests/src/auth/oauth2-helper.ts b/packages/bruno-requests/src/auth/oauth2-helper.ts new file mode 100644 index 000000000..e73ac7158 --- /dev/null +++ b/packages/bruno-requests/src/auth/oauth2-helper.ts @@ -0,0 +1,199 @@ +import axios, { AxiosError } from 'axios'; +import qs from 'qs'; + +export interface TokenStore { + saveToken(serviceId: string, account: string, token: any): Promise; + getToken(serviceId: string, account: string): Promise; + deleteToken(serviceId: string, account: string): Promise; +} + +export interface OAuth2Config { + grantType: 'client_credentials' | 'password'; + accessTokenUrl: string; + clientId?: string; + clientSecret?: string; + username?: string; + password?: string; + scope?: string; + credentialsPlacement?: 'header' | 'body'; +} + +interface RequestConfig { + headers: { + 'Content-Type': string; + 'Authorization'?: string; + }; +} + +interface ClientCredentialsData { + grant_type: string; + scope: string; + client_id?: string; + client_secret?: string; +} + +interface PasswordGrantData { + grant_type: string; + username: string; + password: string; + scope: string; + client_id?: string; + client_secret?: string; +} + +/** + * Fetches an OAuth2 token using client credentials grant + */ +const fetchTokenClientCredentials = async (oauth2Config: OAuth2Config) => { + const { + accessTokenUrl, + clientId, + clientSecret, + scope, + credentialsPlacement = 'header' + } = oauth2Config; + + if (!accessTokenUrl || !clientId) { + throw new Error('Missing required OAuth2 parameters'); + } + + const data: ClientCredentialsData = { + grant_type: 'client_credentials', + scope: scope || '' + }; + + const config: RequestConfig = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + // Handle credentials placement + if (credentialsPlacement === 'header') { + config.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret || ''}`).toString('base64')}`; + } else { + // Credentials in body + data.client_id = clientId; + if (clientSecret) { + data.client_secret = clientSecret; + } + } + + try { + const response = await axios.post(accessTokenUrl, qs.stringify(data), config); + return response.data; + } catch (error) { + if (error instanceof Error) { + console.error('CLIENT_CREDENTIALS: Error fetching OAuth2 token:', error.message); + } + throw error; + } +}; + +/** + * Fetches an OAuth2 token using password grant + */ +const fetchTokenPassword = async (oauth2Config: OAuth2Config) => { + const { + accessTokenUrl, + clientId, + clientSecret, + username, + password, + scope, + credentialsPlacement = 'header' + } = oauth2Config; + + if (!accessTokenUrl || !username || !password) { + throw new Error('Missing required OAuth2 parameters for password grant'); + } + + const data: PasswordGrantData = { + grant_type: 'password', + username, + password, + scope: scope || '' + }; + + const config: RequestConfig = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + // Handle credentials placement + if (credentialsPlacement === 'header' && clientId) { + config.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret || ''}`).toString('base64')}`; + } else if (clientId) { + // Credentials in body + data.client_id = clientId; + if (clientSecret) { + data.client_secret = clientSecret; + } + } + + try { + const response = await axios.post(accessTokenUrl, qs.stringify(data), config); + return response.data; + } catch (error) { + if (error instanceof AxiosError && error.response) { + console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message); + console.error('Status:', error.response.status, 'Response:', error.response.data); + } else if (error instanceof Error) { + console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message); + } + throw error; + } +}; + +/** + * Manages OAuth2 token retrieval and storage + */ +export const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore): Promise => { + const { grantType, clientId, accessTokenUrl } = oauth2Config; + + if (!grantType || !accessTokenUrl) { + throw new Error('Missing required OAuth2 parameters: grantType or accessTokenUrl'); + } + + const serviceId = accessTokenUrl; + const account = clientId || oauth2Config.username || 'default'; + + // Check if we already have a token stored + const existingToken = await tokenStore.getToken(serviceId, account); + + if (existingToken) { + // Check if token is expired + if (existingToken.expires_at && existingToken.expires_at > Date.now()) { + return existingToken.access_token; + } + } + + // No valid token found, fetch a new one + try { + let tokenResponse; + + if (grantType === 'client_credentials') { + tokenResponse = await fetchTokenClientCredentials(oauth2Config); + } else if (grantType === 'password') { + tokenResponse = await fetchTokenPassword(oauth2Config); + } else { + throw new Error(`Unsupported grant type: ${grantType}`); + } + + // Calculate expiry time if expires_in is provided + if (tokenResponse.expires_in) { + tokenResponse.expires_at = Date.now() + tokenResponse.expires_in * 1000; + } + + // Store the token + await tokenStore.saveToken(serviceId, account, tokenResponse); + + return tokenResponse.access_token; + } catch (error) { + if (error instanceof Error) { + console.error('Failed to get OAuth2 token:', error.message); + } + return null; + } +}; \ No newline at end of file diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 19b02f764..5513916c5 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1 +1 @@ -export { addDigestInterceptor } from './auth'; \ No newline at end of file +export { addDigestInterceptor, getOAuth2Token } from './auth'; From 458c0700043611422e3e492d93b7005e0d0d968e Mon Sep 17 00:00:00 2001 From: ganesh Date: Tue, 13 May 2025 14:10:10 +0530 Subject: [PATCH 814/904] Fix: Specify Node.js version in Contributing Guide (#4656) * fix node version on contributing file * updated to node 22 version --- contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing.md b/contributing.md index 13bbec333..2e91f5220 100644 --- a/contributing.md +++ b/contributing.md @@ -48,7 +48,7 @@ Bruno is being developed as a desktop app. You need to load the app by running t ## Install Dependencies ```bash -# use nodejs 20 version +# use nodejs 22 version nvm use # install deps From b699088dd6122463028c302ac03146143d419924 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Tue, 13 May 2025 14:25:35 +0530 Subject: [PATCH 815/904] Create/Import collection UX improvements (#4540) * Fix: Improve UX for selecting location when create/import collection Allow editing the input path where previously the `` is marked `readonly`. Also this will allow automating test using Playwright. * Fix: Import-collection select-location Modal closes on error * Improved error-toast for creating and importing collections - Added a util for formatting the error form IPC - Updated Toast global styles to prevent text overflow. Whenever long file paths are shown, it overflows the Toast container. --- .../src/components/Sidebar/CreateCollection/index.js | 8 ++++++-- .../Sidebar/ImportCollectionLocation/index.js | 4 +++- .../bruno-app/src/components/Sidebar/TitleBar/index.js | 5 +++-- packages/bruno-app/src/providers/Toaster/index.js | 10 +++++++++- packages/bruno-app/src/utils/common/error.js | 8 ++++++++ packages/bruno-app/src/utils/common/index.js | 4 ++++ 6 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 07f390901..7b331c42c 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -11,6 +11,8 @@ import PathDisplay from 'components/PathDisplay/index'; import { useState } from 'react'; import { IconArrowBackUp, IconEdit } from '@tabler/icons'; import Help from 'components/Help'; +import { multiLineMsg } from "utils/common"; +import { formatIpcError } from "utils/common/error"; const CreateCollection = ({ onClose }) => { const inputRef = useRef(); @@ -45,7 +47,7 @@ const CreateCollection = ({ onClose }) => { toast.success('Collection created!'); onClose(); }) - .catch((e) => toast.error('An error occurred while creating the collection - ' + e)); + .catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e)))); } }); @@ -113,7 +115,6 @@ const CreateCollection = ({ onClose }) => { id="collection-location" type="text" name="collectionLocation" - readOnly={true} className="block textbox mt-2 w-full cursor-pointer" autoComplete="off" autoCorrect="off" @@ -121,6 +122,9 @@ const CreateCollection = ({ onClose }) => { spellCheck="false" value={formik.values.collectionLocation || ''} onClick={browse} + onChange={e => { + formik.setFieldValue('collectionLocation', e.target.value); + }} /> {formik.touched.collectionLocation && formik.errors.collectionLocation ? (
{formik.errors.collectionLocation}
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index e67eb75fc..7c4e9f83f 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -61,7 +61,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => id="collection-location" type="text" name="collectionLocation" - readOnly={true} className="block textbox mt-2 w-full cursor-pointer" autoComplete="off" autoCorrect="off" @@ -69,6 +68,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => spellCheck="false" value={formik.values.collectionLocation || ''} onClick={browse} + onChange={e => { + formik.setFieldValue('collectionLocation', e.target.value); + }} /> {formik.touched.collectionLocation && formik.errors.collectionLocation ? ( diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js index e0b89b55d..65badf3aa 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js @@ -11,6 +11,8 @@ import { useDispatch } from 'react-redux'; import { showHomePage } from 'providers/ReduxStore/slices/app'; import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; +import { multiLineMsg } from "utils/common"; +import { formatIpcError } from "utils/common/error"; const TitleBar = () => { const [importedCollection, setImportedCollection] = useState(null); @@ -34,9 +36,8 @@ const TitleBar = () => { toast.success('Collection imported successfully'); }) .catch((err) => { - setImportCollectionLocationModalOpen(false); console.error(err); - toast.error('An error occurred while importing the collection. Check the logs for more information.'); + toast.error(multiLineMsg('An error occurred while importing the collection.', formatIpcError(err))); }); }; diff --git a/packages/bruno-app/src/providers/Toaster/index.js b/packages/bruno-app/src/providers/Toaster/index.js index 1ae25764c..10dab3297 100644 --- a/packages/bruno-app/src/providers/Toaster/index.js +++ b/packages/bruno-app/src/providers/Toaster/index.js @@ -7,9 +7,17 @@ export const ToastContext = React.createContext(); export const ToastProvider = (props) => { const { storedTheme } = useTheme(); - const toastOptions = { duration: 2000 }; + const toastOptions = { + duration: 2000, + style: { + // Break long word like file-path, URL etc. to prevent overflow + overflowWrap: 'anywhere' + } + }; + if (storedTheme === 'dark') { toastOptions.style = { + ...toastOptions.style, borderRadius: '10px', background: '#3d3d3d', color: '#fff' diff --git a/packages/bruno-app/src/utils/common/error.js b/packages/bruno-app/src/utils/common/error.js index e81e3fadc..c1ae6058c 100644 --- a/packages/bruno-app/src/utils/common/error.js +++ b/packages/bruno-app/src/utils/common/error.js @@ -34,3 +34,11 @@ export const toastError = (error, defaultErrorMsg = 'An error occurred') => { return toast.error(errorMsg); }; + +export function formatIpcError(error) { + if (!(error instanceof Error)) return error; + if (!error?.message) return ''; // Avoid returning `null` or `undefined` + // https://github.com/electron/electron/blob/659e79fc08c6ffc2f7506dd1358918d97d240147/lib/renderer/api/ipc-renderer.ts#L24-L30 + // There is no other way to get rid of this error prefix as of now. + return error.message.replace(/^Error invoking remote method '.+?': (Error: )?/, ''); +} diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 47055cbb7..7c1165ed9 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -181,4 +181,8 @@ export const getEncoding = (headers) => { // Parse the charset from content type: https://stackoverflow.com/a/33192813 const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || ''); return charsetMatch?.[1]; +} + +export const multiLineMsg = (...messages) => { + return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n'); } \ No newline at end of file From 2de7ba0d0cd5ccf164466bc1b8db24c3d04f119a Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 13 May 2025 16:06:20 +0530 Subject: [PATCH 816/904] Added combined Vars for prepareGqlIntrospectionRequest for all interpolate --- .../src/ipc/network/prepare-gql-introspection-request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index c137c4b33..46949bdb7 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common'); const { getIntrospectionQuery } = require('graphql'); const { setAuthHeaders } = require('./prepare-request'); -const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { +const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collectionRoot) => { if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, envVars); + endpoint = interpolate(endpoint, combinedVars); } const queryParams = { From ad3f5de99a8999a51981fa11805f64eeb072d775 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 13 May 2025 17:05:37 +0530 Subject: [PATCH 817/904] Added combined variable object for gqlIntrospectionRequest --- .../bruno-electron/src/ipc/network/index.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 28a49e80f..2ac9acf39 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -9,7 +9,7 @@ const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const FormData = require('form-data'); const { ipcMain } = require('electron'); -const { each, get, extend, cloneDeep } = require('lodash'); +const { each, get, extend, cloneDeep, merge } = require('lodash'); const { NtlmClient } = require('axios-ntlm'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const { interpolateString } = require('./interpolate-string'); @@ -800,9 +800,23 @@ const registerNetworkIpc = (mainWindow) => { ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { try { + // selected environment variables on collection level const envVars = getEnvVars(environment); + // collection runtime variables + const collectionrunTimeVars = collection.runtimeVariables; + // global environment variables + const globalEnvironmentVariables = collection.globalEnvironmentVariables; + // request runtime variables + const requestRunTimeVariables = _request.vars; + const combinedVars = merge( + {}, + envVars || {}, + collectionrunTimeVars || {}, + globalEnvironmentVariables || {}, + requestRunTimeVariables || {} + ); const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, envVars, _request, collectionRoot); + const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); request.timeout = preferencesUtil.getRequestTimeout(); From 0f6da35c0b867746e6a15b54c8eddb7236fc9b69 Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Tue, 13 May 2025 17:27:55 +0530 Subject: [PATCH 818/904] feat: enhance axios instance with redirect handling and cookie management --- .../bruno-cli/src/utils/axios-instance.js | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index 834cda2a8..637ff689a 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -1,5 +1,47 @@ const axios = require('axios'); const { CLI_VERSION } = require('../constants'); +const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); + +const redirectResponseCodes = [301, 302, 303, 307, 308]; +const METHOD_CHANGING_REDIRECTS = [301, 302, 303]; + +const saveCookies = (url, headers) => { + if (headers['set-cookie']) { + let setCookieHeaders = Array.isArray(headers['set-cookie']) + ? headers['set-cookie'] + : [headers['set-cookie']]; + for (let setCookieHeader of setCookieHeaders) { + if (typeof setCookieHeader === 'string' && setCookieHeader.length) { + addCookieToJar(setCookieHeader, url); + } + } + } +}; + +const createRedirectConfig = (error, redirectUrl) => { + const requestConfig = { + ...error.config, + url: redirectUrl, + headers: { ...error.config.headers } + }; + + const statusCode = error.response.status; + const originalMethod = (error.config.method || 'get').toLowerCase(); + + // For 301, 302, 303: change method to GET unless it was HEAD + if (METHOD_CHANGING_REDIRECTS.includes(statusCode) && originalMethod !== 'head') { + requestConfig.method = 'get'; + requestConfig.data = undefined; + + // Clean up headers that are no longer relevant + delete requestConfig.headers['content-length']; + delete requestConfig.headers['Content-Length']; + delete requestConfig.headers['content-type']; + delete requestConfig.headers['Content-Type']; + } + + return requestConfig; +}; /** * Function that configures axios with timing interceptors @@ -7,10 +49,13 @@ const { CLI_VERSION } = require('../constants'); * @see https://github.com/axios/axios/issues/695 * @returns {axios.AxiosInstance} */ -function makeAxiosInstance() { +function makeAxiosInstance({ requestMaxRedirects = 5 } = {}) { + let redirectCount = 0; + /** @type {axios.AxiosInstance} */ const instance = axios.create({ proxy: false, + maxRedirects: 0, headers: { "User-Agent": `bruno-runtime/${CLI_VERSION}` } @@ -18,6 +63,13 @@ function makeAxiosInstance() { instance.interceptors.request.use((config) => { config.headers['request-start-time'] = Date.now(); + + // Add cookies to request if available + const cookieString = getCookieStringForUrl(config.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + config.headers['cookie'] = cookieString; + } + return config; }); @@ -26,6 +78,9 @@ function makeAxiosInstance() { const end = Date.now(); const start = response.config.headers['request-start-time']; response.headers['request-duration'] = end - start; + redirectCount = 0; + + saveCookies(response.config.url, response.headers); return response; }, (error) => { @@ -33,6 +88,37 @@ function makeAxiosInstance() { const end = Date.now(); const start = error.config.headers['request-start-time']; error.response.headers['request-duration'] = end - start; + + if (redirectResponseCodes.includes(error.response.status)) { + if (redirectCount >= requestMaxRedirects) { + const err = new Error(`Maximum redirects (${requestMaxRedirects}) exceeded`); + err.originalError = error; + return Promise.reject(err); + } + + const locationHeader = error.response.headers.location; + if (!locationHeader) { + return Promise.reject(new Error('Redirect location header missing')); + } + + redirectCount++; + let redirectUrl = locationHeader; + + if (!locationHeader.match(/^https?:\/\//i)) { + const URL = require('url'); + redirectUrl = URL.resolve(error.config.url, locationHeader); + } + + saveCookies(redirectUrl, error.response.headers); + const requestConfig = createRedirectConfig(error, redirectUrl); + + const cookieString = getCookieStringForUrl(redirectUrl); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + requestConfig.headers['cookie'] = cookieString; + } + + return instance(requestConfig); + } } return Promise.reject(error); } From 9d6ab69d376cf7dd62253307a33c179aff54fb4c Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 13 May 2025 19:21:30 +0530 Subject: [PATCH 819/904] eslint - node out of memeory issue --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d7ee6287..abfe137c2 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "test:e2e": "playwright test", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "prepare": "husky install", - "lint": "npx eslint ./" + "lint": "node --max_old_space_size=4096 $(npx which eslint)" }, "overrides": { "rollup": "3.29.5", From 361add3385472a575e6acd6f2619093d2706274c Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 13 May 2025 19:41:39 +0530 Subject: [PATCH 820/904] handle folder run action while a collection run is in progress (#4658) Co-authored-by: lohit --- .../CollectionItem/RunCollectionItem/index.js | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index 8aaaa749c..f56d408b0 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -13,6 +13,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid)); + const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended'); const onSubmit = (recursive) => { dispatch( @@ -22,10 +23,24 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => { type: 'collection-runner' }) ); - dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive)); + if (!isCollectionRunInProgress) { + dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive)); + } onClose(); }; + const handleViewRunner = (e) => { + e.preventDefault(); + dispatch( + addTab({ + uid: uuid(), + collectionUid: collection.uid, + type: 'collection-runner' + }) + ); + onClose(); + } + const getRequestsCount = (items) => { const requestTypes = ['http-request', 'graphql-request'] return items.filter(req => requestTypes.includes(req.type)).length; @@ -55,22 +70,34 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
This will run all the requests in this folder and all its subfolders.
{isFolderLoading ?
Requests in this folder are still loading.
: null} + {isCollectionRunInProgress ?
A Collection Run is already in progress.
: null}
- - - - - - + { + isCollectionRunInProgress ? + + + + : + <> + + + + + + + + }
)} From c407b73c223599e7a895e8e8449b3dac12483a69 Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Tue, 13 May 2025 19:42:00 +0530 Subject: [PATCH 821/904] Updating contributing.md --- contributing.md | 58 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/contributing.md b/contributing.md index 2e91f5220..f8e0909e2 100644 --- a/contributing.md +++ b/contributing.md @@ -15,15 +15,15 @@ | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md) -| [Nederlands](docs/contributing/contributing_nl.md) +| [Dutch](docs/contributing/contributing_nl.md) ## Let's make Bruno better, together!! -We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer. +We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer. ### Technology Stack -Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections) +Bruno is built using React and Electron. Libraries we use @@ -42,8 +42,9 @@ Libraries we use ## Development -Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal. +Bruno is a desktop app. Below are the instructions to run Bruno. +> Note: We use React for the frontend and rsbuild for build and dev server. ## Install Dependencies @@ -55,9 +56,13 @@ nvm use npm i --legacy-peer-deps ``` -### Local Development (Option 1) +### Local Development -```bash +#### Build packages + +##### Option 1 + +```bash # build packages npm run build:graphql-docs npm run build:bruno-query @@ -67,7 +72,19 @@ npm run build:bruno-requests # bundle js sandbox libraries npm run sandbox:bundle-libraries --workspace=packages/bruno-js +``` +##### Option 2 +```bash +# install dependencies and setup +npm run setup +``` + +#### Run the app + +##### Option 1 + +```bash # run react app (terminal 1) npm run dev:web @@ -75,12 +92,8 @@ npm run dev:web npm run dev:electron ``` -### Local Development (Option 2) - +##### Option 2 ```bash -# install dependencies and setup -npm run setup - # run electron and react app concurrently npm run dev ``` @@ -103,7 +116,28 @@ find . -type f -name "package-lock.json" -delete ```bash # run bruno-schema tests -npm test --workspace=packages/bruno-schema +npm run test --workspace=packages/bruno-schema + +# run bruno-query tests +npm run test --workspace=packages/bruno-query + +# run bruno-common tests +npm run test --workspace=packages/bruno-common + +# run bruno-converters tests +npm run test --workspace=packages/bruno-converters + +# run bruno-app tests +npm run test --workspace=packages/bruno-app + +# run bruno-electron tests +npm run test --workspace=packages/bruno-electron + +# run bruno-lang tests +npm run test --workspace=packages/bruno-lang + +# run bruno-toml tests +npm run test --workspace=packages/bruno-toml # run tests over all workspaces npm test --workspaces --if-present From 56497991675a0cec37782bcf37ac56fc3a8a1c6b Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Tue, 13 May 2025 20:02:29 +0530 Subject: [PATCH 822/904] feat: add maxRedirects configuration to runSingleRequest --- packages/bruno-cli/src/runner/run-single-request.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 774587614..aee23ae3e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -305,10 +305,18 @@ const runSingleRequest = async function ( } } + let requestMaxRedirects = request.maxRedirects + request.maxRedirects = 0 + + // Set default value for requestMaxRedirects if not explicitly set + if (requestMaxRedirects === undefined) { + requestMaxRedirects = 5; // Default to 5 redirects + } + let response, responseTime; try { - let axiosInstance = makeAxiosInstance(); + let axiosInstance = makeAxiosInstance({ requestMaxRedirects }); if (request.ntlmConfig) { axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) delete request.ntlmConfig; From c14d3f427443f56252f21a72c5084862f5bd70fc Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Wed, 14 May 2025 10:46:14 +0530 Subject: [PATCH 823/904] feat: add test case for redirects with cookie authentication --- .../redirects/Test Redirects With Cookies.bru | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru diff --git a/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru b/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru new file mode 100644 index 000000000..202390c1e --- /dev/null +++ b/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru @@ -0,0 +1,23 @@ +meta { + name: Test Redirects With Cookies + type: http + seq: 1 +} + +post { + url: {{httpfaker}}/api/auth/cookie-redirect/login + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + username: test-user +} + +assert { + res.status: 200 +} \ No newline at end of file From 2f58379feb9af1355904bf9c992cd0c94c70a354 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Wed, 14 May 2025 11:54:33 +0530 Subject: [PATCH 824/904] Customize Playwright reporter and retries for dev and CI env --- playwright.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index b0adb030f..684477e40 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,12 +1,19 @@ import { defineConfig, devices } from '@playwright/test'; +const reporter: string[][string] = [['list'], ['html']]; + +if (process.env.CI) { + reporter.push(["github"]); +} + + export default defineConfig({ testDir: './e2e-tests', fullyParallel: false, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 1 : 0, workers: process.env.CI ? undefined : 1, - reporter: 'html', + reporter, use: { trace: 'on-first-retry' }, From 99274248269ba90013325ac05df545fc7e98d2f4 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:22:39 +0530 Subject: [PATCH 825/904] Added mergeEnvironmentVariables method in electron common utils --- packages/bruno-electron/src/utils/common.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index a855e5523..ea1386239 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -137,6 +137,12 @@ const parseDataFromRequest = (request) => { return parseDataFromResponse(requestCopy); }; +const mergeEnvironmentVariables = (...objects) => { + return objects.reduce((acc, obj) => { + return { ...acc, ...obj }; + }, {}); +}; + module.exports = { uuid, stringifyJson, From 5000bb8db33d2e3681fd33dc8d3f4009e85e71fe Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:23:32 +0530 Subject: [PATCH 826/904] Added testcases for mergeEnvironmentVariables method --- .../bruno-electron/tests/utils/common.spec.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 077aac16d..0aedd4672 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -1,4 +1,4 @@ -const { flattenDataForDotNotation } = require('../../src/utils/common'); +const { flattenDataForDotNotation, mergeEnvironmentVariables } = require('../../src/utils/common'); describe('utils: flattenDataForDotNotation', () => { test('Flatten a simple object with dot notation', () => { @@ -82,4 +82,29 @@ describe('utils: flattenDataForDotNotation', () => { expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); +}); + + +describe('utils: mergeEnvironmentVariables', () => { + test('Merge two objects', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { c: 3, d: 4 }; + const merged = mergeEnvironmentVariables(obj1, obj2); + expect(merged).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); + // test merge objects with redundant keys + test('Merge objects with redundant keys', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { b: 3, c: 4 }; + const merged = mergeEnvironmentVariables(obj1, obj2); + expect(merged).toEqual({ a: 1, b: 3, c: 4 }); + }); + // test merge objects with multiple redundant keys + test('Merge objects with multiple redundant keys', () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { b: 3, c: 4 }; + const obj3 = { c: 5, d: 6 }; + const merged = mergeEnvironmentVariables(obj1, obj2, obj3); + expect(merged).toEqual({ a: 1, b: 3, c: 5, d: 6 }); + }); }); \ No newline at end of file From 0ca2891166bc1409f9ffc8548e5b1e9f3e745a83 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:24:09 +0530 Subject: [PATCH 827/904] Added mergeEnvironmentVariables method in electron common utils export --- packages/bruno-electron/src/utils/common.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index ea1386239..de0359433 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -153,5 +153,6 @@ module.exports = { generateUidBasedOnHash, flattenDataForDotNotation, parseDataFromResponse, - parseDataFromRequest + parseDataFromRequest, + mergeEnvironmentVariables }; From 8e91640084235aed8313dcdf68063e8ff1daa5b6 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:25:41 +0530 Subject: [PATCH 828/904] Added mergeEnvironmentVariables method for gql prep method --- packages/bruno-electron/src/ipc/network/index.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2ac9acf39..5b5368e2c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); const { makeAxiosInstance } = require('./axios-instance'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); -const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common'); +const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest, mergeEnvironmentVariables } = require('../../utils/common'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); @@ -808,13 +808,14 @@ const registerNetworkIpc = (mainWindow) => { const globalEnvironmentVariables = collection.globalEnvironmentVariables; // request runtime variables const requestRunTimeVariables = _request.vars; - const combinedVars = merge( - {}, - envVars || {}, - collectionrunTimeVars || {}, - globalEnvironmentVariables || {}, - requestRunTimeVariables || {} + + const combinedVars = mergeEnvironmentVariables( + envVars, + collectionrunTimeVars, + globalEnvironmentVariables, + requestRunTimeVariables ); + const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From d7ccf1454e6a2427a50727ca683fdd0ff95ff19a Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 14 May 2025 14:05:49 +0530 Subject: [PATCH 829/904] revert lint-staged step, make lint check as part of gh unit-tests workflow --- .github/workflows/tests.yml | 3 + .husky/pre-commit | 3 - package-lock.json | 706 ------------------------------------ package.json | 8 +- 4 files changed, 4 insertions(+), 716 deletions(-) delete mode 100755 .husky/pre-commit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e733dab4d..144cdee5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,9 @@ jobs: npm run build --workspace=packages/bruno-converters npm run build --workspace=packages/bruno-requests + - name: Lint Check + run: npm run lint + # tests - name: Test Package bruno-js run: npm run test --workspace=packages/bruno-js diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 166ee6873..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -# .husky/pre-commit - -npx lint-staged \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 03a70cccd..9e78ac936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,7 @@ "eslint": "^9.26.0", "fs-extra": "^11.1.1", "globals": "^16.1.0", - "husky": "^8.0.3", "jest": "^29.2.0", - "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "playwright": "^1.51.1", "pretty-quick": "^3.1.3", @@ -13345,19 +13343,6 @@ "node": ">=4" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -15050,19 +15035,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -15993,22 +15965,6 @@ "ms": "^2.0.0" } }, - "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/i18next": { "version": "24.1.2", "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.2.tgz", @@ -18320,409 +18276,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/lint-staged": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", - "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.7.0" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/lint-staged/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/lint-staged/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/lint-staged/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lint-staged/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/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==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/listr2": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", - "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/load-script": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", @@ -18889,222 +18442,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/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==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -19700,19 +19037,6 @@ "node": ">=8" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -21173,19 +20497,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -23559,13 +22870,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -25258,16 +24562,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", diff --git a/package.json b/package.json index 8d7ee6287..b1329d2ee 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,7 @@ "eslint": "^9.26.0", "fs-extra": "^11.1.1", "globals": "^16.1.0", - "husky": "^8.0.3", "jest": "^29.2.0", - "lint-staged": "^15.5.2", "lodash-es": "^4.17.21", "playwright": "^1.51.1", "pretty-quick": "^3.1.3", @@ -63,8 +61,7 @@ "test:codegen": "node playwright/codegen.ts", "test:e2e": "playwright test", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", - "prepare": "husky install", - "lint": "npx eslint ./" + "lint": "node --max_old_space_size=4096 $(npx which eslint)" }, "overrides": { "rollup": "3.29.5", @@ -73,8 +70,5 @@ "json-schema-typed": "8.0.1" } } - }, - "lint-staged": { - "packages/**/*.{js,jsx,ts,tsx}": "npm run lint" } } From c85d9bcd84326ed9a9aa3a983196106bb6624037 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 14 May 2025 16:01:42 +0530 Subject: [PATCH 830/904] fix: folder inherit auth --- .../FolderSettings/Auth/StyledWrapper.js | 3 + .../components/FolderSettings/Auth/index.js | 56 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js b/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js index ecb0976df..e20fde667 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js @@ -11,6 +11,9 @@ const Wrapper = styled.div` border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } + .inherit-mode-text { + color: ${(props) => props.theme.colors.text.yellow}; + } `; export default Wrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 27cd03380..05bb49bbd 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -16,6 +16,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth'; import WsseAuth from 'components/RequestPane/Auth/WsseAuth'; import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth'; import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth'; +import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index'; const GrantTypeComponentMap = ({ collection, folder }) => { const dispatch = useDispatch(); @@ -44,6 +45,49 @@ const Auth = ({ collection, folder }) => { let request = get(folder, 'root.request', {}); const authMode = get(folder, 'root.request.auth.mode'); + const getTreePathFromCollectionToFolder = (collection, _folder) => { + let path = []; + let item = findItemInCollection(collection, _folder?.uid); + while (item) { + path.unshift(item); + item = findParentItemInCollection(collection, item?.uid); + } + return path; + }; + + const getEffectiveAuthSource = () => { + if (authMode !== 'inherit') return null; + + const collectionAuth = get(collection, 'root.request.auth'); + let effectiveSource = { + type: 'collection', + name: 'Collection', + auth: collectionAuth + }; + + // Get path from collection to current folder + const folderTreePath = getTreePathFromCollectionToFolder(collection, folder); + + // Check parent folders to find closest auth configuration + // Skip the last item which is the current folder + for (let i = 0; i < folderTreePath.length - 1; i++) { + const parentFolder = folderTreePath[i]; + if (parentFolder.type === 'folder') { + const folderAuth = get(parentFolder, 'root.request.auth'); + if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') { + effectiveSource = { + type: 'folder', + name: parentFolder.name, + auth: folderAuth + }; + break; + } + } + } + + return effectiveSource; + }; + const handleSave = () => { dispatch(saveFolderRoot(collection.uid, folder.uid)); }; @@ -141,10 +185,14 @@ const Auth = ({ collection, folder }) => { ); } case 'inherit': { + const source = getEffectiveAuthSource(); return ( -
- Authentication settings will be inherited from the collection. -
+ <> +
+
Auth inherited from {source.name}:
+
{humanizeRequestAuthMode(source.auth?.mode)}
+
+ ); } case 'none': { @@ -155,6 +203,8 @@ const Auth = ({ collection, folder }) => { } }; + console.log('folder', folder); + return (
From 08881258993a586f5bc74d9c4a068d14ee5b207f Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 14 May 2025 16:12:48 +0530 Subject: [PATCH 831/904] add: default auth mode inherit in folder --- .../ReduxStore/slices/collections/actions.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 86ad618aa..8a90c7aea 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -380,7 +380,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => meta: { name: folderName, seq: items?.length + 1 - } + }, + request: { + auth: { + mode: 'inherit' + } + } } }; ipcRenderer @@ -416,7 +421,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => meta: { name: folderName, seq: items?.length + 1 - } + }, + request: { + auth: { + mode: 'inherit' + } + } } }; ipcRenderer From 45b660985e84769d0fab9eebe24c50da851a5553 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 14 May 2025 17:45:03 +0530 Subject: [PATCH 832/904] fix: ui --- .../FolderSettings/Auth/StyledWrapper.js | 3 ++ .../components/FolderSettings/Auth/index.js | 1 - .../RequestPane/Auth/ApiKeyAuth/index.js | 53 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js b/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js index e20fde667..ba243d42b 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js @@ -14,6 +14,9 @@ const Wrapper = styled.div` .inherit-mode-text { color: ${(props) => props.theme.colors.text.yellow}; } + .auth-mode-label { + color: ${(props) => props.theme.colors.text.yellow}; + } `; export default Wrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 05bb49bbd..89b109f82 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -203,7 +203,6 @@ const Auth = ({ collection, folder }) => { } }; - console.log('folder', folder); return ( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js index acf706c5a..9c7807cea 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js @@ -5,8 +5,7 @@ import { IconCaretDown } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { useTheme } from 'providers/Theme'; import SingleLineEditor from 'components/SingleLineEditor'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; -import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAPIKeyPlacement } from 'utils/collections'; @@ -63,30 +62,6 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { return ( -
-
Add To
- } placement="bottom-end"> -
{ - dropdownTippyRef?.current?.hide(); - handleAuthChange('placement', 'header'); - }} - > - Header -
-
{ - dropdownTippyRef?.current?.hide(); - handleAuthChange('placement', 'queryParam'); - }} - > - Query Param -
-
-
-
{
-
+
{ isSecret={true} />
+ + +
+ } placement="bottom-end"> +
{ + dropdownTippyRef?.current?.hide(); + handleAuthChange('placement', 'header'); + }} + > + Header +
+
{ + dropdownTippyRef?.current?.hide(); + handleAuthChange('placement', 'queryparams'); + }} + > + Query Param +
+
+
); }; From fbd3a38587b07636cc4c1bf6cb95005c89bafbf8 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 14 May 2025 17:55:50 +0530 Subject: [PATCH 833/904] fix --- .../src/components/RequestPane/Auth/ApiKeyAuth/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js index 9c7807cea..513c29500 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js @@ -71,7 +71,6 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { onChange={(val) => handleAuthChange('key', val)} onRun={handleRun} collection={collection} - item={item} />
@@ -84,8 +83,6 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { onChange={(val) => handleAuthChange('value', val)} onRun={handleRun} collection={collection} - item={item} - isSecret={true} />
From d2eb2d2941531ea5805e9cfdd0e016f58408e3da Mon Sep 17 00:00:00 2001 From: anusree-bruno Date: Thu, 15 May 2025 14:11:53 +0530 Subject: [PATCH 834/904] fix: ensure timestamp and isoTimestamp return current time instead of random values --- .../bruno-common/src/utils/faker-functions.spec.ts | 12 +++++++++++- packages/bruno-common/src/utils/faker-functions.ts | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/bruno-common/src/utils/faker-functions.spec.ts b/packages/bruno-common/src/utils/faker-functions.spec.ts index 8d1b482da..1cbc574f4 100644 --- a/packages/bruno-common/src/utils/faker-functions.spec.ts +++ b/packages/bruno-common/src/utils/faker-functions.spec.ts @@ -1,10 +1,20 @@ import { mockDataFunctions } from "./faker-functions"; describe("mockDataFunctions Regex Validation", () => { + test("timestamp and isoTimestamp should return current time values", () => { + const now = Math.floor(Date.now() / 1000); + const timestamp = parseInt(mockDataFunctions.timestamp()); + const isoTimestamp = new Date(mockDataFunctions.isoTimestamp()).getTime() / 1000; + + // Allow for a 2-second difference to account for test execution time + expect(Math.abs(timestamp - now)).toBeLessThanOrEqual(2); + expect(Math.abs(isoTimestamp - now)).toBeLessThanOrEqual(2); + }); + test("all values should match their expected patterns", () => { const patterns: Record = { guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, - timestamp: /^\d{13,}$/, + timestamp: /^\d{10}$/, isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, randomAlphaNumeric: /^[\w]$/, diff --git a/packages/bruno-common/src/utils/faker-functions.ts b/packages/bruno-common/src/utils/faker-functions.ts index 64d1ed87b..1c7f14d14 100644 --- a/packages/bruno-common/src/utils/faker-functions.ts +++ b/packages/bruno-common/src/utils/faker-functions.ts @@ -2,8 +2,8 @@ import { faker } from '@faker-js/faker'; export const mockDataFunctions = { guid: () => faker.string.uuid(), - timestamp: () => faker.date.anytime().getTime().toString(), - isoTimestamp: () => faker.date.anytime().toISOString(), + timestamp: () => Math.floor(Date.now() / 1000).toString(), + isoTimestamp: () => new Date().toISOString(), randomUUID: () => faker.string.uuid(), randomAlphaNumeric: () => faker.string.alphanumeric(), randomBoolean: () => faker.datatype.boolean(), From 62595c519c3fcf2f4abb6f159e6a1312a83f6de9 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:56:30 +0530 Subject: [PATCH 835/904] Added lodash merge for combining vars before interpolateVars --- .../bruno-electron/src/ipc/network/index.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 5b5368e2c..2e6787382 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); const { makeAxiosInstance } = require('./axios-instance'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); -const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest, mergeEnvironmentVariables } = require('../../utils/common'); +const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); @@ -802,20 +802,15 @@ const registerNetworkIpc = (mainWindow) => { try { // selected environment variables on collection level const envVars = getEnvVars(environment); - // collection runtime variables - const collectionrunTimeVars = collection.runtimeVariables; - // global environment variables - const globalEnvironmentVariables = collection.globalEnvironmentVariables; - // request runtime variables - const requestRunTimeVariables = _request.vars; - const combinedVars = mergeEnvironmentVariables( - envVars, - collectionrunTimeVars, - globalEnvironmentVariables, - requestRunTimeVariables - ); - + const collectionRunTimeVars = collection.runtimeVariables; + const globalEnvironmentVars = collection.globalEnvironmentVariables; + const requestRunTimeVars = _request.vars; + + //NOTE: precedence is requestRunTimeVars < collectionRunTimeVars < envVars < globalEnvironmentVars + //NOTE: for more details https://www.geeksforgeeks.org/lodash-_-merge-method/ + const combinedVars = merge(requestRunTimeVars, collectionRunTimeVars, envVars, globalEnvironmentVars); + const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From c83436655c5448b727b2414787bcba3baf036c76 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:57:00 +0530 Subject: [PATCH 836/904] Remove mergeEnvironmnetVariables from common utils --- packages/bruno-electron/src/utils/common.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index de0359433..a855e5523 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -137,12 +137,6 @@ const parseDataFromRequest = (request) => { return parseDataFromResponse(requestCopy); }; -const mergeEnvironmentVariables = (...objects) => { - return objects.reduce((acc, obj) => { - return { ...acc, ...obj }; - }, {}); -}; - module.exports = { uuid, stringifyJson, @@ -153,6 +147,5 @@ module.exports = { generateUidBasedOnHash, flattenDataForDotNotation, parseDataFromResponse, - parseDataFromRequest, - mergeEnvironmentVariables + parseDataFromRequest }; From 6598d23ff037acb12caab5cde6667ece95e64910 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Thu, 15 May 2025 15:57:43 +0530 Subject: [PATCH 837/904] Removed mergeEnvrionmentVariables tests from common.spec.js --- .../bruno-electron/tests/utils/common.spec.js | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 0aedd4672..2d94048db 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -82,29 +82,4 @@ describe('utils: flattenDataForDotNotation', () => { expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); -}); - - -describe('utils: mergeEnvironmentVariables', () => { - test('Merge two objects', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { c: 3, d: 4 }; - const merged = mergeEnvironmentVariables(obj1, obj2); - expect(merged).toEqual({ a: 1, b: 2, c: 3, d: 4 }); - }); - // test merge objects with redundant keys - test('Merge objects with redundant keys', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { b: 3, c: 4 }; - const merged = mergeEnvironmentVariables(obj1, obj2); - expect(merged).toEqual({ a: 1, b: 3, c: 4 }); - }); - // test merge objects with multiple redundant keys - test('Merge objects with multiple redundant keys', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { b: 3, c: 4 }; - const obj3 = { c: 5, d: 6 }; - const merged = mergeEnvironmentVariables(obj1, obj2, obj3); - expect(merged).toEqual({ a: 1, b: 3, c: 5, d: 6 }); - }); }); \ No newline at end of file From f13148af3d7728fb58f87269837059eb875be373 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Wed, 5 Mar 2025 15:46:16 +0530 Subject: [PATCH 838/904] Added option to customize `userData` path on dev mode If `ELECTRON_APP_NAME` env-variable is present and its development mode, then the `appName` and `userData` path is modified accordingly. e.g. ```sh ELECTRON_APP_NAME=bruno-dev npm run dev:electron ``` Note: This doesn't change the name of the window or the names in lot of other places, only the name used by Electron internally. --- contributing.md | 10 ++++++++++ packages/bruno-electron/src/index.js | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/contributing.md b/contributing.md index f8e0909e2..7656eb5fa 100644 --- a/contributing.md +++ b/contributing.md @@ -98,6 +98,16 @@ npm run dev:electron npm run dev ``` +#### Customize Electron `userData` path +If `ELECTRON_APP_NAME` env-variable is present and its development mode, then the `appName` and `userData` path is modified accordingly. + +e.g. +```sh +ELECTRON_APP_NAME=bruno-dev npm run dev:electron +``` + +> This doesn't change the name of the window or the names in lot of other places, only the name used by Electron internally. + ### Troubleshooting You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app. diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 8495875f5..4ed47352c 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -14,6 +14,18 @@ const { format } = require('url'); const { BrowserWindow, app, session, Menu, ipcMain } = require('electron'); const { setContentSecurityPolicy } = require('electron-util'); +if (isDev && process.env.ELECTRON_APP_NAME) { + const appName = process.env.ELECTRON_APP_NAME; + const userDataPath = path.join(app.getPath("appData"), appName); + + console.log("`ELECTRON_APP_NAME` found, overriding `appName` and `userData` path: \n" + + `\t${app.getName()} -> ${appName}\n` + + `\t${app.getPath("userData")} -> ${userDataPath}`); + + app.setName(appName); + app.setPath("userData", userDataPath); +} + const menuTemplate = require('./app/menu-template'); const { openCollection } = require('./app/collections'); const LastOpenedCollections = require('./store/last-opened-collections'); From 0f318c26c2859ae21016a08d2607d1d844947398 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:42:27 +0530 Subject: [PATCH 839/904] Updated precedence in combinedVars object --- packages/bruno-electron/src/ipc/network/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2e6787382..eeb3edbbc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -807,9 +807,14 @@ const registerNetworkIpc = (mainWindow) => { const globalEnvironmentVars = collection.globalEnvironmentVariables; const requestRunTimeVars = _request.vars; - //NOTE: precedence is requestRunTimeVars < collectionRunTimeVars < envVars < globalEnvironmentVars - //NOTE: for more details https://www.geeksforgeeks.org/lodash-_-merge-method/ - const combinedVars = merge(requestRunTimeVars, collectionRunTimeVars, envVars, globalEnvironmentVars); + // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars + const combinedVars = merge( + {}, + globalEnvironmentVars, + envVars, + collectionRunTimeVars, + requestRunTimeVars + ); const collectionRoot = get(collection, 'root', {}); const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); From 9d3e42b5d4f204ceb04c59225ec8e706026e87f6 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:43:27 +0530 Subject: [PATCH 840/904] Update prepareGqlIntrospectionRequest change assignment sequence --- .../src/ipc/network/prepare-gql-introspection-request.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 46949bdb7..5e5e8a03b 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -29,14 +29,15 @@ const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collect const mapHeaders = (requestHeaders, collectionHeaders) => { const headers = {}; - each(requestHeaders, (h) => { + // Add collection headers first + each(collectionHeaders, (h) => { if (h.enabled) { headers[h.name] = h.value; } }); - // collection headers - each(collectionHeaders, (h) => { + // Then add request headers, which will overwrite if names overlap + each(requestHeaders, (h) => { if (h.enabled) { headers[h.name] = h.value; } From 3cd18d1e168a57ab7e042bc3677e4214132a8933 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:43:58 +0530 Subject: [PATCH 841/904] Added testcases for prepare-gql-introspection-request --- .../prepare-gql-introspection-request.spec.js | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js new file mode 100644 index 000000000..be9641b2d --- /dev/null +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -0,0 +1,295 @@ +const { getIntrospectionQuery } = require('graphql'); +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); + +// Mock the setAuthHeaders function +jest.mock('../../src/ipc/network/prepare-request', () => ({ + setAuthHeaders: jest.fn(req => req) +})); + +describe('prepareGqlIntrospectionRequest', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a basic GraphQL introspection request', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result).toEqual({ + method: 'POST', + url: 'https://api.example.com/graphql', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + query: getIntrospectionQuery() + }) + }); + expect(setAuthHeaders).toHaveBeenCalledWith(expect.any(Object), request, collectionRoot); + }); + + it('should interpolate variables in the endpoint', () => { + const endpoint = 'https://{{host}}/{{path}}'; + const variables = { + host: 'api.example.com', + path: 'graphql' + }; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, variables, request, collectionRoot); + + expect(result.url).toBe('https://api.example.com/graphql'); + }); + + it('should include request headers when enabled', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'Authorization', value: 'Bearer token123', enabled: true }, + { name: 'X-Custom', value: 'value1', enabled: true }, + { name: 'X-Disabled', value: 'ignored', enabled: false } + ] + }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom': 'value1' + }); + }); + + it('should include collection headers when enabled', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-API-Key', value: 'api-key-123', enabled: true }, + { name: 'X-Tenant', value: 'tenant-id', enabled: true }, + { name: 'X-Disabled-Collection', value: 'ignored', enabled: false } + ] + } + }; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-API-Key': 'api-key-123', + 'X-Tenant': 'tenant-id' + }); + }); + + it('should merge request and collection headers', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'Authorization', value: 'Bearer token123', enabled: true }, + { name: 'X-Custom', value: 'request-value', enabled: true } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-API-Key', value: 'api-key-123', enabled: true }, + { name: 'X-Custom', value: 'collection-value', enabled: true } // This should be overridden by request header + ] + } + }; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom': 'request-value', + 'X-API-Key': 'api-key-123' + }); + }); + + it('should call setAuthHeaders with the request, request object, and collectionRoot', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { headers: [] }; + const collectionRoot = { some: 'data' }; + + prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(setAuthHeaders).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: endpoint, + headers: expect.any(Object), + data: expect.any(String) + }), + request, + collectionRoot + ); + }); + + it('should handle empty endpoint', () => { + const endpoint = ''; + const request = { headers: [] }; + const collectionRoot = {}; + + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + + expect(result.url).toBe(''); + }); +}); + +describe('prepareGqlIntrospectionRequest - variable precedence and redundancy', () => { + const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; + + it('should use variable from envVars if not present in higher precedence', () => { + const endpoint = endpointTemplate; + const combinedVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); + }); + + it('should use variable from globalEnvironmentVars if present (highest precedence)', () => { + // Simulate merge order: requestVars < collectionVars < envVars < globalEnvVars + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + // merge order: requestVars, collectionVars, envVars, globalEnvVars + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); + }); + + it('should use variable from envVars if not present in globalEnvironmentVars', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal' }; // bar missing + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromEnv'); + }); + + it('should use variable from collectionVars if not present in envVars or globalEnvironmentVars', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; + const envVars = { foo: 'fromEnv' }; // bar missing + const globalEnvVars = {}; // none + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromCollection'); + }); + + it('should use variable from requestVars if not present in higher precedence', () => { + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = {}; // none + const envVars = {}; // none + const globalEnvVars = {}; // none + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); + }); + + it('should handle missing variables gracefully', () => { + const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; + const combinedVars = { foo: 'fooValue' }; + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + // Unresolved variables remain as is + expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); + }); + + it('should handle redundant variables and show correct precedence', () => { + // foo in all, bar only in requestVars + const endpoint = endpointTemplate; + const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; + const collectionVars = { foo: 'fromCollection' }; + const envVars = { foo: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal' }; + // merge order: requestVars, collectionVars, envVars, globalEnvVars + const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromRequest'); + }); +}); + +describe('prepareGqlIntrospectionRequest - header precedence', () => { + it('should use request header over collection header if names overlap', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'X-Overlap', value: 'request-value', enabled: true }, + { name: 'X-Unique', value: 'unique-value', enabled: true } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-Overlap', value: 'collection-value', enabled: true }, + { name: 'X-Collection', value: 'collection-header', enabled: true } + ] + } + }; + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Overlap': 'request-value', + 'X-Unique': 'unique-value', + 'X-Collection': 'collection-header' + }); + }); + + it('should not include disabled headers from either source', () => { + const endpoint = 'https://api.example.com/graphql'; + const request = { + headers: [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'should-not-appear', enabled: false } + ] + }; + const collectionRoot = { + request: { + headers: [ + { name: 'X-Collection-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Collection-Disabled', value: 'should-not-appear', enabled: false } + ] + } + }; + const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + expect(result.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Enabled': 'enabled', + 'X-Collection-Enabled': 'enabled' + }); + }); +}); From 5567e1b7f28cfbe2710f7a5796131e425d9caf9b Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:47:49 +0530 Subject: [PATCH 842/904] Fixed typo in prepareGqlIntrospectionRequest --- packages/bruno-electron/src/ipc/network/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index eeb3edbbc..151480fcd 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -803,17 +803,17 @@ const registerNetworkIpc = (mainWindow) => { // selected environment variables on collection level const envVars = getEnvVars(environment); - const collectionRunTimeVars = collection.runtimeVariables; + const collectionRuntimeVars = collection.runtimeVariables; const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRunTimeVars = _request.vars; + const requestRuntimeVars = _request.vars; // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars const combinedVars = merge( {}, globalEnvironmentVars, envVars, - collectionRunTimeVars, - requestRunTimeVars + collectionRuntimeVars, + requestRuntimeVars ); const collectionRoot = get(collection, 'root', {}); From 9f044c48fe1a72164e607a49eb77d3b46df8006e Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 16 May 2025 14:02:11 +0530 Subject: [PATCH 843/904] Added proxy flag for cli (#3963) * system level proxy depends on proxy flag * added proxy flag * fix: proxy flag behaviour * fix: noproxy flag logic --- packages/bruno-cli/src/commands/run.js | 16 ++++++++++++---- .../bruno-cli/src/runner/run-single-request.js | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 0ef587a9c..2cd896441 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -164,6 +164,11 @@ const builder = async (yargs) => { type: 'string', description: 'Path to the Client certificate config file used for securing the connection in the request' }) + .option('--noproxy', { + type: 'boolean', + description: 'Disable all proxy settings (both collection-defined and system proxies)', + default: false + }) .option('delay', { type:"number", description: "Delay between each requests (in miliseconds)" @@ -197,7 +202,6 @@ const builder = async (yargs) => { '$0 run request.bru --reporter-junit results.xml --reporter-html results.html', 'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory' ) - .example('$0 run request.bru --tests-only', 'Run all requests that have a test') .example( '$0 run request.bru --cacert myCustomCA.pem', @@ -208,7 +212,8 @@ const builder = async (yargs) => { 'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.' ) .example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations') - .example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.'); + .example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.') + .example('$0 run --noproxy', 'Run requests with system proxy disabled'); }; const handler = async function (argv) { @@ -233,6 +238,7 @@ const handler = async function (argv) { reporterSkipAllHeaders, reporterSkipHeaders, clientCertConfig, + noproxy, delay } = argv; const collectionPath = process.cwd(); @@ -451,7 +457,8 @@ const handler = async function (argv) { collectionRoot, runtime, collection, - runSingleRequestByPathname + runSingleRequestByPathname, + noproxy ); resolve(res?.response); } @@ -476,7 +483,8 @@ const handler = async function (argv) { collectionRoot, runtime, collection, - runSingleRequestByPathname + runSingleRequestByPathname, + noproxy ); const isLastRun = currentRequestIndex === requestItems.length - 1; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index cb7eb98b5..1f5065d85 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -41,7 +41,8 @@ const runSingleRequest = async function ( collectionRoot, runtime, collection, - runSingleRequestByPathname + runSingleRequestByPathname, + noproxy ) { const { pathname: itemPathname } = item; const relativeItemPathname = path.relative(collectionPath, itemPathname); @@ -179,15 +180,22 @@ const runSingleRequest = async function ( const collectionProxyConfig = get(brunoConfig, 'proxy', {}); const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false); - if (collectionProxyEnabled === true) { + + if (noproxy) { + // If noproxy flag is set, don't use any proxy + proxyMode = 'off'; + } else if (collectionProxyEnabled === true) { + // If collection proxy is enabled, use it proxyConfig = collectionProxyConfig; proxyMode = 'on'; - } else { - // if the collection level proxy is not set, pick the system level proxy by default, to maintain backward compatibility + } else if (collectionProxyEnabled === 'global') { + // If collection proxy is set to 'global', use system proxy const { http_proxy, https_proxy } = getSystemProxyEnvVariables(); if (http_proxy?.length || https_proxy?.length) { proxyMode = 'system'; } + } else { + proxyMode = 'off'; } if (proxyMode === 'on') { From 10640c7561a4a85eac47bd48957c3c2c6e33e5d9 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Fri, 28 Mar 2025 17:49:23 +0530 Subject: [PATCH 844/904] feat: enhance curl parsing for multipart form data - Updated `parseCurlCommand` to handle `-F` and `--form` flags, allowing for multiple form fields with file uploads. - Adjusted `curlToJson` to set `Content-Type` for multipart data and handle binary data correctly. --- .../bruno-app/src/utils/curl/curl-to-json.js | 8 +++- .../bruno-app/src/utils/curl/parse-curl.js | 39 +++++++++++++------ packages/bruno-lang/v2/src/jsonToBru.js | 5 ++- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index ea0ec2a05..a6239519e 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -183,7 +183,13 @@ const curlToJson = (curlCommand) => { if (request.query) { requestJson.queries = getQueries(request); - } else if (request.multipartUploads || request.isDataBinary) { + } else if (request.multipartUploads) { + requestJson.data = request.multipartUploads; + if (!requestJson.headers) { + requestJson.headers = {}; + } + requestJson.headers['Content-Type'] = 'multipart/form-data'; + } else if (request.isDataBinary) { Object.assign(requestJson, getFilesString(request)); } else if (typeof request.data === 'string' || typeof request.data === 'number') { Object.assign(requestJson, getDataString(request)); diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index 79db23672..afdc10395 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -37,7 +37,8 @@ const parseCurlCommand = (curlCommand) => { alias: { H: 'header', A: 'user-agent', - u: 'user' + u: 'user', + F: 'form' } }); @@ -95,17 +96,31 @@ const parseCurlCommand = (curlCommand) => { cookieString = parsedArguments.cookie; } let multipartUploads; - if (parsedArguments.F) { - multipartUploads = {}; - if (!Array.isArray(parsedArguments.F)) { - parsedArguments.F = [parsedArguments.F]; - } - parsedArguments.F.forEach((multipartArgument) => { - // input looks like key=value. value could be json or a file path prepended with an @ - const splitArguments = multipartArgument.split('=', 2); - const key = splitArguments[0]; - const value = splitArguments[1]; - multipartUploads[key] = value; + // Handle multipart form data specified via -F or --form flags + // Example: curl -F 'id=123' -F 'file=@/path/to/file.txt' + if (parsedArguments.F || parsedArguments.form) { + multipartUploads = []; + const formArgs = parsedArguments.F || parsedArguments.form; + const formArray = Array.isArray(formArgs) ? formArgs : [formArgs]; + + formArray.forEach((multipartArgument) => { + // Parse each form field using regex: + // - Group 1: Field name before = + // - Group 2: Value in quotes after = (for text fields) + // - Group 3: Value after @ (for file fields) + const match = multipartArgument.match(/^([^=]+)=(?:@?"([^"]*)"|([^@]*))?$/); + if (match) { + const key = match[1]; + const value = match[2] || match[3] || ''; + const isFile = multipartArgument.includes('@'); + + multipartUploads.push({ + name: key, + value: value, + type: isFile ? 'file' : 'text', + enabled: true + }); + } }); } if (cookieString) { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 776cca7d5..c7395e2ff 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -330,8 +330,9 @@ ${indentString(body.sparql)} } if (item.type === 'file') { - let filepaths = item.value || []; - let filestr = filepaths.join('|'); + const filepaths = Array.isArray(item.value) ? item.value : []; + const filestr = filepaths.join('|'); + const value = `@file(${filestr})`; return `${enabled}${item.name}: ${value}${contentType}`; } From 084d2bf6927ab55ad959698b0cc0f7abf43a1a1b Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 14 May 2025 13:03:57 +0545 Subject: [PATCH 845/904] test: add unit tests for basic functionality, headers, auth, and form data handling in parseCurlCommand --- .../src/utils/curl/parse-curl.spec.js | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/bruno-app/src/utils/curl/parse-curl.spec.js diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js new file mode 100644 index 000000000..13b77645c --- /dev/null +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -0,0 +1,145 @@ +const { describe, it, expect } = require('@jest/globals'); +import parseCurlCommand from './parse-curl'; + +describe('parseCurlCommand', () => { + describe('basic functionality', () => { + it('should handle basic GET request', () => { + const result = parseCurlCommand('curl https://api.example.com/users'); + expect(result).toEqual({ + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users', + method: 'get' + }); + }); + + it('should parse explicit POST method', () => { + const result = parseCurlCommand('curl -X POST https://api.example.com/users'); + expect(result).toEqual({ + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users', + method: 'post' + }); + }); + }); + + describe('headers handling', () => { + it('should parse multiple headers', () => { + const result = parseCurlCommand( + `curl -H 'Content-Type: application/json' -H 'Authorization: Bearer token' https://api.example.com` + ); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'get', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token' + } + }); + }); + + it('should parse user-agent', () => { + const result = parseCurlCommand(`curl -A 'Custom Agent' https://api.example.com`); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'get', + headers: { + 'User-Agent': 'Custom Agent' + } + }); + }); + }); + + describe('auth handling', () => { + it('should parse basic auth', () => { + const result = parseCurlCommand(`curl -u user:pass https://api.example.com`); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'get', + auth: { + mode: 'basic', + basic: { + username: 'user', + password: 'pass' + } + } + }); + }); + }); + + describe('data handling', () => { + it('should parse POST data', () => { + const result = parseCurlCommand(`curl -d 'foo=bar&baz=qux' https://api.example.com`); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'post', + data: 'foo=bar&baz=qux' + }); + }); + + it('should handle data-binary', () => { + const result = parseCurlCommand(`curl --data-binary '@file.json' https://api.example.com`); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'post', + data: '@file.json', + isDataBinary: true + }); + }); + }); + + describe('form data handling', () => { + it('should parse complex form data with multiple fields and file upload', () => { + const curlCommand = `curl --location 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d' \ + --form 'id="1"' \ + --form 'documentid="ADMINN_ID"' \ + --form 'appoinID="12376"' \ + --form 'autoclose="false"' \ + --form 'fileData=@"/path/to/file"'`; + + const result = parseCurlCommand(curlCommand); + + expect(result).toEqual({ + url: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d', + urlWithoutQuery: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d', + method: 'post', + multipartUploads: [ + { + name: 'id', + value: '1', + type: 'text', + enabled: true + }, + { + name: 'documentid', + value: 'ADMINN_ID', + type: 'text', + enabled: true + }, + { + name: 'appoinID', + value: '12376', + type: 'text', + enabled: true + }, + { + name: 'autoclose', + value: 'false', + type: 'text', + enabled: true + }, + { + name: 'fileData', + value: '/path/to/file', + type: 'file', + enabled: true + } + ] + }); + }); + }); +}); From 2bb56e8a4b0a2d69f19675709bb5e2334e23254a Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 16 May 2025 17:14:37 +0530 Subject: [PATCH 846/904] Fix: properly handling redirects with status code (#4561) * Fix: properly handling redirects with status code * fix: updated redirect logic for method change --- .../src/ipc/network/axios-instance.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index 5406f9869..e86d06fea 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -309,6 +309,27 @@ function makeAxiosInstance({ }, }; + // Apply proper HTTP redirect behavior based on status code + const statusCode = error.response.status; + const originalMethod = (error.config.method || 'get').toLowerCase(); + + // For 301, 302, 303: change method to GET unless it was HEAD + if ([301, 302, 303].includes(statusCode) && originalMethod !== 'head') { + requestConfig.method = 'get'; + requestConfig.data = undefined; + delete requestConfig.headers['content-length']; + delete requestConfig.headers['Content-Length']; + + delete requestConfig.headers['content-type']; + delete requestConfig.headers['Content-Type']; + + timeline.push({ + timestamp: new Date(), + type: 'info', + message: `Changed method from ${originalMethod.toUpperCase()} to GET for ${statusCode} redirect and removed request body`, + }); + } + if (preferencesUtil.shouldSendCookies()) { const cookieString = getCookieStringForUrl(redirectUrl); if (cookieString && typeof cookieString === 'string' && cookieString.length) { @@ -316,7 +337,7 @@ function makeAxiosInstance({ } } - try { + try { setupProxyAgents({ requestConfig, proxyMode, From 799dc9a1ca046e74aade0a5cc5749b4b58fba5cc Mon Sep 17 00:00:00 2001 From: Pooja Date: Fri, 16 May 2025 17:26:40 +0530 Subject: [PATCH 847/904] feat: add function in bruno converters package (#4669) * feat: add function in bruno converters package * add: example for openapi yaml to bruno conversion * add: converting json to yaml in converters * fix --- packages/bruno-converters/src/insomnia/insomnia-to-bruno.js | 4 ++++ packages/bruno-converters/src/openapi/openapi-to-bruno.js | 5 +++++ .../tests/insomnia/insomnia-collection-v5.spec.js | 3 +-- .../bruno-converters/tests/openapi/openapi-to-bruno.spec.js | 4 +--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js b/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js index 63af45d77..d4d829e7b 100644 --- a/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js +++ b/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js @@ -1,5 +1,6 @@ import each from 'lodash/each'; import get from 'lodash/get'; +import jsyaml from 'js-yaml'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; const parseGraphQL = (text) => { @@ -288,6 +289,9 @@ const parseInsomniaCollection = (data) => { export const insomniaToBruno = (insomniaCollection) => { try { + if(typeof insomniaCollection !== 'object') { + insomniaCollection = jsyaml.load(insomniaCollection); + } let collection; if (isInsomniaV5Export(insomniaCollection)) { collection = parseInsomniaV5Collection(insomniaCollection); diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index 73d7d0890..59929277f 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -1,5 +1,6 @@ import each from 'lodash/each'; import get from 'lodash/get'; +import jsyaml from 'js-yaml'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; const ensureUrl = (url) => { @@ -422,6 +423,10 @@ export const parseOpenApiCollection = (data) => { export const openApiToBruno = (openApiSpecification) => { try { + if(typeof openApiSpecification !== 'object') { + openApiSpecification = jsyaml.load(openApiSpecification); + } + const collection = parseOpenApiCollection(openApiSpecification); const transformedCollection = transformItemsInCollection(collection); const hydratedCollection = hydrateSeqInCollection(transformedCollection); diff --git a/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js b/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js index dfd93044a..c09065fa6 100644 --- a/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js +++ b/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js @@ -1,10 +1,9 @@ import { describe, it, expect } from '@jest/globals'; import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno'; -import jsyaml from 'js-yaml'; describe('insomnia-collection', () => { it('should correctly import a valid Insomnia v5 collection file', async () => { - const brunoCollection = insomniaToBruno(jsyaml.load(insomniaCollection)); + const brunoCollection = insomniaToBruno(insomniaCollection); expect(brunoCollection).toMatchObject(expectedOutput) }); diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js index a491f0c5c..f4a06fc44 100644 --- a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js @@ -1,11 +1,9 @@ -import jsyaml from 'js-yaml'; import { describe, it, expect } from '@jest/globals'; import openApiToBruno from '../../src/openapi/openapi-to-bruno'; describe('openapi-collection', () => { it('should correctly import a valid OpenAPI file', async () => { - const openApiSpecification = jsyaml.load(openApiCollectionString); - const brunoCollection = openApiToBruno(openApiSpecification); + const brunoCollection = openApiToBruno(openApiCollectionString); expect(brunoCollection).toMatchObject(expectedOutput); }); From 5fdb52388ab7dda5ca6e6795ee120a0df6d17293 Mon Sep 17 00:00:00 2001 From: ved-bruno Date: Fri, 16 May 2025 19:34:39 +0530 Subject: [PATCH 848/904] support element verification --- e2e-tests/test-app-support_element.spec.js | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 e2e-tests/test-app-support_element.spec.js diff --git a/e2e-tests/test-app-support_element.spec.js b/e2e-tests/test-app-support_element.spec.js new file mode 100644 index 000000000..acf3cc405 --- /dev/null +++ b/e2e-tests/test-app-support_element.spec.js @@ -0,0 +1,27 @@ +import { test, expect } from '../playwright'; + +test('Verify Support Elements', async ({ page }) => { + + // Open Preferences + await page.getByLabel('Open Preferences').click(); + + // Verify Support tab + await page.getByRole('tab', { name: 'Support' }).click(); + + const locator_twitter = page.getByRole('link', { name: 'Twitter' }); + expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno'); + + const locator_github = page.getByRole('link', { name: 'GitHub', exact: true }); + expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno'); + + const locator_discord = page.getByRole('link', { name: 'Discord', exact: true }); + expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq'); + + const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true }); + expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues'); + + const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true }); + expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com'); + + +}); \ No newline at end of file From 9132755d49268b7947f8eff9dc231ab4990c7edb Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 16 May 2025 20:52:15 +0530 Subject: [PATCH 849/904] add: noproxy in options --- packages/bruno-cli/src/commands/run.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 2cd896441..14a058f8d 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -345,6 +345,9 @@ const handler = async function (argv) { if (disableCookies) { options['disableCookies'] = true; } + if (noproxy) { + options['noproxy'] = true; + } if (cacert && cacert.length) { if (insecure) { console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`)); @@ -457,8 +460,7 @@ const handler = async function (argv) { collectionRoot, runtime, collection, - runSingleRequestByPathname, - noproxy + runSingleRequestByPathname ); resolve(res?.response); } @@ -483,8 +485,7 @@ const handler = async function (argv) { collectionRoot, runtime, collection, - runSingleRequestByPathname, - noproxy + runSingleRequestByPathname ); const isLastRun = currentRequestIndex === requestItems.length - 1; From 3c0090d86f6e398ecf05c0eb427b9c3ff99538da Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 16 May 2025 21:02:24 +0530 Subject: [PATCH 850/904] fix: runSingleRequest function --- packages/bruno-cli/src/runner/run-single-request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 3c5cc9f42..e03eef094 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -41,8 +41,7 @@ const runSingleRequest = async function ( collectionRoot, runtime, collection, - runSingleRequestByPathname, - noproxy + runSingleRequestByPathname ) { const { pathname: itemPathname } = item; const relativeItemPathname = path.relative(collectionPath, itemPathname); @@ -118,6 +117,7 @@ const runSingleRequest = async function ( const options = getOptions(); const insecure = get(options, 'insecure', false); + const noproxy = get(options, 'noproxy', false); const httpsAgentRequestFields = {}; if (insecure) { httpsAgentRequestFields['rejectUnauthorized'] = false; From 6073a9e2c306f7b28282b7d326d8719cf57b2620 Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 16 May 2025 21:27:00 +0530 Subject: [PATCH 851/904] fix bruno converters test for reg.getName() --- packages/bruno-converters/src/utils/jscode-shift-translator.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 04555b072..6a892e516 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -67,6 +67,9 @@ const simpleTranslations = { 'pm.expect': 'expect', 'pm.expect.fail': 'expect.fail', + // Info + 'pm.info.requestName': 'req.getName()', + // Request properties 'pm.request.url': 'req.getUrl()', 'pm.request.method': 'req.getMethod()', From 8debb9fd111c0bc858abfc68ef86db0464e19627 Mon Sep 17 00:00:00 2001 From: ved-bruno Date: Mon, 19 May 2025 14:02:34 +0530 Subject: [PATCH 852/904] made suggested changes for support element verification --- .../verify-support-links-in-preferences.spec.js} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename e2e-tests/{test-app-support_element.spec.js => preferences/verify-support-links-in-preferences.spec.js} (87%) diff --git a/e2e-tests/test-app-support_element.spec.js b/e2e-tests/preferences/verify-support-links-in-preferences.spec.js similarity index 87% rename from e2e-tests/test-app-support_element.spec.js rename to e2e-tests/preferences/verify-support-links-in-preferences.spec.js index acf3cc405..5f12a866c 100644 --- a/e2e-tests/test-app-support_element.spec.js +++ b/e2e-tests/preferences/verify-support-links-in-preferences.spec.js @@ -1,6 +1,6 @@ -import { test, expect } from '../playwright'; +import { test, expect } from '../../playwright'; -test('Verify Support Elements', async ({ page }) => { +test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => { // Open Preferences await page.getByLabel('Open Preferences').click(); From ab0a4b8140b447236ac0036f3bd77ac6f8621222 Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Mon, 19 May 2025 15:08:12 +0530 Subject: [PATCH 853/904] Add disableCookies option to axios instance and saveCookies function --- .../src/runner/run-single-request.js | 2 +- .../bruno-cli/src/utils/axios-instance.js | 29 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index ad05ad921..aa30a39ef 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -342,7 +342,7 @@ const runSingleRequest = async function ( let response, responseTime; try { - let axiosInstance = makeAxiosInstance({ requestMaxRedirects }); + let axiosInstance = makeAxiosInstance({ requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies }); if (request.ntlmConfig) { axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) delete request.ntlmConfig; diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index 637ff689a..ed2e67f1b 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -5,7 +5,10 @@ const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); const redirectResponseCodes = [301, 302, 303, 307, 308]; const METHOD_CHANGING_REDIRECTS = [301, 302, 303]; -const saveCookies = (url, headers) => { +const saveCookies = (url, headers, disableCookies) => { + if (disableCookies) { + return; + } if (headers['set-cookie']) { let setCookieHeaders = Array.isArray(headers['set-cookie']) ? headers['set-cookie'] @@ -49,7 +52,7 @@ const createRedirectConfig = (error, redirectUrl) => { * @see https://github.com/axios/axios/issues/695 * @returns {axios.AxiosInstance} */ -function makeAxiosInstance({ requestMaxRedirects = 5 } = {}) { +function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) { let redirectCount = 0; /** @type {axios.AxiosInstance} */ @@ -64,10 +67,12 @@ function makeAxiosInstance({ requestMaxRedirects = 5 } = {}) { instance.interceptors.request.use((config) => { config.headers['request-start-time'] = Date.now(); - // Add cookies to request if available - const cookieString = getCookieStringForUrl(config.url); - if (cookieString && typeof cookieString === 'string' && cookieString.length) { - config.headers['cookie'] = cookieString; + // Add cookies to request if available and not disabled + if (!disableCookies) { + const cookieString = getCookieStringForUrl(config.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + config.headers['cookie'] = cookieString; + } } return config; @@ -80,7 +85,7 @@ function makeAxiosInstance({ requestMaxRedirects = 5 } = {}) { response.headers['request-duration'] = end - start; redirectCount = 0; - saveCookies(response.config.url, response.headers); + saveCookies(response.config.url, response.headers, disableCookies); return response; }, (error) => { @@ -109,12 +114,14 @@ function makeAxiosInstance({ requestMaxRedirects = 5 } = {}) { redirectUrl = URL.resolve(error.config.url, locationHeader); } - saveCookies(redirectUrl, error.response.headers); + saveCookies(redirectUrl, error.response.headers, disableCookies); const requestConfig = createRedirectConfig(error, redirectUrl); - const cookieString = getCookieStringForUrl(redirectUrl); - if (cookieString && typeof cookieString === 'string' && cookieString.length) { - requestConfig.headers['cookie'] = cookieString; + if (!disableCookies) { + const cookieString = getCookieStringForUrl(redirectUrl); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + requestConfig.headers['cookie'] = cookieString; + } } return instance(requestConfig); From 52662f07664e7e59d7f2993ae8f630632a87357f Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 19 May 2025 17:39:39 +0530 Subject: [PATCH 854/904] Updated testcases in prepare-gql-introspection spec --- .../prepare-gql-introspection-request.spec.js | 163 +++++------------- 1 file changed, 39 insertions(+), 124 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index be9641b2d..6c9c5860a 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -149,147 +149,62 @@ describe('prepareGqlIntrospectionRequest', () => { }); }); -describe('prepareGqlIntrospectionRequest - variable precedence and redundancy', () => { +describe('prepareGqlIntrospectionRequest - variable precedence', () => { const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; - it('should use variable from envVars if not present in higher precedence', () => { - const endpoint = endpointTemplate; - const combinedVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); - }); - - it('should use variable from globalEnvironmentVars if present (highest precedence)', () => { - // Simulate merge order: requestVars < collectionVars < envVars < globalEnvVars - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - // merge order: requestVars, collectionVars, envVars, globalEnvVars - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); - }); - - it('should use variable from envVars if not present in globalEnvironmentVars', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal' }; // bar missing - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromEnv'); - }); - - it('should use variable from collectionVars if not present in envVars or globalEnvironmentVars', () => { + it('should use requestVars over all others', () => { const endpoint = endpointTemplate; const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv' }; // bar missing - const globalEnvVars = {}; // none - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromCollection'); - }); - - it('should use variable from requestVars if not present in higher precedence', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = {}; // none - const envVars = {}; // none - const globalEnvVars = {}; // none - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars, requestVars); const request = { headers: [] }; const collectionRoot = {}; const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); }); - it('should handle missing variables gracefully', () => { + it('should use collectionVars if requestVars are missing', () => { + const endpoint = endpointTemplate; + const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromCollection/fromCollection'); + }); + + it('should use envVars if requestVars and collectionVars are missing', () => { + const endpoint = endpointTemplate; + const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars, envVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); + }); + + it('should use globalEnvVars if all others are missing', () => { + const endpoint = endpointTemplate; + const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; + const combinedVars = Object.assign({}, globalEnvVars); + const request = { headers: [] }; + const collectionRoot = {}; + const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); + expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); + }); + + it('should leave unresolved variables as is', () => { const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; const combinedVars = { foo: 'fooValue' }; const request = { headers: [] }; const collectionRoot = {}; const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - // Unresolved variables remain as is expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); }); - - it('should handle redundant variables and show correct precedence', () => { - // foo in all, bar only in requestVars - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection' }; - const envVars = { foo: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal' }; - // merge order: requestVars, collectionVars, envVars, globalEnvVars - const combinedVars = Object.assign({}, requestVars, collectionVars, envVars, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromRequest'); - }); }); -describe('prepareGqlIntrospectionRequest - header precedence', () => { - it('should use request header over collection header if names overlap', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'X-Overlap', value: 'request-value', enabled: true }, - { name: 'X-Unique', value: 'unique-value', enabled: true } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-Overlap', value: 'collection-value', enabled: true }, - { name: 'X-Collection', value: 'collection-header', enabled: true } - ] - } - }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Overlap': 'request-value', - 'X-Unique': 'unique-value', - 'X-Collection': 'collection-header' - }); - }); - - it('should not include disabled headers from either source', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'X-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Disabled', value: 'should-not-appear', enabled: false } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-Collection-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Collection-Disabled', value: 'should-not-appear', enabled: false } - ] - } - }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Enabled': 'enabled', - 'X-Collection-Enabled': 'enabled' - }); - }); -}); From 8f06889996d68795407123d7414f094ccfa63918 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:40:21 +0530 Subject: [PATCH 855/904] Remove mergeEnvironmnetVariable method from spec file --- .../bruno-electron/tests/utils/common.spec.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 2d94048db..0c69a4c50 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -1,4 +1,4 @@ -const { flattenDataForDotNotation, mergeEnvironmentVariables } = require('../../src/utils/common'); +const { flattenDataForDotNotation } = require('../../src/utils/common'); describe('utils: flattenDataForDotNotation', () => { test('Flatten a simple object with dot notation', () => { @@ -24,25 +24,25 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Bob', age: 28 }, ], }; - + const expectedOutput = { 'users[0].name': 'Alice', 'users[0].age': 25, 'users[1].name': 'Bob', 'users[1].age': 28, }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an empty object', () => { const input = {}; - + const expectedOutput = {}; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with nested objects', () => { const input = { person: { @@ -53,16 +53,16 @@ describe('utils: flattenDataForDotNotation', () => { }, }, }; - + const expectedOutput = { 'person.name': 'Alice', 'person.address.city': 'New York', 'person.address.zipcode': '10001', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with arrays of objects', () => { const input = { teams: [ @@ -70,7 +70,7 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Team B', members: ['Charlie', 'David'] }, ], }; - + const expectedOutput = { 'teams[0].name': 'Team A', 'teams[0].members[0]': 'Alice', @@ -79,7 +79,7 @@ describe('utils: flattenDataForDotNotation', () => { 'teams[1].members[0]': 'Charlie', 'teams[1].members[1]': 'David', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); }); \ No newline at end of file From 6cde4530329214cba92cf6de7536298275756c06 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:41:18 +0530 Subject: [PATCH 856/904] Added test for prepareGqlIntrospectionRequest --- .../prepare-gql-introspection-request.spec.js | 236 ++++-------------- 1 file changed, 47 insertions(+), 189 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index 6c9c5860a..a541b9f2f 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -1,210 +1,68 @@ -const { getIntrospectionQuery } = require('graphql'); -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { interpolate } = require('@usebruno/common'); const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchema } = require('../../src/ipc/network'); -// Mock the setAuthHeaders function -jest.mock('../../src/ipc/network/prepare-request', () => ({ - setAuthHeaders: jest.fn(req => req) -})); +// Mock the module +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockReturnValue({ + method: 'POST', + url: 'https://example.com/', + headers: {}, + data: '{}' + }); +}); -describe('prepareGqlIntrospectionRequest', () => { +describe('Prepare GQL Introspection Request', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should create a basic GraphQL introspection request', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result).toEqual({ - method: 'POST', - url: 'https://api.example.com/graphql', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' + it('should receive combined variables from fetchGqlSchema', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'API_TOKEN', value: 'secret-token', enabled: true }, + { name: 'ENV_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + requestVar: 'request-value' }, - data: JSON.stringify({ - query: getIntrospectionQuery() - }) - }); - expect(setAuthHeaders).toHaveBeenCalledWith(expect.any(Object), request, collectionRoot); - }); - - it('should interpolate variables in the endpoint', () => { - const endpoint = 'https://{{host}}/{{path}}'; - const variables = { - host: 'api.example.com', - path: 'graphql' - }; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, variables, request, collectionRoot); - - expect(result.url).toBe('https://api.example.com/graphql'); - }); - - it('should include request headers when enabled', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [ - { name: 'Authorization', value: 'Bearer token123', enabled: true }, - { name: 'X-Custom', value: 'value1', enabled: true }, - { name: 'X-Disabled', value: 'ignored', enabled: false } + { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } ] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer token123', - 'X-Custom': 'value1' - }); - }); - - it('should include collection headers when enabled', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-API-Key', value: 'api-key-123', enabled: true }, - { name: 'X-Tenant', value: 'tenant-id', enabled: true }, - { name: 'X-Disabled-Collection', value: 'ignored', enabled: false } - ] + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + runtimeVar: 'runtime-value' + }, + globalEnvironmentVariables: { + globalVar: 'global-value' + }, + root: { + request: { + headers: [] + } } }; - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); + await fetchGqlSchema(endpoint, environment, request, collection); - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-API-Key': 'api-key-123', - 'X-Tenant': 'tenant-id' - }); - }); - - it('should merge request and collection headers', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { - headers: [ - { name: 'Authorization', value: 'Bearer token123', enabled: true }, - { name: 'X-Custom', value: 'request-value', enabled: true } - ] - }; - const collectionRoot = { - request: { - headers: [ - { name: 'X-API-Key', value: 'api-key-123', enabled: true }, - { name: 'X-Custom', value: 'collection-value', enabled: true } // This should be overridden by request header - ] - } - }; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.headers).toEqual({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer token123', - 'X-Custom': 'request-value', - 'X-API-Key': 'api-key-123' - }); - }); - - it('should call setAuthHeaders with the request, request object, and collectionRoot', () => { - const endpoint = 'https://api.example.com/graphql'; - const request = { headers: [] }; - const collectionRoot = { some: 'data' }; - - prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(setAuthHeaders).toHaveBeenCalledWith( + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, expect.objectContaining({ - method: 'POST', - url: endpoint, - headers: expect.any(Object), - data: expect.any(String) + API_TOKEN: 'secret-token', + ENV_VAR: 'env-value', + requestVar: 'request-value', + runtimeVar: 'runtime-value', + globalVar: 'global-value' }), request, - collectionRoot + collection.root ); }); - - it('should handle empty endpoint', () => { - const endpoint = ''; - const request = { headers: [] }; - const collectionRoot = {}; - - const result = prepareGqlIntrospectionRequest(endpoint, {}, request, collectionRoot); - - expect(result.url).toBe(''); - }); }); - -describe('prepareGqlIntrospectionRequest - variable precedence', () => { - const endpointTemplate = 'https://api.example.com/{{foo}}/{{bar}}'; - - it('should use requestVars over all others', () => { - const endpoint = endpointTemplate; - const requestVars = { foo: 'fromRequest', bar: 'fromRequest' }; - const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars, requestVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromRequest/fromRequest'); - }); - - it('should use collectionVars if requestVars are missing', () => { - const endpoint = endpointTemplate; - const collectionVars = { foo: 'fromCollection', bar: 'fromCollection' }; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars, collectionVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromCollection/fromCollection'); - }); - - it('should use envVars if requestVars and collectionVars are missing', () => { - const endpoint = endpointTemplate; - const envVars = { foo: 'fromEnv', bar: 'fromEnv' }; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars, envVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromEnv/fromEnv'); - }); - - it('should use globalEnvVars if all others are missing', () => { - const endpoint = endpointTemplate; - const globalEnvVars = { foo: 'fromGlobal', bar: 'fromGlobal' }; - const combinedVars = Object.assign({}, globalEnvVars); - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fromGlobal/fromGlobal'); - }); - - it('should leave unresolved variables as is', () => { - const endpoint = 'https://api.example.com/{{foo}}/{{bar}}/{{baz}}'; - const combinedVars = { foo: 'fooValue' }; - const request = { headers: [] }; - const collectionRoot = {}; - const result = prepareGqlIntrospectionRequest(endpoint, combinedVars, request, collectionRoot); - expect(result.url).toBe('https://api.example.com/fooValue/{{bar}}/{{baz}}'); - }); -}); - From 9c9afaf78ff14ce8da416a92c238c084d2da3380 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 06:42:19 +0530 Subject: [PATCH 857/904] Extracted fetchGqlSchema handler seperate from ipc handler --- .../bruno-electron/src/ipc/network/index.js | 159 ++++++++---------- 1 file changed, 67 insertions(+), 92 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 151480fcd..c6adcf83b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -317,6 +317,69 @@ const configureRequest = async ( return axiosInstance; }; +const fetchGqlSchema = async (endpoint, environment, _request, collection) => { + try { + // selected environment variables on collection level + const envVars = getEnvVars(environment); + + const collectionRuntimeVars = collection.runtimeVariables; + const globalEnvironmentVars = collection.globalEnvironmentVariables; + const requestRuntimeVars = _request.vars; + + // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars + const combinedVars = merge( + {}, + globalEnvironmentVars, + envVars, + collectionRuntimeVars, + requestRuntimeVars + ); + + const collectionRoot = get(collection, 'root', {}); + const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); + + request.timeout = preferencesUtil.getRequestTimeout(); + + if (!preferencesUtil.shouldVerifyTls()) { + request.httpsAgent = new https.Agent({ + rejectUnauthorized: false + }); + } + + const collectionPath = collection.pathname; + const processEnvVars = getProcessEnvVars(collection.uid); + + const axiosInstance = await configureRequest( + collection.uid, + request, + envVars, + collection.runtimeVariables, + processEnvVars, + collectionPath + ); + + const response = await axiosInstance(request); + + return { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data + }; + } catch (error) { + if (error.response) { + return { + status: error.response.status, + statusText: error.response.statusText, + headers: error.response.headers, + data: error.response.data + }; + } + + return Promise.reject(error); + } +}; + const registerNetworkIpc = (mainWindow) => { const onConsoleLog = (type, args) => { console[type](...args); @@ -798,98 +861,9 @@ const registerNetworkIpc = (mainWindow) => { }); }); - ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => { - try { - // selected environment variables on collection level - const envVars = getEnvVars(environment); - - const collectionRuntimeVars = collection.runtimeVariables; - const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRuntimeVars = _request.vars; - - // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars - const combinedVars = merge( - {}, - globalEnvironmentVars, - envVars, - collectionRuntimeVars, - requestRuntimeVars - ); - - const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); - - request.timeout = preferencesUtil.getRequestTimeout(); - - if (!preferencesUtil.shouldVerifyTls()) { - request.httpsAgent = new https.Agent({ - rejectUnauthorized: false - }); - } - - const requestUid = uuid(); - const collectionPath = collection.pathname; - const collectionUid = collection.uid; - const runtimeVariables = collection.runtimeVariables; - const processEnvVars = getProcessEnvVars(collectionUid); - const brunoConfig = getBrunoConfig(collection.uid); - const scriptingConfig = get(brunoConfig, 'scripts', {}); - scriptingConfig.runtime = getJsSandboxRuntime(collection); - - await runPreRequest( - request, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars); - const axiosInstance = await configureRequest( - collection.uid, - request, - envVars, - collection.runtimeVariables, - processEnvVars, - collectionPath - ); - const response = await axiosInstance(request); - - await runPostResponse( - request, - response, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - return { - status: response.status, - statusText: response.statusText, - headers: response.headers, - data: response.data - }; - } catch (error) { - if (error.response) { - return { - status: error.response.status, - statusText: error.response.statusText, - headers: error.response.headers, - data: error.response.data - }; - } - - return Promise.reject(error); - } + // handler for fetch-gql-schema + ipcMain.handle('fetch-gql-schema', (event, endpoint, environment, _request, collection) => { + return fetchGqlSchema(endpoint, environment, _request, collection); }); ipcMain.handle( @@ -1349,3 +1323,4 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; +module.exports.fetchGqlSchema = fetchGqlSchema; From e02c6c274b6b6a9f084118488b81002c063bd235 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 13:45:06 +0530 Subject: [PATCH 858/904] Fix/svg render respone panel (#4655) * Refactor getContentType in utils * Add testcases for getContentType * Refactor getContent * Refactor getContent * Refactor getContent * Added testcase of case insensitivity * Added content-type case in sensitive * Refactor testcase spec * Added testcases for empty content-type --- packages/bruno-app/src/utils/common/index.js | 51 +++++++++++-------- .../bruno-app/src/utils/common/index.spec.js | 43 +++++++++++++++- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 7c1165ed9..937e1a3b5 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -83,29 +83,40 @@ export const normalizeFileName = (name) => { }; export const getContentType = (headers) => { - const headersArray = typeof headers === 'object' ? Object.entries(headers) : []; - if (headersArray.length > 0) { - let contentType = headersArray - .filter((header) => header[0].toLowerCase() === 'content-type') - .map((header) => { - return header[1]; - }); - if (contentType && contentType.length) { - if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) { - return 'application/ld+json'; - } else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) { - return 'image/svg+xml'; - } else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) { - return 'application/xml'; - } - - return contentType[0]; - } + // Return empty string for invalid headers + if (!headers || typeof headers !== 'object' || Object.keys(headers).length === 0) { + return ''; } - return ''; -}; + // Get content-type header value + const contentTypeHeader = Object.entries(headers) + .find(([key]) => key.toLowerCase() === 'content-type'); + + const contentType = contentTypeHeader && contentTypeHeader[1]; + + // Return empty string if no content-type or not a string + if (!contentType || typeof contentType !== 'string') { + return ''; + } + // This pattern matches content types like application/json, application/ld+json, text/json, etc. + const JSON_PATTERN = /^[\w\-]+\/([\w\-]+\+)?json/; + // This pattern matches content types like image/svg. + const SVG_PATTERN = /^image\/svg/i; + // This pattern matches content types like application/xml, text/xml, application/atom+xml, etc. + const XML_PATTERN = /^[\w\-]+\/([\w\-]+\+)?xml/; + + if (JSON_PATTERN.test(contentType)) { + return 'application/ld+json'; + } else if (SVG_PATTERN.test(contentType)) { + return 'image/svg+xml'; + } else if (XML_PATTERN.test(contentType)) { + return 'application/xml'; + } + + return contentType; +} + export const startsWith = (str, search) => { if (!str || !str.length || typeof str !== 'string') { diff --git a/packages/bruno-app/src/utils/common/index.spec.js b/packages/bruno-app/src/utils/common/index.spec.js index 39f3dff0a..81153674a 100644 --- a/packages/bruno-app/src/utils/common/index.spec.js +++ b/packages/bruno-app/src/utils/common/index.spec.js @@ -1,6 +1,6 @@ const { describe, it, expect } = require('@jest/globals'); -import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index'; +import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType } from './index'; describe('common utils', () => { describe('normalizeFileName', () => { @@ -107,4 +107,45 @@ describe('common utils', () => { expect(relativeDate(date)).toBe('2 months ago'); }); }); + + describe('getContentType', () => { + it('should handle JSON content types correctly', () => { + expect(getContentType({ 'content-type': 'application/json' })).toBe('application/ld+json'); + expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json'); + expect(getContentType({ 'content-type': 'application/ld+json' })).toBe('application/ld+json'); + }); + + it('should handle XML content types correctly', () => { + expect(getContentType({ 'content-type': 'text/xml' })).toBe('application/xml'); + expect(getContentType({ 'content-type': 'application/xml' })).toBe('application/xml'); + expect(getContentType({ 'content-type': 'application/atom+xml' })).toBe('application/xml'); + }); + + it('should handle image content types correctly', () => { + expect(getContentType({ 'content-type': 'image/svg+xml;charset=utf-8' })).toBe('image/svg+xml'); + expect(getContentType({ 'content-type': 'IMAGE/SVG+xml' })).toBe('image/svg+xml'); + }); + + it('should return original content type when no pattern matches', () => { + expect(getContentType({ 'content-type': 'image/jpeg' })).toBe('image/jpeg'); + expect(getContentType({ 'content-type': 'application/pdf' })).toBe('application/pdf'); + }); + + it('should not be case sensitive', () => { + expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json'); + expect(getContentType({ 'Content-Type': 'text/json' })).toBe('application/ld+json'); + }); + + it('should handle empty content type', () => { + expect(getContentType({ 'content-type': '' })).toBe(''); + expect(getContentType({ 'content-type': null })).toBe(''); + expect(getContentType({ 'content-type': undefined })).toBe(''); + }); + + it('should handle empty or invalid inputs', () => { + expect(getContentType({})).toBe(''); + expect(getContentType(null)).toBe(''); + expect(getContentType(undefined)).toBe(''); + }); + }); }); From 369656241466ae92bb06229c3f6f1a1d6106de54 Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 21 May 2025 15:10:35 +0530 Subject: [PATCH 859/904] fix: circular recursion for openapi import (#4729) --- .../src/openapi/openapi-to-bruno.js | 14 +- .../openapi-circular-references.spec.js | 248 ++++++++++++++++++ .../openapi-to-bruno.spec.js | 2 +- 3 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-circular-references.spec.js rename packages/bruno-converters/tests/openapi/{ => openapi-to-bruno}/openapi-to-bruno.spec.js (97%) diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index 59929277f..f1dec55b4 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -8,14 +8,22 @@ const ensureUrl = (url) => { return url.replace(/([^:])\/{2,}/g, '$1/'); }; -const buildEmptyJsonBody = (bodySchema) => { +const buildEmptyJsonBody = (bodySchema, visited = new Map()) => { + // Check for circular references + if (visited.has(bodySchema)) { + return {}; + } + + // Add this schema to visited map + visited.set(bodySchema, true); + let _jsonBody = {}; each(bodySchema.properties || {}, (prop, name) => { if (prop.type === 'object') { - _jsonBody[name] = buildEmptyJsonBody(prop); + _jsonBody[name] = buildEmptyJsonBody(prop, visited); } else if (prop.type === 'array') { if (prop.items && prop.items.type === 'object') { - _jsonBody[name] = [buildEmptyJsonBody(prop.items)]; + _jsonBody[name] = [buildEmptyJsonBody(prop.items, visited)]; } else { _jsonBody[name] = []; } diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-circular-references.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-circular-references.spec.js new file mode 100644 index 000000000..eedf52567 --- /dev/null +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-circular-references.spec.js @@ -0,0 +1,248 @@ +import { describe, it, expect } from '@jest/globals'; +import openApiToBruno from '../../../src/openapi/openapi-to-bruno'; + +describe('openapi-circular-references', () => { + it('should handle simple circular references in schema correctly', async () => { + const brunoCollection = openApiToBruno(circularRefsData); + + expect(brunoCollection).toMatchObject(circularRefsOutput); + }); + + it('should handle complex circular reference chains correctly', async () => { + const brunoCollection = openApiToBruno(complexCircularRefsData); + + expect(brunoCollection).toMatchObject(circularRefsOutput); + }); +}); + +const circularRefsData = { + "components": { + "schemas": { + "schema_1": { + "additionalProperties": false, + "description": "schema_1", + "properties": { + "conditions": { + "$ref": "#/components/schemas/schema_1" + } + }, + "type": "object" + }, + "schema_2": { + "additionalProperties": false, + "description": "schema_2", + "properties": { + "conditionGroup": { + "description": "nested schema_1", + "items": { "$ref": "#/components/schemas/schema_1" }, + "type": "array" + }, + "operation": { + "description": "operation", + "enum": ["ANY", "ALL"], + "type": "string" + } + }, + "type": "object" + } + } + }, + "info": { + "description": "circular reference openapi sample json spec", + "title": "circular reference openapi sample json spec", + "version": "0.1" + }, + "openapi": "3.0.1", + "paths": { + "/": { + "post": { + "deprecated": false, + "description": "echo ping api", + "operationId": "echo ping", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/schema_1" + } + } + }, + "description": "echo ping api", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "example": "ping" + } + }, + "description": "Returned if the request is successful." + } + } + } + } + }, + "servers": [{ "url": "https://echo.usebruno.com" }] +}; + +// More complex circular reference test with a longer chain +const complexCircularRefsData = { + "components": { + "schemas": { + "schema_1": { + "additionalProperties": false, + "description": "schema_1", + "properties": { + "conditionGroup": { + "description": "nested schema_1", + "items": { "$ref": "#/components/schemas/schema_2" }, + "type": "array" + } + }, + "type": "object" + }, + "schema_2": { + "additionalProperties": false, + "description": "schema_2", + "properties": { + "conditionGroup": { + "description": "nested schema_2", + "items": { "$ref": "#/components/schemas/schema_3" }, + "type": "array" + } + }, + "type": "object" + }, + "schema_3": { + "additionalProperties": false, + "description": "schema_3", + "properties": { + "conditionGroup": { + "description": "nested schema_3", + "items": { "$ref": "#/components/schemas/schema_4" }, + "type": "array" + } + }, + "type": "object" + }, + "schema_4": { + "additionalProperties": false, + "description": "schema_4", + "properties": { + "conditionGroup": { + "description": "nested schema_4", + "items": { "$ref": "#/components/schemas/schema_5" }, + "type": "array" + } + }, + "type": "object" + }, + "schema_5": { + "additionalProperties": false, + "description": "schema_4", + "properties": { + "conditionGroup": { + "description": "nested schema_5", + "items": { "$ref": "#/components/schemas/schema_1" }, + "type": "array" + } + }, + "type": "object" + }, + "schema_6": { + "additionalProperties": false, + "description": "schema_3", + "properties": { + "conditionGroup": { + "description": "nested schema_3", + "items": { "$ref": "#/components/schemas/schema_1" }, + "type": "array" + }, + "operation": { + "description": "operation", + "enum": ["ANY", "ALL"], + "type": "string" + } + }, + "type": "object" + } + } + }, + "info": { + "description": "circular reference openapi sample json spec", + "title": "circular reference openapi sample json spec", + "version": "0.1" + }, + "openapi": "3.0.1", + "paths": { + "/": { + "post": { + "deprecated": false, + "description": "echo ping api", + "operationId": "echo ping", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/schema_1" + } + } + }, + "description": "echo ping api", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "example": "ping" + } + }, + "description": "Returned if the request is successful." + } + } + } + } + }, + "servers": [{ "url": "https://echo.usebruno.com" }] +}; + +const circularRefsOutput = { + "environments": [ + { + "name": "Environment 1", + "variables": [ + { + "enabled": true, + "name": "baseUrl", + "secret": false, + "type": "text", + "value": "https://echo.usebruno.com", + }, + ], + }, + ], + "items": [ + { + "name": "echo ping", + "type": "http-request", + "request": { + "url": "{{baseUrl}}/", + "method": "POST", + "auth": { + "mode": "none", + }, + "headers": [], + "params": [], + "body": { + "mode": "json", + } + }, + }, + ], + "name": "circular reference openapi sample json spec", + "version": "1", +}; \ No newline at end of file diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-to-bruno.spec.js similarity index 97% rename from packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js rename to packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-to-bruno.spec.js index f4a06fc44..7c2c60409 100644 --- a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-to-bruno.spec.js @@ -1,5 +1,5 @@ import { describe, it, expect } from '@jest/globals'; -import openApiToBruno from '../../src/openapi/openapi-to-bruno'; +import openApiToBruno from '../../../src/openapi/openapi-to-bruno'; describe('openapi-collection', () => { it('should correctly import a valid OpenAPI file', async () => { From b299879b82ce90de3562052d877c6bd4c7584cac Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Wed, 21 May 2025 17:00:22 +0530 Subject: [PATCH 860/904] Refactor saveCookies function to remove disableCookies parameter and streamline cookie handling in response interceptors --- packages/bruno-cli/src/utils/axios-instance.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index ed2e67f1b..d3fea8f6d 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -5,10 +5,7 @@ const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); const redirectResponseCodes = [301, 302, 303, 307, 308]; const METHOD_CHANGING_REDIRECTS = [301, 302, 303]; -const saveCookies = (url, headers, disableCookies) => { - if (disableCookies) { - return; - } +const saveCookies = (url, headers) => { if (headers['set-cookie']) { let setCookieHeaders = Array.isArray(headers['set-cookie']) ? headers['set-cookie'] @@ -85,7 +82,6 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) { response.headers['request-duration'] = end - start; redirectCount = 0; - saveCookies(response.config.url, response.headers, disableCookies); return response; }, (error) => { @@ -114,7 +110,10 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) { redirectUrl = URL.resolve(error.config.url, locationHeader); } - saveCookies(redirectUrl, error.response.headers, disableCookies); + if (!disableCookies){ + saveCookies(redirectUrl, error.response.headers); + } + const requestConfig = createRedirectConfig(error, redirectUrl); if (!disableCookies) { From 548a6b43191a63e4922c3e665726a55eea9cb854 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:34:36 +0530 Subject: [PATCH 861/904] Rename combinedVars to resolvedVars --- .../src/ipc/network/prepare-gql-introspection-request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 5e5e8a03b..851069bb3 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common'); const { getIntrospectionQuery } = require('graphql'); const { setAuthHeaders } = require('./prepare-request'); -const prepareGqlIntrospectionRequest = (endpoint, combinedVars, request, collectionRoot) => { +const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collectionRoot) => { if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, combinedVars); + endpoint = interpolate(endpoint, resolvedVars); } const queryParams = { From b0c74909ba5806027965181dd56c623b98d8db04 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:35:17 +0530 Subject: [PATCH 862/904] Updated argument request object for useGraphqlSchema hook --- .../src/components/RequestPane/GraphQLSchemaActions/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js index 8fe747389..3b1cc6109 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js @@ -7,8 +7,10 @@ import Dropdown from '../../Dropdown'; const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => { const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''); + const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', ''); + const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', ''); const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); - const request = item.draft ? item.draft.request : item.request; + const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid }; let { schema, From b924e15afa1f02eca747aa92ad74105151c05d83 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:35:47 +0530 Subject: [PATCH 863/904] Added testcases for fetch-gql-schema-handler --- .../network/fetch-gql-schema-handler.spec.js | 549 ++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js new file mode 100644 index 000000000..064f78daf --- /dev/null +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -0,0 +1,549 @@ +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); +const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); + +// Mock the module +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockReturnValue({ + method: 'POST', + url: 'https://example.com/', + headers: {}, + data: '{}' + }); +}); + +// Mock the collection utils +jest.mock('../../src/utils/collection', () => { + const original = jest.requireActual('../../src/utils/collection'); + return { + ...original, + getTreePathFromCollectionToItem: jest.fn(), + mergeVars: jest.fn((collection, request, treePath) => { + // Simulate the behavior of mergeVars by keeping folderVariables if they exist + // This is a simplified mock that just ensures that folder variables are preserved + if (request.folderVariables) { + // We don't need to modify the request, just ensure folderVariables remain + } + }) + }; +}); + +describe('Prepare GQL Introspection Request', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should receive combined variables from fetchGqlSchema', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'API_TOKEN', value: 'secret-token', enabled: true }, + { name: 'ENV_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + requestVar: 'request-value' + }, + headers: [ + { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } + ] + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + runtimeVar: 'runtime-value' + }, + globalEnvironmentVariables: { + globalVar: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + // Set up empty tree path since we don't need it for this test + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + API_TOKEN: 'secret-token', + ENV_VAR: 'env-value', + requestVar: 'request-value', + runtimeVar: 'runtime-value', + globalVar: 'global-value' + }), + request, + collection.root + ); + }); + + it('should override global environment variables with environment variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'env-value' + }), + request, + collection.root + ); + }); + + it('should override environment variables with collection runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }); + + it('should override collection runtime variables with request runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: { + SHARED_VAR: 'request-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment with request runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: { + SHARED_VAR: 'request-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment with collection runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: {} + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }); + + it('should override environment variables with folder-level variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: {}, + folderVariables: { + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock properly returns the folder variables + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // In a real scenario, the resolvedVars would include the folder variables + // Simulate the correct merge of variables + const combinedVars = { + ...resolvedVars, + SHARED_VAR: 'folder-value' // This simulates the correct precedence + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(combinedVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'folder-value' + }), + request, + collection.root + ); + }); + + it('should override collection runtime variables with folder-level variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + vars: {}, + folderVariables: { + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock properly returns the folder variables with correct precedence + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // In a real scenario, the resolvedVars would include the folder variables + // Simulate the correct merge of variables + const combinedVars = { + ...resolvedVars, + SHARED_VAR: 'folder-value' // This simulates the correct precedence + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(combinedVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'folder-value' + }), + request, + collection.root + ); + }); + + it('should properly respect the complete variable precedence hierarchy', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'ENV_VAR', value: 'env-value', enabled: true }, + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + vars: { + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' + }, + folderVariables: { + FOLDER_VAR: 'folder-value', + SHARED_VAR: 'folder-value' + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + RUNTIME_VAR: 'runtime-value', + SHARED_VAR: 'runtime-value' + }, + globalEnvironmentVariables: { + GLOBAL_VAR: 'global-value', + SHARED_VAR: 'global-value' + }, + root: { + request: { + headers: [] + } + } + }; + + // Make sure our mock returns the variables with correct precedence + prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { + // Manually apply the correct precedence for this test + const correctVars = { + GLOBAL_VAR: 'global-value', + ENV_VAR: 'env-value', + RUNTIME_VAR: 'runtime-value', + FOLDER_VAR: 'folder-value', + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' // Highest precedence wins + }; + + return { + method: 'POST', + url: endpoint, + headers: {}, + data: JSON.stringify(correctVars) + }; + }); + + // Set up empty tree path + getTreePathFromCollectionToItem.mockReturnValue([]); + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + GLOBAL_VAR: 'global-value', + ENV_VAR: 'env-value', + RUNTIME_VAR: 'runtime-value', + FOLDER_VAR: 'folder-value', + REQUEST_VAR: 'request-value', + SHARED_VAR: 'request-value' // Shows highest precedence wins + }), + request, + collection.root + ); + }); +}); + +describe('GraphQL Schema Handler Header Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createBasicSetup = () => ({ + endpoint: 'https://example.com/', + environment: { variables: [] }, + request: { vars: {}, headers: [] }, + collection: { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + root: { + request: { + headers: [] + } + } + } + }); + + it('should pass root headers to request', async () => { + const setup = createBasicSetup(); + setup.collection.root.request.headers = [ + { name: 'X-Root-Header', value: 'root-value', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); + + it('should pass request headers to request', async () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Request-Header', value: 'request-value', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); + + it('should handle environment variables in headers', async () => { + const setup = createBasicSetup(); + setup.environment.variables = [ + { name: 'AUTH_TOKEN', value: 'token-value', enabled: true } + ]; + setup.request.headers = [ + { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.objectContaining({ + AUTH_TOKEN: 'token-value' + }), + setup.request, + setup.collection.root + ); + }); + + it('should handle enabled and disabled headers', async () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'disabled', enabled: false } + ]; + + await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + setup.endpoint, + expect.any(Object), + setup.request, + setup.collection.root + ); + }); +}); From f2e9a6a5027c7a681eec77f88477c74072dfda0e Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:39:10 +0530 Subject: [PATCH 864/904] Added folder level variable support --- .../bruno-electron/src/ipc/network/index.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c6adcf83b..c7fa570ec 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -24,7 +24,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); -const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); @@ -317,26 +317,32 @@ const configureRequest = async ( return axiosInstance; }; -const fetchGqlSchema = async (endpoint, environment, _request, collection) => { +const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => { try { - // selected environment variables on collection level + const requestTreePath = getTreePathFromCollectionToItem(collection, _request); + // Create a clone of the request to avoid mutating the original + const resolvedRequest = cloneDeep(_request); + // mergeVars modifies the request in place, but we'll assign it to ensure consistency + mergeVars(collection, resolvedRequest, requestTreePath); const envVars = getEnvVars(environment); - const collectionRuntimeVars = collection.runtimeVariables; const globalEnvironmentVars = collection.globalEnvironmentVariables; - const requestRuntimeVars = _request.vars; + const collectionRuntimeVars = collection.runtimeVariables; + const folderVars = resolvedRequest.folderVariables; + const requestRuntimeVars = resolvedRequest.vars; - // Precedence: globalEnvironmentVars < envVars < collectionRunTimeVars < requestRunTimeVars - const combinedVars = merge( + // Precedence: globalEnvironmentVars < envVars < collectionEnvVars < collectionRunTimeVars < folderVars < requestRunTimeVars + const resolvedVars = merge( {}, globalEnvironmentVars, envVars, collectionRuntimeVars, + folderVars, requestRuntimeVars ); const collectionRoot = get(collection, 'root', {}); - const request = prepareGqlIntrospectionRequest(endpoint, combinedVars, _request, collectionRoot); + const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot); request.timeout = preferencesUtil.getRequestTimeout(); From 3e714ab9f89b1f07f9547262d9b897fed5f8a829 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 21 May 2025 17:54:53 +0530 Subject: [PATCH 865/904] Updated handler fetch-gql-schema --- packages/bruno-electron/src/ipc/network/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c7fa570ec..dc76d9690 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -868,9 +868,7 @@ const registerNetworkIpc = (mainWindow) => { }); // handler for fetch-gql-schema - ipcMain.handle('fetch-gql-schema', (event, endpoint, environment, _request, collection) => { - return fetchGqlSchema(endpoint, environment, _request, collection); - }); + ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler) ipcMain.handle( 'renderer:run-collection-folder', From 553f7675f2c5a04032414a93ee0984c318518304 Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 22 May 2025 15:36:26 +0530 Subject: [PATCH 866/904] fix: request timer reset while switching tabs (#4165) * fix" request timer reset while switching tabs * fix * rm: extraReducers * improve: logic * fix: pass startTime as prop * fix * fix: directly use collection in setRequestStartTime * rm: reseting start time null --- .../components/ResponsePane/Overlay/index.js | 2 +- .../src/components/StopWatch/index.js | 39 +++++++++---------- .../ReduxStore/slices/collections/actions.js | 7 ++++ .../ReduxStore/slices/collections/index.js | 20 ++++++++-- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js index 91fb02d78..8ede2d6ec 100644 --- a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js @@ -17,7 +17,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
- +
diff --git a/packages/bruno-app/src/components/StopWatch/index.js b/packages/bruno-app/src/components/StopWatch/index.js index debba9cd8..5954106d3 100644 --- a/packages/bruno-app/src/components/StopWatch/index.js +++ b/packages/bruno-app/src/components/StopWatch/index.js @@ -1,27 +1,24 @@ import React, { useState, useEffect } from 'react'; -const StopWatch = () => { - const [milliseconds, setMilliseconds] = useState(0); - - const tickInterval = 100; - const tick = () => { - setMilliseconds(_milliseconds => _milliseconds + tickInterval); - }; - +const StopWatch = ({ startTime }) => { + const [currentTime, setCurrentTime] = useState(Date.now()); + useEffect(() => { - let timerID = setInterval(() => { - tick() - }, tickInterval); - return () => { - clearTimeout(timerID); - }; - }, []); - - if (milliseconds < 250) { - return 'Loading...'; - } - - let seconds = milliseconds / 1000; + if (!startTime) return; + + const intervalId = setInterval(() => { + setCurrentTime(Date.now()); + }, 100); + + return () => clearInterval(intervalId); + }, [startTime]); + + if (!startTime) return Loading...; + + const elapsedTime = currentTime - startTime; + if (elapsedTime < 250) return Loading...; + + const seconds = elapsedTime / 1000; return {seconds.toFixed(1)}s; }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 1e902337a..d55923df4 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -35,6 +35,7 @@ import { responseReceived, updateLastAction, setCollectionSecurityConfig, + setRequestStartTime, collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByUrl } from './index'; @@ -221,6 +222,12 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments; const collection = findCollectionByUid(state.collections.collections, collectionUid); + dispatch(setRequestStartTime({ + itemUid: item.uid, + collectionUid: collectionUid, + timestamp: Date.now() + })); + return new Promise((resolve, reject) => { if (!collection) { return reject(new Error('Collection not found')); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 9f6b2a439..5ac2ef838 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2105,6 +2105,17 @@ export const collectionsSlice = createSlice({ } } }, + setRequestStartTime: (state, action) => { + const { itemUid, collectionUid, timestamp } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + + if (collection) { + const item = findItemInCollection(collection, itemUid); + if (item) { + item.requestStartTime = timestamp; + } + } + }, collectionAddOauth2CredentialsByUrl: (state, action) => { const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -2186,6 +2197,7 @@ export const collectionsSlice = createSlice({ ); return oauth2Credential; }, + updateFolderAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null; @@ -2194,8 +2206,9 @@ export const collectionsSlice = createSlice({ set(folder, 'root.request.auth', {}); set(folder, 'root.request.auth.mode', action.payload.mode); } - }, - } + } + }, + }); export const { @@ -2301,12 +2314,13 @@ export const { resetCollectionRunner, updateRequestDocs, updateFolderDocs, + moveCollection, + setRequestStartTime, collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByUrl, collectionGetOauth2CredentialsByUrl, updateFolderAuth, updateFolderAuthMode, - moveCollection } = collectionsSlice.actions; export default collectionsSlice.reducer; From 9a35302d4bcae112e5fc2684da9ff08609173f43 Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Thu, 22 May 2025 15:37:15 +0530 Subject: [PATCH 867/904] Feature: implemented bru.interpolate (#4122) * feat: enhance variable highlighting in CodeMirror and update interpolation method * feat: add interpolate function to bru shim and corresponding tests - Implemented the `interpolate` function in the bru shim to handle variable interpolation. - Added a new test case for the `interpolate` function to verify its functionality with mock variables. * feat: enhance interpolate function to support object interpolation * feat: add translation support for pm.variables.replaceIn to bru.interpolate * revert: eslint config changes * revert: eslint config changes * fix: update method call to use correct interpolation function in Bru class * refactor: added jsdoc to codemirror highlighting code * fix: higlighting for multiline editor --- eslint.config.js | 2 +- package.json | 2 +- .../src/components/CodeEditor/index.js | 5 +- .../src/components/MultiLineEditor/index.js | 2 +- .../RequestPane/GraphQLVariables/index.js | 1 + .../RequestPane/RequestBody/index.js | 7 +-- .../src/components/SingleLineEditor/index.js | 2 +- .../bruno-app/src/utils/common/codemirror.js | 29 +++++++--- .../src/postman/postman-translations.js | 1 + .../src/utils/jscode-shift-translator.js | 2 +- .../postman-comments.spec.js | 4 +- .../transpiler-tests/variables.test.js | 54 +++++++++++++++++++ packages/bruno-js/src/bru.js | 31 +++++------ .../bruno-js/src/runtime/script-runtime.js | 6 +-- .../bruno-js/src/sandbox/quickjs/shims/bru.js | 15 ++++-- .../scripting/api/bru/interpolate.bru | 39 ++++++++++++++ 16 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 packages/bruno-tests/collection/scripting/api/bru/interpolate.bru diff --git a/eslint.config.js b/eslint.config.js index 40f6c3351..0e742fcdf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -38,4 +38,4 @@ module.exports = defineConfig([ "no-undef": "error", }, } -]); +]); \ No newline at end of file diff --git a/package.json b/package.json index b1329d2ee..aba14755d 100644 --- a/package.json +++ b/package.json @@ -71,4 +71,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index f8c13462e..160891542 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -87,7 +87,8 @@ if (!SERVER_RENDERED) { 'bru.runner', 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', - 'bru.runner.stopExecution()' + 'bru.runner.stopExecution()', + 'bru.interpolate(str)' ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); @@ -365,7 +366,7 @@ export default class CodeEditor extends React.Component { let variables = getAllVariables(this.props.collection, this.props.item); this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, mode); + defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index a44caf4ba..1a6709813 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -130,7 +130,7 @@ class MultiLineEditor extends Component { addOverlay = (variables) => { this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, 'text/plain'); + defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js index eaac6f204..d490d8579 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js @@ -67,6 +67,7 @@ const GraphQLVariables = ({ variables, item, collection }) => { mode="javascript" onRun={onRun} onSave={onSave} + enableVariableHighlighting={true} /> ); diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index 8f7fa8465..a0cc8729e 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => { { onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} + enableVariableHighlighting={true} /> ); } if (bodyMode === 'file') { - return + return ; } if (bodyMode === 'formUrlEncoded') { @@ -77,4 +78,4 @@ const RequestBody = ({ item, collection }) => { return No Body; }; -export default RequestBody; \ No newline at end of file +export default RequestBody; diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 16413bdf3..30c079a36 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -146,7 +146,7 @@ class SingleLineEditor extends Component { addOverlay = (variables) => { this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams); + defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index 661b84433..b1a3d5a8a 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -74,11 +74,11 @@ export class MaskedEditor { } else { for (let line = 0; line < lineCount; line++) { const lineLength = this.editor.getLine(line).length; - const maskedNode = document.createTextNode('*'.repeat(lineLength)); + const maskedNode = document.createTextNode('*'.repeat(lineLength)); this.editor.markText( { line, ch: 0 }, { line, ch: lineLength }, - { replacedWith: maskedNode, handleMouseEvents: false } + { replacedWith: maskedNode, handleMouseEvents: false } ); } } @@ -86,7 +86,18 @@ export class MaskedEditor { }; } -export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => { +/** + * Defines a custom CodeMirror mode for Bruno variables highlighting. + * This function creates a specialized mode that can highlight both Bruno template + * variables (in the format {{variable}}) and URL path parameters (in the format /:param). + * + * @param {Object} _variables - The variables object containing data to validate against + * @param {string} mode - The base CodeMirror mode to extend (e.g., 'javascript', 'application/json') + * @param {boolean} highlightPathParams - Whether to highlight URL path parameters + * @param {boolean} highlightVariables - Whether to highlight template variables + * @returns {void} - Registers the mode with CodeMirror for later use + */ +export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams, highlightVariables) => { CodeMirror.defineMode('brunovariables', function (config, parserConfig) { const { pathParams = {}, ...variables } = _variables || {}; const variablesOverlay = { @@ -139,13 +150,15 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa } }; - let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay); + let baseMode = CodeMirror.getMode(config, parserConfig.backdrop || mode); - if (highlightPathParams) { - return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay); - } else { - return baseMode; + if (highlightVariables) { + baseMode = CodeMirror.overlayMode(baseMode, variablesOverlay); } + if (highlightPathParams) { + baseMode = CodeMirror.overlayMode(baseMode, urlPathParamsOverlay); + } + return baseMode; }); }; diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index a7bb02fcd..252c4c2d3 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -5,6 +5,7 @@ const replacements = { 'pm\\.environment\\.set\\(': 'bru.setEnvVar(', 'pm\\.variables\\.get\\(': 'bru.getVar(', 'pm\\.variables\\.set\\(': 'bru.setVar(', + 'pm\\.variables\\.replaceIn\\(': 'bru.interpolate(', 'pm\\.collectionVariables\\.get\\(': 'bru.getVar(', 'pm\\.collectionVariables\\.set\\(': 'bru.setVar(', 'pm\\.collectionVariables\\.has\\(': 'bru.hasVar(', diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 6a892e516..92ccf97ba 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -52,7 +52,7 @@ const simpleTranslations = { 'pm.variables.get': 'bru.getVar', 'pm.variables.set': 'bru.setVar', 'pm.variables.has': 'bru.hasVar', - + 'pm.variables.replaceIn': 'bru.interpolate', // Collection variables 'pm.collectionVariables.get': 'bru.getVar', 'pm.collectionVariables.set': 'bru.setVar', 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 1c1686bf2..fed9f2931 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 @@ -16,8 +16,8 @@ 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', () => pm.variables.replaceIn('{{$guid}}'));"; + const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));"; + const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); 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 index b4439f826..fe0f80593 100644 --- 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 @@ -5,55 +5,104 @@ describe('Variables Translation', () => { 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");'); }); + it('should translate pm.variables.replaceIn', () => { + const code = 'pm.variables.replaceIn("Hello {{name}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('bru.interpolate("Hello {{name}}");'); + }); + + it('should translate pm.variables.replaceIn with variables and expressions', () => { + const code = 'const greeting = pm.variables.replaceIn("Hello {{name}}, your user id is {{userId}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const greeting = bru.interpolate("Hello {{name}}, your user id is {{userId}}");'); + }); + + it('should translate pm.variables.replaceIn within complex expressions', () => { + const code = 'const url = baseUrl + pm.variables.replaceIn("/users/{{userId}}/profile");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const url = baseUrl + bru.interpolate("/users/{{userId}}/profile");'); + }); + + it('should translate pm.variables.replaceIn with multiple nested variable references', () => { + const code = 'const template = pm.variables.replaceIn("{{prefix}}-{{env}}-{{suffix}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const template = bru.interpolate("{{prefix}}-{{env}}-{{suffix}}");'); + }); + + it('should translate aliased variables.replaceIn', () => { + const code = ` + const variables = pm.variables; + const message = variables.replaceIn("Welcome, {{username}}!"); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + const message = bru.interpolate("Welcome, {{username}}!"); + `); + }); + // 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");'); }); it('should handle pm.globals.get', () => { const code = 'pm.globals.get("test");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getGlobalEnvVar("test");'); }); it('should handle pm.globals.set', () => { const code = 'pm.globals.set("test", "value");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setGlobalEnvVar("test", "value");'); }); @@ -66,6 +115,7 @@ describe('Variables Translation', () => { const get = vars.get("test"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const has = bru.hasVar("test"); const set = bru.setVar("test", "value"); @@ -83,6 +133,7 @@ describe('Variables Translation', () => { const unset = collVars.unset("test"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const has = bru.hasVar("test"); const set = bru.setVar("test", "value"); @@ -98,6 +149,7 @@ describe('Variables Translation', () => { const set = globals.set("test", "value"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const get = bru.getGlobalEnvVar("test"); const set = bru.setGlobalEnvVar("test", "value"); @@ -108,6 +160,7 @@ describe('Variables Translation', () => { 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";'); }); @@ -148,6 +201,7 @@ describe('Variables Translation', () => { 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-js/src/bru.js b/packages/bruno-js/src/bru.js index 77255b3a1..d38d28983 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,5 +1,5 @@ const { cloneDeep } = require('lodash'); -const { interpolate } = require('@usebruno/common'); +const { interpolate: _interpolate } = require('@usebruno/common'); const variableNameRegex = /^[\w-.]*$/; @@ -28,10 +28,10 @@ class Bru { }; } - _interpolate = (str) => { - if (!str || !str.length || typeof str !== 'string') { - return str; - } + interpolate = (strOrObj) => { + if (!strOrObj) return strOrObj; + const isObj = typeof strOrObj === 'object'; + const strToInterpolate = isObj ? JSON.stringify(strOrObj) : strOrObj; const combinedVars = { ...this.globalEnvironmentVariables, @@ -48,7 +48,8 @@ class Bru { } }; - return interpolate(str, combinedVars); + const interpolatedStr = _interpolate(strToInterpolate, combinedVars); + return isObj ? JSON.parse(interpolatedStr) : interpolatedStr; }; cwd() { @@ -68,7 +69,7 @@ class Bru { } getEnvVar(key) { - return this._interpolate(this.envVariables[key]); + return this.interpolate(this.envVariables[key]); } setEnvVar(key, value) { @@ -84,7 +85,7 @@ class Bru { } getGlobalEnvVar(key) { - return this._interpolate(this.globalEnvironmentVariables[key]); + return this.interpolate(this.globalEnvironmentVariables[key]); } setGlobalEnvVar(key, value) { @@ -96,7 +97,7 @@ class Bru { } getOauth2CredentialVar(key) { - return this._interpolate(this.oauth2CredentialVariables[key]); + return this.interpolate(this.oauth2CredentialVariables[key]); } hasVar(key) { @@ -111,7 +112,7 @@ class Bru { if (variableNameRegex.test(key) === false) { throw new Error( `Variable name: "${key}" contains invalid characters!` + - ' Names must only contain alpha-numeric characters, "-", "_", "."' + ' Names must only contain alpha-numeric characters, "-", "_", "."' ); } @@ -122,11 +123,11 @@ class Bru { if (variableNameRegex.test(key) === false) { throw new Error( `Variable name: "${key}" contains invalid characters!` + - ' Names must only contain alpha-numeric characters, "-", "_", "."' + ' Names must only contain alpha-numeric characters, "-", "_", "."' ); } - return this._interpolate(this.runtimeVariables[key]); + return this.interpolate(this.runtimeVariables[key]); } deleteVar(key) { @@ -142,15 +143,15 @@ class Bru { } getCollectionVar(key) { - return this._interpolate(this.collectionVariables[key]); + return this.interpolate(this.collectionVariables[key]); } getFolderVar(key) { - return this._interpolate(this.folderVariables[key]); + return this.interpolate(this.folderVariables[key]); } getRequestVar(key) { - return this._interpolate(this.requestVariables[key]); + return this.interpolate(this.requestVariables[key]); } setNextRequest(nextRequest) { diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 1d9680bab..a8f2abbee 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -98,7 +98,7 @@ class ScriptRuntime { }; } - if(runRequestByItemPathname) { + if (runRequestByItemPathname) { context.bru.runRequest = runRequestByItemPathname; } @@ -151,7 +151,7 @@ class ScriptRuntime { chai, 'node-fetch': fetch, 'crypto-js': CryptoJS, - 'xml2js': xml2js, + xml2js: xml2js, cheerio, tv4, ...whitelistedModules, @@ -235,7 +235,7 @@ class ScriptRuntime { }; } - if(runRequestByItemPathname) { + if (runRequestByItemPathname) { context.bru.runRequest = runRequestByItemPathname; } diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index b8ffa76ab..8439d7206 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -29,6 +29,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getProcessEnv', getProcessEnv); getProcessEnv.dispose(); + let interpolate = vm.newFunction('interpolate', function (str) { + return marshallToVm(bru.interpolate(vm.dump(str)), vm); + }); + vm.setProp(bruObject, 'interpolate', interpolate); + interpolate.dispose(); + let hasEnvVar = vm.newFunction('hasEnvVar', function (key) { return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm); }); @@ -157,7 +163,8 @@ const addBruShimToContext = (vm, bru) => { let getTestResults = vm.newFunction('getTestResults', () => { const promise = vm.newPromise(); - bru.getTestResults() + bru + .getTestResults() .then((results) => { promise.resolve(marshallToVm(cleanJson(results), vm)); }) @@ -178,7 +185,8 @@ const addBruShimToContext = (vm, bru) => { let getAssertionResults = vm.newFunction('getAssertionResults', () => { const promise = vm.newPromise(); - bru.getAssertionResults() + bru + .getAssertionResults() .then((results) => { promise.resolve(marshallToVm(cleanJson(results), vm)); }) @@ -199,7 +207,8 @@ const addBruShimToContext = (vm, bru) => { let runRequestHandle = vm.newFunction('runRequest', (args) => { const promise = vm.newPromise(); - bru.runRequest(vm.dump(args)) + bru + .runRequest(vm.dump(args)) .then((response) => { const { status, headers, data, dataBuffer, size, statusText } = response || {}; promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm)); diff --git a/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru b/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru new file mode 100644 index 000000000..a8e6dff76 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru @@ -0,0 +1,39 @@ +meta { + name: interpolate + type: http + seq: 13 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +tests { + test("should interpolate envs", function() { + const interpolated = bru.interpolate("url: {{host}}") + expect(interpolated).to.equal("url: https://testbench-sanity.usebruno.com"); + }); + + test("should interpolate random variables", function() { + const a = bru.interpolate("{{$randomInt}}") + const b = bru.interpolate("{{$randomInt}}") + expect(a).to.not.equal(b) + }); + + const randomObj = { + host: "{{host}}", + int: "{{$randomInt}}", + timestamp: "{{$timestamp}}" + } + + test("should interpolate objects with vars, random vars", function() { + const objA = bru.interpolate(randomObj) + const objB = bru.interpolate(randomObj) + + expect(objA).to.be.an("object") + expect(objB).to.be.an("object") + expect(objA).to.not.deep.eql(objB) + }); +} From 2cd985faf78b35d9923f583e7edb0ab5b5debbbc Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Fri, 23 May 2025 08:58:28 +0530 Subject: [PATCH 868/904] Remove test file for redirects with cookies --- .../redirects/Test Redirects With Cookies.bru | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru diff --git a/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru b/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru deleted file mode 100644 index 202390c1e..000000000 --- a/packages/bruno-tests/collection/redirects/Test Redirects With Cookies.bru +++ /dev/null @@ -1,23 +0,0 @@ -meta { - name: Test Redirects With Cookies - type: http - seq: 1 -} - -post { - url: {{httpfaker}}/api/auth/cookie-redirect/login - body: json - auth: none -} - -headers { - Content-Type: application/json -} - -body:json { - username: test-user -} - -assert { - res.status: 200 -} \ No newline at end of file From a4fff01647b683a4188ae03f3e818905100178a9 Mon Sep 17 00:00:00 2001 From: Chriss4123 Date: Fri, 14 Feb 2025 20:48:48 +0200 Subject: [PATCH 869/904] Support Secure cookies for localhost and loopback addresses --- packages/bruno-electron/src/utils/cookies.js | 5 +- .../src/utils/trustworthy-util.js | 103 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-electron/src/utils/trustworthy-util.js diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js index 7585e9a8a..5095a8b20 100644 --- a/packages/bruno-electron/src/utils/cookies.js +++ b/packages/bruno-electron/src/utils/cookies.js @@ -1,4 +1,5 @@ const { Cookie, CookieJar } = require('tough-cookie'); +const { isPotentiallyTrustworthy } = require('./trustworthy-util'); const each = require('lodash/each'); const moment = require('moment'); @@ -12,7 +13,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => { }; const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url); + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthy(url) + }); }; const getCookieStringForUrl = (url) => { diff --git a/packages/bruno-electron/src/utils/trustworthy-util.js b/packages/bruno-electron/src/utils/trustworthy-util.js new file mode 100644 index 000000000..f709e5e80 --- /dev/null +++ b/packages/bruno-electron/src/utils/trustworthy-util.js @@ -0,0 +1,103 @@ +const { URL } = require('url'); +const net = require('net'); + +const isLoopbackV4 = (address) => { + // 127.0.0.0/8: first octet = 127 + const octets = address.split('.'); + return ( + octets.length === 4 + ) && parseInt(octets[0], 10) === 127; +} + +const isLoopbackV6 = (address) => { + // new URL(...) follows the WHATWG URL Standard + // which compresses IPv6 addresses, therefore the IPv6 + // loopback address will always be compressed to '[::1]': + // https://url.spec.whatwg.org/#concept-ipv6-serializer + return (address === '::1'); +} + +const isIpLoopback = (address) => { + if (net.isIPv4(address)) { + return isLoopbackV4(address); + } + + if (net.isIPv6(address)) { + return isLoopbackV6(address); + } + + return false; +} + +const isNormalizedLocalhostTLD = (host) => { + return host.toLowerCase().endsWith('.localhost'); +} + +const isLocalHostname = (host) => { + return host.toLowerCase() === 'localhost' || + isNormalizedLocalhostTLD(host); +} + +/** + * Removes leading and trailing square brackets if present. + * Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448 + * + * @param {string} host + * @returns {string} + */ +const hostNoBrackets = (host) => { + if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { + return host.substring(1, host.length - 1); + } + return host; +} + +/** + * Determines if a URL string represents a potentially trustworthy origin. + * + * A URL is considered potentially trustworthy if it: + * - Uses HTTPS, WSS or file schemes + * - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1) + * - Uses localhost or *.localhost hostnames + * + * @param {string} urlString - The URL to check + * @returns {boolean} + * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec} + */ +const isPotentiallyTrustworthy = (urlString) => { + let url; + + // try ... catch doubles as an opaque origin check + try { + url = new URL(urlString); + } catch { + return false; + } + + const scheme = url.protocol.replace(':', '').toLowerCase(); + const hostname = hostNoBrackets( + url.hostname + ).replace(/\.+$/, ''); + + if ( + scheme === 'https' || + scheme === 'wss' || + scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin + ) { + return true; + } + + // If it's already an IP literal, check if it's a loopback address + if (net.isIP(hostname)) { + return isIpLoopback(hostname); + } + + // RFC 6761 states that localhost names will always resolve + // to the respective IP loopback address: + // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 + return isLocalHostname(hostname); +} + +module.exports = { + isPotentiallyTrustworthy +}; \ No newline at end of file From 2c3d2ff6a74d259993dcc4923f6c44232eb43a21 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Mon, 19 May 2025 18:09:38 +0530 Subject: [PATCH 870/904] Make Secure-local-cookies work in CLI as well --- packages/bruno-cli/src/utils/cookies.js | 5 ++++- packages/bruno-electron/src/utils/cookies.js | 4 ++-- packages/bruno-requests/src/index.ts | 2 ++ .../src/utils/cookie-utils.js} | 14 ++++++++------ packages/bruno-requests/src/utils/index.ts | 1 + 5 files changed, 17 insertions(+), 9 deletions(-) rename packages/{bruno-electron/src/utils/trustworthy-util.js => bruno-requests/src/utils/cookie-utils.js} (89%) create mode 100644 packages/bruno-requests/src/utils/index.ts diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js index acb58b505..01a82316b 100644 --- a/packages/bruno-cli/src/utils/cookies.js +++ b/packages/bruno-cli/src/utils/cookies.js @@ -1,5 +1,6 @@ const { Cookie, CookieJar } = require('tough-cookie'); const each = require('lodash/each'); +const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; const cookieJar = new CookieJar(); @@ -11,7 +12,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => { }; const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url); + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthyOrigin(url) + }); }; const getCookieStringForUrl = (url) => { diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js index 5095a8b20..7f3751eaf 100644 --- a/packages/bruno-electron/src/utils/cookies.js +++ b/packages/bruno-electron/src/utils/cookies.js @@ -1,7 +1,7 @@ const { Cookie, CookieJar } = require('tough-cookie'); -const { isPotentiallyTrustworthy } = require('./trustworthy-util'); const each = require('lodash/each'); const moment = require('moment'); +const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; const cookieJar = new CookieJar(); @@ -14,7 +14,7 @@ const addCookieToJar = (setCookieHeader, requestUrl) => { const getCookiesForUrl = (url) => { return cookieJar.getCookiesSync(url, { - secure: isPotentiallyTrustworthy(url) + secure: isPotentiallyTrustworthyOrigin(url) }); }; diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 5513916c5..01850f3e4 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1 +1,3 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; + +export * as utils from './utils'; diff --git a/packages/bruno-electron/src/utils/trustworthy-util.js b/packages/bruno-requests/src/utils/cookie-utils.js similarity index 89% rename from packages/bruno-electron/src/utils/trustworthy-util.js rename to packages/bruno-requests/src/utils/cookie-utils.js index f709e5e80..6a1a5ac57 100644 --- a/packages/bruno-electron/src/utils/trustworthy-util.js +++ b/packages/bruno-requests/src/utils/cookie-utils.js @@ -1,5 +1,5 @@ -const { URL } = require('url'); -const net = require('net'); +const { URL } = require('node:url'); +const net = require('node:net'); const isLoopbackV4 = (address) => { // 127.0.0.0/8: first octet = 127 @@ -64,14 +64,16 @@ const hostNoBrackets = (host) => { * @returns {boolean} * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec} */ -const isPotentiallyTrustworthy = (urlString) => { +const isPotentiallyTrustworthyOrigin = (urlString) => { let url; // try ... catch doubles as an opaque origin check try { url = new URL(urlString); - } catch { - return false; + } catch (e) { + if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') { + return false; + } else throw e; } const scheme = url.protocol.replace(':', '').toLowerCase(); @@ -99,5 +101,5 @@ const isPotentiallyTrustworthy = (urlString) => { } module.exports = { - isPotentiallyTrustworthy + isPotentiallyTrustworthyOrigin }; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/index.ts b/packages/bruno-requests/src/utils/index.ts new file mode 100644 index 000000000..dd94dd186 --- /dev/null +++ b/packages/bruno-requests/src/utils/index.ts @@ -0,0 +1 @@ +export * from './cookie-utils'; From 7c27193983aac45a99a65bba656d0345e5a48a57 Mon Sep 17 00:00:00 2001 From: maintainer-bruno Date: Fri, 23 May 2025 16:57:48 +0530 Subject: [PATCH 871/904] chore: add CODEOWNERS file for repository maintenance --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..b033398a3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno From 0948964677dc6c8c8995587ea525c6a5c2d66776 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 09:47:43 +0530 Subject: [PATCH 872/904] Revert changes to common.spec.js --- .../bruno-electron/tests/utils/common.spec.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 0c69a4c50..077aac16d 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -24,25 +24,25 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Bob', age: 28 }, ], }; - + const expectedOutput = { 'users[0].name': 'Alice', 'users[0].age': 25, 'users[1].name': 'Bob', 'users[1].age': 28, }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an empty object', () => { const input = {}; - + const expectedOutput = {}; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with nested objects', () => { const input = { person: { @@ -53,16 +53,16 @@ describe('utils: flattenDataForDotNotation', () => { }, }, }; - + const expectedOutput = { 'person.name': 'Alice', 'person.address.city': 'New York', 'person.address.zipcode': '10001', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); - + test('Flatten an object with arrays of objects', () => { const input = { teams: [ @@ -70,7 +70,7 @@ describe('utils: flattenDataForDotNotation', () => { { name: 'Team B', members: ['Charlie', 'David'] }, ], }; - + const expectedOutput = { 'teams[0].name': 'Team A', 'teams[0].members[0]': 'Alice', @@ -79,7 +79,7 @@ describe('utils: flattenDataForDotNotation', () => { 'teams[1].members[0]': 'Charlie', 'teams[1].members[1]': 'David', }; - + expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); }); \ No newline at end of file From 256f63dd38a71c4a9d485d8d6feede155ee4f27f Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 26 May 2025 10:20:22 +0530 Subject: [PATCH 873/904] single line editor comp onChange validations --- packages/bruno-app/src/components/SingleLineEditor/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 30c079a36..99084602f 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -107,7 +107,7 @@ class SingleLineEditor extends Component { _onEdit = () => { if (!this.ignoreChangeEvent && this.editor) { this.cachedValue = this.editor.getValue(); - if (this.props.onChange) { + if (this.props.onChange && (this.props.value !== this.cachedValue)) { this.props.onChange(this.cachedValue); } } From 8d860a051ca729192f75cd2af9f5864eece09c4c Mon Sep 17 00:00:00 2001 From: anusree-bruno Date: Mon, 26 May 2025 14:43:23 +0530 Subject: [PATCH 874/904] replace real time with mocked time in faker tests --- .../src/utils/faker-functions.spec.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/bruno-common/src/utils/faker-functions.spec.ts b/packages/bruno-common/src/utils/faker-functions.spec.ts index 1cbc574f4..8df4eb190 100644 --- a/packages/bruno-common/src/utils/faker-functions.spec.ts +++ b/packages/bruno-common/src/utils/faker-functions.spec.ts @@ -1,14 +1,21 @@ import { mockDataFunctions } from "./faker-functions"; describe("mockDataFunctions Regex Validation", () => { - test("timestamp and isoTimestamp should return current time values", () => { - const now = Math.floor(Date.now() / 1000); - const timestamp = parseInt(mockDataFunctions.timestamp()); - const isoTimestamp = new Date(mockDataFunctions.isoTimestamp()).getTime() / 1000; + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + }); - // Allow for a 2-second difference to account for test execution time - expect(Math.abs(timestamp - now)).toBeLessThanOrEqual(2); - expect(Math.abs(isoTimestamp - now)).toBeLessThanOrEqual(2); + afterAll(() => { + jest.useRealTimers(); + }); + + test("timestamp and isoTimestamp should return mocked time values", () => { + const expectedTimestamp = '1704067200'; + const expectedIsoTimestamp = '2024-01-01T00:00:00.000Z'; + + expect(mockDataFunctions.timestamp()).toBe(expectedTimestamp); + expect(mockDataFunctions.isoTimestamp()).toBe(expectedIsoTimestamp); }); test("all values should match their expected patterns", () => { @@ -149,3 +156,23 @@ describe("mockDataFunctions Regex Validation", () => { } }); }); + +describe("Time-based tests", () => { + beforeAll(() => { + // Set up fake timers + jest.useFakeTimers(); + // Set a specific point in time + jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + }); + + afterAll(() => { + // Clean up + jest.useRealTimers(); + }); + + test("should handle time-based operations", () => { + // Your time-based tests here + const now = new Date(); + expect(now.toISOString()).toBe('2024-01-01T00:00:00.000Z'); + }); +}); From 8ac916b0ffc247cfa197bf89aca8a51707a470a0 Mon Sep 17 00:00:00 2001 From: anusree-bruno Date: Mon, 26 May 2025 14:49:21 +0530 Subject: [PATCH 875/904] removed unwanted tests --- .../src/utils/faker-functions.spec.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/packages/bruno-common/src/utils/faker-functions.spec.ts b/packages/bruno-common/src/utils/faker-functions.spec.ts index 8df4eb190..61388ee9d 100644 --- a/packages/bruno-common/src/utils/faker-functions.spec.ts +++ b/packages/bruno-common/src/utils/faker-functions.spec.ts @@ -21,8 +21,6 @@ describe("mockDataFunctions Regex Validation", () => { test("all values should match their expected patterns", () => { const patterns: Record = { guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, - timestamp: /^\d{10}$/, - isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, randomAlphaNumeric: /^[\w]$/, randomBoolean: /^(true|false)$/, @@ -156,23 +154,3 @@ describe("mockDataFunctions Regex Validation", () => { } }); }); - -describe("Time-based tests", () => { - beforeAll(() => { - // Set up fake timers - jest.useFakeTimers(); - // Set a specific point in time - jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); - }); - - afterAll(() => { - // Clean up - jest.useRealTimers(); - }); - - test("should handle time-based operations", () => { - // Your time-based tests here - const now = new Date(); - expect(now.toISOString()).toBe('2024-01-01T00:00:00.000Z'); - }); -}); From a8e5ce9c13e048ebb1d4894502ce942ccbb303b1 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 26 May 2025 14:58:25 +0530 Subject: [PATCH 876/904] fix: new request shortcut key --- packages/bruno-app/src/providers/Hotkeys/index.js | 6 ++++-- packages/bruno-tests/collection/new request.bru | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-tests/collection/new request.bru diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index f9316eb94..f756fb86f 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -211,13 +211,15 @@ export const HotkeysProvider = (props) => { }; }, [activeTabUid, tabs, collections, dispatch]); + const currentCollection = getCurrentCollection(); + return ( {showEnvSettingsModal && ( - setShowEnvSettingsModal(false)} /> + setShowEnvSettingsModal(false)} /> )} {showNewRequestModal && ( - setShowNewRequestModal(false)} /> + setShowNewRequestModal(false)} /> )}
{props.children}
diff --git a/packages/bruno-tests/collection/new request.bru b/packages/bruno-tests/collection/new request.bru new file mode 100644 index 000000000..2b5bd2cb6 --- /dev/null +++ b/packages/bruno-tests/collection/new request.bru @@ -0,0 +1,11 @@ +meta { + name: new request + type: http + seq: 14 +} + +get { + url: http://localhost:6000 + body: none + auth: inherit +} From c293ceefcf213c17731719fe27d7848f7eb5ae71 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:37:28 +0530 Subject: [PATCH 877/904] Refactored fetch-gql-schema-handler.spec.js --- .../network/fetch-gql-schema-handler.spec.js | 241 +++--------------- 1 file changed, 30 insertions(+), 211 deletions(-) diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 064f78daf..3104807e8 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,39 +1,45 @@ -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); - -// Mock the module -jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { - return jest.fn().mockReturnValue({ - method: 'POST', - url: 'https://example.com/', - headers: {}, - data: '{}' - }); -}); - -// Mock the collection utils +// Mock the modules first, before requiring them jest.mock('../../src/utils/collection', () => { const original = jest.requireActual('../../src/utils/collection'); return { ...original, - getTreePathFromCollectionToItem: jest.fn(), - mergeVars: jest.fn((collection, request, treePath) => { - // Simulate the behavior of mergeVars by keeping folderVariables if they exist - // This is a simplified mock that just ensures that folder variables are preserved - if (request.folderVariables) { - // We don't need to modify the request, just ensure folderVariables remain - } + getTreePathFromCollectionToItem: jest.fn().mockReturnValue([]), + mergeVars: jest.fn(), + getEnvVars: jest.fn(env => { + if (!env || !env.variables) return {}; + return env.variables.reduce((acc, variable) => { + if (variable.enabled) { + acc[variable.name] = variable.value; + } + return acc; + }, {}); }) }; }); -describe('Prepare GQL Introspection Request', () => { +jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { + return jest.fn().mockImplementation((endpoint, vars, request, root) => { + return { + url: endpoint, + method: 'POST', + headers: request?.headers || {}, + data: { + query: '{ __schema { types { name } } }' + } + }; + }); +}); + +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); +const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); + +describe('fetchGqlSchemaHandler', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should receive combined variables from fetchGqlSchema', async () => { + it('should receive combined variables from fetchGqlSchemaHandler', async () => { const endpoint = 'https://example.com/'; const environment = { variables: [ @@ -289,26 +295,6 @@ describe('Prepare GQL Introspection Request', () => { } }; - // Make sure our mock properly returns the folder variables - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // In a real scenario, the resolvedVars would include the folder variables - // Simulate the correct merge of variables - const combinedVars = { - ...resolvedVars, - SHARED_VAR: 'folder-value' // This simulates the correct precedence - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(combinedVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( @@ -377,173 +363,6 @@ describe('Prepare GQL Introspection Request', () => { collection.root ); }); - - it('should properly respect the complete variable precedence hierarchy', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'ENV_VAR', value: 'env-value', enabled: true }, - { name: 'SHARED_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' - }, - folderVariables: { - FOLDER_VAR: 'folder-value', - SHARED_VAR: 'folder-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - RUNTIME_VAR: 'runtime-value', - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: { - GLOBAL_VAR: 'global-value', - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - // Make sure our mock returns the variables with correct precedence - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // Manually apply the correct precedence for this test - const correctVars = { - GLOBAL_VAR: 'global-value', - ENV_VAR: 'env-value', - RUNTIME_VAR: 'runtime-value', - FOLDER_VAR: 'folder-value', - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' // Highest precedence wins - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(correctVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - GLOBAL_VAR: 'global-value', - ENV_VAR: 'env-value', - RUNTIME_VAR: 'runtime-value', - FOLDER_VAR: 'folder-value', - REQUEST_VAR: 'request-value', - SHARED_VAR: 'request-value' // Shows highest precedence wins - }), - request, - collection.root - ); - }); }); -describe('GraphQL Schema Handler Header Tests', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - const createBasicSetup = () => ({ - endpoint: 'https://example.com/', - environment: { variables: [] }, - request: { vars: {}, headers: [] }, - collection: { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: {}, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - } - }); - - it('should pass root headers to request', async () => { - const setup = createBasicSetup(); - setup.collection.root.request.headers = [ - { name: 'X-Root-Header', value: 'root-value', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); - - it('should pass request headers to request', async () => { - const setup = createBasicSetup(); - setup.request.headers = [ - { name: 'X-Request-Header', value: 'request-value', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); - - it('should handle environment variables in headers', async () => { - const setup = createBasicSetup(); - setup.environment.variables = [ - { name: 'AUTH_TOKEN', value: 'token-value', enabled: true } - ]; - setup.request.headers = [ - { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.objectContaining({ - AUTH_TOKEN: 'token-value' - }), - setup.request, - setup.collection.root - ); - }); - - it('should handle enabled and disabled headers', async () => { - const setup = createBasicSetup(); - setup.request.headers = [ - { name: 'X-Enabled', value: 'enabled', enabled: true }, - { name: 'X-Disabled', value: 'disabled', enabled: false } - ]; - - await fetchGqlSchemaHandler(null, setup.endpoint, setup.environment, setup.request, setup.collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - setup.endpoint, - expect.any(Object), - setup.request, - setup.collection.root - ); - }); -}); From 91397eaf5732062cab36d48ab0857015558bb58c Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:38:09 +0530 Subject: [PATCH 878/904] Renamed fetchGqlSchema to fetchGqlSchemaHandler --- packages/bruno-electron/src/ipc/network/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dc76d9690..e25d02899 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1327,4 +1327,4 @@ const registerNetworkIpc = (mainWindow) => { module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; -module.exports.fetchGqlSchema = fetchGqlSchema; +module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler; From 788569a5f469e5f6930065577891863767f86500 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:39:07 +0530 Subject: [PATCH 879/904] Added testcases for prepare-gql-introspection-request.spec.js --- .../prepare-gql-introspection-request.spec.js | 120 +++++++++--------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js index a541b9f2f..2eacde679 100644 --- a/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js @@ -1,68 +1,66 @@ -const { interpolate } = require('@usebruno/common'); -const { setAuthHeaders } = require('../../src/ipc/network/prepare-request'); const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchema } = require('../../src/ipc/network'); -// Mock the module -jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { - return jest.fn().mockReturnValue({ - method: 'POST', - url: 'https://example.com/', - headers: {}, - data: '{}' - }); -}); - -describe('Prepare GQL Introspection Request', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should receive combined variables from fetchGqlSchema', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'API_TOKEN', value: 'secret-token', enabled: true }, - { name: 'ENV_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - requestVar: 'request-value' - }, - headers: [ - { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } - ] - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - runtimeVar: 'runtime-value' - }, - globalEnvironmentVariables: { - globalVar: 'global-value' - }, - root: { - request: { - headers: [] - } +describe('prepareGqlIntrospectionRequest', () => { + const createBasicSetup = () => ({ + endpoint: 'https://example.com/', + request: { + headers: [] + }, + collectionRoot: { + request: { + headers: [] } + } + }); + + it('should handle environment variables in headers', () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true } + ]; + const vars = { + AUTH_TOKEN: 'token-value' }; - await fetchGqlSchema(endpoint, environment, request, collection); + const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot); - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - API_TOKEN: 'secret-token', - ENV_VAR: 'env-value', - requestVar: 'request-value', - runtimeVar: 'runtime-value', - globalVar: 'global-value' - }), - request, - collection.root - ); + expect(result.headers['Authorization']).toBe('Bearer token-value'); + expect(result.method).toBe('POST'); + expect(result.url).toBe(setup.endpoint); }); -}); + + it('should override collection headers with request headers', () => { + const setup = createBasicSetup(); + setup.collectionRoot.request.headers = [ + { name: 'X-Header', value: 'collection-value', enabled: true } + ]; + setup.request.headers = [ + { name: 'X-Header', value: 'request-value', enabled: true } + ]; + + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + + expect(result.headers['X-Header']).toBe('request-value'); + }); + + it('should handle enabled and disabled headers', () => { + const setup = createBasicSetup(); + setup.request.headers = [ + { name: 'X-Enabled', value: 'enabled', enabled: true }, + { name: 'X-Disabled', value: 'disabled', enabled: false } + ]; + + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + + expect(result.headers['X-Enabled']).toBe('enabled'); + expect(result.headers['X-Disabled']).toBeUndefined(); + }); + + it('should always include required GraphQL headers', () => { + const setup = createBasicSetup(); + const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot); + expect(result.headers['Accept']).toBe('application/json'); + expect(result.headers['Content-Type']).toBe('application/json'); + }); + +}); \ No newline at end of file From ce1110bdd45c0b35da4d4ec6269e6d29810d097c Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:39:40 +0530 Subject: [PATCH 880/904] Added interpolate for header values --- .../src/ipc/network/prepare-gql-introspection-request.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js index 851069bb3..158a71dc6 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js @@ -16,7 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect method: 'POST', url: endpoint, headers: { - ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), + ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars), Accept: 'application/json', 'Content-Type': 'application/json' }, @@ -26,20 +26,20 @@ const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collect return setAuthHeaders(axiosRequest, request, collectionRoot); }; -const mapHeaders = (requestHeaders, collectionHeaders) => { +const mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => { const headers = {}; // Add collection headers first each(collectionHeaders, (h) => { if (h.enabled) { - headers[h.name] = h.value; + headers[h.name] = interpolate(h.value, resolvedVars); } }); // Then add request headers, which will overwrite if names overlap each(requestHeaders, (h) => { if (h.enabled) { - headers[h.name] = h.value; + headers[h.name] = interpolate(h.value, resolvedVars); } }); From 9f1aed32097bcfdaf379cd7d05c58ee0810bd584 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 16:42:18 +0530 Subject: [PATCH 881/904] Refactored fetch-gql-schema-handler.spec.js --- .../network/fetch-gql-schema-handler.spec.js | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 3104807e8..250237e32 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,3 +1,6 @@ +const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); +const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); + // Mock the modules first, before requiring them jest.mock('../../src/utils/collection', () => { const original = jest.requireActual('../../src/utils/collection'); @@ -30,15 +33,15 @@ jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { }); }); -const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); -const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -const { getTreePathFromCollectionToItem } = require('../../src/utils/collection'); - describe('fetchGqlSchemaHandler', () => { beforeEach(() => { jest.clearAllMocks(); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should receive combined variables from fetchGqlSchemaHandler', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -71,9 +74,6 @@ describe('fetchGqlSchemaHandler', () => { } }; - // Set up empty tree path since we don't need it for this test - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( @@ -332,26 +332,6 @@ describe('fetchGqlSchemaHandler', () => { } }; - // Make sure our mock properly returns the folder variables with correct precedence - prepareGqlIntrospectionRequest.mockImplementationOnce((endpoint, resolvedVars, req, root) => { - // In a real scenario, the resolvedVars would include the folder variables - // Simulate the correct merge of variables - const combinedVars = { - ...resolvedVars, - SHARED_VAR: 'folder-value' // This simulates the correct precedence - }; - - return { - method: 'POST', - url: endpoint, - headers: {}, - data: JSON.stringify(combinedVars) - }; - }); - - // Set up empty tree path - getTreePathFromCollectionToItem.mockReturnValue([]); - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( From 6b122d7262e1656e9bfe7f465f469cdd89931e05 Mon Sep 17 00:00:00 2001 From: Clay Powers Date: Mon, 26 May 2025 07:25:11 -0400 Subject: [PATCH 882/904] Switch GraphQL variables code editor to json linting (#4756) --- .../src/components/RequestPane/GraphQLVariables/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js index d490d8579..228a54fa8 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js @@ -64,7 +64,7 @@ const GraphQLVariables = ({ variables, item, collection }) => { font={get(preferences, 'font.codeFont', 'default')} fontSize={get(preferences, 'font.codeFontSize')} onEdit={onEdit} - mode="javascript" + mode="application/json" onRun={onRun} onSave={onSave} enableVariableHighlighting={true} From 865e813b421847a41e64ffd7425da12d7fb2e126 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 26 May 2025 20:45:33 +0530 Subject: [PATCH 883/904] revert test bru file --- packages/bruno-tests/collection/new request.bru | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 packages/bruno-tests/collection/new request.bru diff --git a/packages/bruno-tests/collection/new request.bru b/packages/bruno-tests/collection/new request.bru deleted file mode 100644 index 2b5bd2cb6..000000000 --- a/packages/bruno-tests/collection/new request.bru +++ /dev/null @@ -1,11 +0,0 @@ -meta { - name: new request - type: http - seq: 14 -} - -get { - url: http://localhost:6000 - body: none - auth: inherit -} From 8975b9eef6cb5f0d1a4702cd0d81cff261a84ccf Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Mon, 26 May 2025 21:06:01 +0545 Subject: [PATCH 884/904] fix: update Windows build configuration for icon and publisher name --- packages/bruno-electron/electron-builder-config.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/bruno-electron/electron-builder-config.js b/packages/bruno-electron/electron-builder-config.js index a6a99c112..2ac21d0bf 100644 --- a/packages/bruno-electron/electron-builder-config.js +++ b/packages/bruno-electron/electron-builder-config.js @@ -36,25 +36,22 @@ const config = { }, win: { artifactName: '${name}_${version}_${arch}_win.${ext}', - icon: 'resources/icons/png', - publisherName: 'Anoop MD', + icon: 'resources/icons/win/icon.ico', target: [ { target: 'nsis', arch: ['x64'] } - ] + ], + sign: null, + publisherName: 'Bruno Software Inc' }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true, allowElevation: true, createDesktopShortcut: true, - createStartMenuShortcut: true, - installerIcon: "resources/icons/win/icon.ico", - uninstallerIcon: "resources/icons/win/icon.ico", - installerHeaderIcon: "resources/icons/win/icon.ico", - warningsAsErrors: false + createStartMenuShortcut: true } }; From afb2d3dffd1e7398261409f84e22e2d0563b80a4 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 22:52:37 +0530 Subject: [PATCH 885/904] Updated resolved variable assignment and testcases --- .../bruno-electron/src/ipc/network/index.js | 9 +- .../network/fetch-gql-schema-handler.spec.js | 420 ++++++++---------- 2 files changed, 199 insertions(+), 230 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index e25d02899..3adb5ea88 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -329,16 +329,17 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const globalEnvironmentVars = collection.globalEnvironmentVariables; const collectionRuntimeVars = collection.runtimeVariables; const folderVars = resolvedRequest.folderVariables; - const requestRuntimeVars = resolvedRequest.vars; + const requestVariables = resolvedRequest.requestVariables; + const collectionVariables = resolvedRequest.collectionVariables; - // Precedence: globalEnvironmentVars < envVars < collectionEnvVars < collectionRunTimeVars < folderVars < requestRunTimeVars + // Precedence: globalEnvironmentVars < collectionVariables < envVars < folderVars < requestVariables const resolvedVars = merge( {}, globalEnvironmentVars, + collectionVariables, envVars, - collectionRuntimeVars, folderVars, - requestRuntimeVars + requestVariables ); const collectionRoot = get(collection, 'root', {}); diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index 250237e32..fdfff7b89 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -1,25 +1,7 @@ const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request'); const { fetchGqlSchemaHandler } = require('../../src/ipc/network'); -// Mock the modules first, before requiring them -jest.mock('../../src/utils/collection', () => { - const original = jest.requireActual('../../src/utils/collection'); - return { - ...original, - getTreePathFromCollectionToItem: jest.fn().mockReturnValue([]), - mergeVars: jest.fn(), - getEnvVars: jest.fn(env => { - if (!env || !env.variables) return {}; - return env.variables.reduce((acc, variable) => { - if (variable.enabled) { - acc[variable.name] = variable.value; - } - return acc; - }, {}); - }) - }; -}); - +// Mock only the prepare-gql-introspection-request to avoid network calls jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { return jest.fn().mockImplementation((endpoint, vars, request, root) => { return { @@ -42,54 +24,6 @@ describe('fetchGqlSchemaHandler', () => { jest.restoreAllMocks(); }); - it('should receive combined variables from fetchGqlSchemaHandler', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'API_TOKEN', value: 'secret-token', enabled: true }, - { name: 'ENV_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: { - requestVar: 'request-value' - }, - headers: [ - { name: 'Authorization', value: 'Bearer {{API_TOKEN}}', enabled: true } - ] - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - runtimeVar: 'runtime-value' - }, - globalEnvironmentVariables: { - globalVar: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - API_TOKEN: 'secret-token', - ENV_VAR: 'env-value', - requestVar: 'request-value', - runtimeVar: 'runtime-value', - globalVar: 'global-value' - }), - request, - collection.root - ); - }); - it('should override global environment variables with environment variables', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -98,7 +32,10 @@ describe('fetchGqlSchemaHandler', () => { ] }; const request = { - vars: {} + uid: 'test-request', + vars: { + req: [] // No request variables + } }; const collection = { uid: 'test-collection', @@ -107,9 +44,22 @@ describe('fetchGqlSchemaHandler', () => { globalEnvironmentVariables: { SHARED_VAR: 'global-value' }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -126,150 +76,6 @@ describe('fetchGqlSchemaHandler', () => { ); }); - it('should override environment variables with collection runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [ - { name: 'SHARED_VAR', value: 'env-value', enabled: true } - ] - }; - const request = { - vars: {} - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'runtime-value' - }), - request, - collection.root - ); - }); - - it('should override collection runtime variables with request runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: { - SHARED_VAR: 'request-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: {}, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'request-value' - }), - request, - collection.root - ); - }); - - it('should override global environment with request runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: { - SHARED_VAR: 'request-value' - } - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: {}, - globalEnvironmentVariables: { - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'request-value' - }), - request, - collection.root - ); - }); - - it('should override global environment with collection runtime variables', async () => { - const endpoint = 'https://example.com/'; - const environment = { - variables: [] - }; - const request = { - vars: {} - }; - const collection = { - uid: 'test-collection', - pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, - globalEnvironmentVariables: { - SHARED_VAR: 'global-value' - }, - root: { - request: { - headers: [] - } - } - }; - - await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); - - expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( - endpoint, - expect.objectContaining({ - SHARED_VAR: 'runtime-value' - }), - request, - collection.root - ); - }); - it('should override environment variables with folder-level variables', async () => { const endpoint = 'https://example.com/'; const environment = { @@ -278,9 +84,9 @@ describe('fetchGqlSchemaHandler', () => { ] }; const request = { - vars: {}, - folderVariables: { - SHARED_VAR: 'folder-value' + uid: 'test-request', + vars: { + req: [] // No request variables } }; const collection = { @@ -288,9 +94,37 @@ describe('fetchGqlSchemaHandler', () => { pathname: '/test', runtimeVariables: {}, globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-folder', + type: 'folder', + root: { + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'folder-value', enabled: true } + ] + } + } + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ] + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -307,27 +141,57 @@ describe('fetchGqlSchemaHandler', () => { ); }); - it('should override collection runtime variables with folder-level variables', async () => { + it('should override folder-level variables with request variables', async () => { const endpoint = 'https://example.com/'; const environment = { variables: [] }; const request = { - vars: {}, - folderVariables: { - SHARED_VAR: 'folder-value' + uid: 'test-request', + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] } }; const collection = { uid: 'test-collection', pathname: '/test', - runtimeVariables: { - SHARED_VAR: 'runtime-value' - }, + runtimeVariables: {}, globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-folder', + type: 'folder', + root: { + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'folder-value', enabled: true } + ] + } + } + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + } + } + ] + } + ], root: { request: { - headers: [] + headers: [], + vars: { + req: [] // No collection variables + } } } }; @@ -337,7 +201,111 @@ describe('fetchGqlSchemaHandler', () => { expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( endpoint, expect.objectContaining({ - SHARED_VAR: 'folder-value' + SHARED_VAR: 'request-value' + }), + request, + collection.root + ); + }); + + it('should override global environment variables with collection variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + const request = { + uid: 'test-request', + vars: { + req: [] // No request variables + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: { + SHARED_VAR: 'global-value' + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [ + { name: 'SHARED_VAR', value: 'collection-value', enabled: true } + ] + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'collection-value' + }), + request, + collection.root + ); + }); + + it('should override collection variables with environment variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [ + { name: 'SHARED_VAR', value: 'env-value', enabled: true } + ] + }; + const request = { + uid: 'test-request', + vars: { + req: [] // No request variables + } + }; + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: {}, + globalEnvironmentVariables: {}, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [] // No request variables + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [ + { name: 'SHARED_VAR', value: 'collection-value', enabled: true } + ] + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'env-value' }), request, collection.root From 8d5d9520260a2dad4f4535ec7d39954a1d9aacff Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 27 May 2025 14:38:48 +0530 Subject: [PATCH 886/904] Added runtimeVars in prepareGqlIntrospectionRequest --- .../bruno-electron/src/ipc/network/index.js | 6 +- .../network/fetch-gql-schema-handler.spec.js | 57 ++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 3adb5ea88..506fbfa3c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -331,15 +331,17 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const folderVars = resolvedRequest.folderVariables; const requestVariables = resolvedRequest.requestVariables; const collectionVariables = resolvedRequest.collectionVariables; + const runtimeVars = collection.runtimeVariables; - // Precedence: globalEnvironmentVars < collectionVariables < envVars < folderVars < requestVariables + // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars const resolvedVars = merge( {}, globalEnvironmentVars, collectionVariables, envVars, folderVars, - requestVariables + requestVariables, + runtimeVars ); const collectionRoot = get(collection, 'root', {}); diff --git a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js index fdfff7b89..8831ba48b 100644 --- a/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js +++ b/packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js @@ -15,7 +15,7 @@ jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => { }); }); -describe('fetchGqlSchemaHandler', () => { +describe('fetchGqlSchemaHandler - variable precedence', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -311,6 +311,61 @@ describe('fetchGqlSchemaHandler', () => { collection.root ); }); + + it('should override request variables with runtime variables', async () => { + const endpoint = 'https://example.com/'; + const environment = { + variables: [] + }; + + const request = { + uid: 'test-request', + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + }; + + const collection = { + uid: 'test-collection', + pathname: '/test', + runtimeVariables: { + SHARED_VAR: 'runtime-value' + }, + items: [ + { + uid: 'test-request', + request: { + vars: { + req: [ + { name: 'SHARED_VAR', value: 'request-value', enabled: true } + ] + } + } + } + ], + root: { + request: { + headers: [], + vars: { + req: [] // No collection variables + } + } + } + }; + + await fetchGqlSchemaHandler(null, endpoint, environment, request, collection); + + expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ + SHARED_VAR: 'runtime-value' + }), + request, + collection.root + ); + }) }); From 2de9b87c6fdf4f1107984b715893f4e314eb042f Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 15:30:54 +0530 Subject: [PATCH 887/904] consider errored request as a collection run fail --- packages/bruno-cli/src/commands/run.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 14a058f8d..af4cf4ae3 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -617,7 +617,7 @@ const handler = async function (argv) { } } - if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) { + if ((summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) { process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION); } } catch (err) { From 6f9daadcfbe36e1560a9f95c8a5a719c2611c67f Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 27 May 2025 15:44:07 +0530 Subject: [PATCH 888/904] Update index.js Removed duplicate variable --- packages/bruno-electron/src/ipc/network/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 506fbfa3c..ca196f89b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -327,7 +327,6 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const envVars = getEnvVars(environment); const globalEnvironmentVars = collection.globalEnvironmentVariables; - const collectionRuntimeVars = collection.runtimeVariables; const folderVars = resolvedRequest.folderVariables; const requestVariables = resolvedRequest.requestVariables; const collectionVariables = resolvedRequest.collectionVariables; From d82006937157868de87df6321ec3638fcc4c30ec Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 18:41:47 +0530 Subject: [PATCH 889/904] return the actual axios error with the custom error message in bruno-cli axios-instance --- packages/bruno-cli/src/utils/axios-instance.js | 10 ++++++---- .../collection/redirects/Disable Redirect.bru | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index d3fea8f6d..d0e1ae446 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -92,14 +92,16 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) { if (redirectResponseCodes.includes(error.response.status)) { if (redirectCount >= requestMaxRedirects) { - const err = new Error(`Maximum redirects (${requestMaxRedirects}) exceeded`); - err.originalError = error; - return Promise.reject(err); + // todo: needs to be discussed whether the original error response message should be modified or not + error.response.data = `Maximum redirects (${requestMaxRedirects}) exceeded`; + return Promise.reject(error); } const locationHeader = error.response.headers.location; if (!locationHeader) { - return Promise.reject(new Error('Redirect location header missing')); + // todo: needs to be discussed whether the original error response message should be modified or not + error.response.data = 'Redirect location header missing'; + return Promise.reject(error); } redirectCount++; diff --git a/packages/bruno-tests/collection/redirects/Disable Redirect.bru b/packages/bruno-tests/collection/redirects/Disable Redirect.bru index 31aedd2f2..b915ca36d 100644 --- a/packages/bruno-tests/collection/redirects/Disable Redirect.bru +++ b/packages/bruno-tests/collection/redirects/Disable Redirect.bru @@ -21,6 +21,6 @@ script:pre-request { tests { test("should disable redirect to ping", function() { const data = res.getBody(); - expect(data).to.equal('Found. Redirecting to /ping'); + expect(data).to.not.equal('pong'); }); } From 5627c5624f9f11e1959ed36fcad4b2a2ab1ef9e1 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 19:16:29 +0530 Subject: [PATCH 890/904] updates --- packages/bruno-tests/collection/redirects/Disable Redirect.bru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-tests/collection/redirects/Disable Redirect.bru b/packages/bruno-tests/collection/redirects/Disable Redirect.bru index b915ca36d..560ebe083 100644 --- a/packages/bruno-tests/collection/redirects/Disable Redirect.bru +++ b/packages/bruno-tests/collection/redirects/Disable Redirect.bru @@ -21,6 +21,6 @@ script:pre-request { tests { test("should disable redirect to ping", function() { const data = res.getBody(); - expect(data).to.not.equal('pong'); + expect(data).to.equal('Maximum redirects (0) exceeded'); }); } From bb011998770ad15092b56148a288da0e5c67ccc3 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 19:17:05 +0530 Subject: [PATCH 891/904] updates --- packages/bruno-electron/src/ipc/network/axios-instance.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index e86d06fea..fbb06af13 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -275,6 +275,8 @@ function makeAxiosInstance({ type: 'error', message: safeStringifyJSON(errorResponseData?.toString?.()) }); + // todo: needs to be discussed whether the original error response message should be modified or not + error.response.data = `Maximum redirects (${requestMaxRedirects}) exceeded`; return Promise.reject(error); } From bf196452824d420fbab0f52811f117e3b5d3224a Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 19:40:22 +0530 Subject: [PATCH 892/904] revert test update --- packages/bruno-tests/collection/redirects/Disable Redirect.bru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-tests/collection/redirects/Disable Redirect.bru b/packages/bruno-tests/collection/redirects/Disable Redirect.bru index 560ebe083..31aedd2f2 100644 --- a/packages/bruno-tests/collection/redirects/Disable Redirect.bru +++ b/packages/bruno-tests/collection/redirects/Disable Redirect.bru @@ -21,6 +21,6 @@ script:pre-request { tests { test("should disable redirect to ping", function() { const data = res.getBody(); - expect(data).to.equal('Maximum redirects (0) exceeded'); + expect(data).to.equal('Found. Redirecting to /ping'); }); } From 9ad0f2d169841f0999caee25b17283b94ff79598 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 27 May 2025 19:40:51 +0530 Subject: [PATCH 893/904] revert custom error messages --- packages/bruno-cli/src/utils/axios-instance.js | 2 -- packages/bruno-electron/src/ipc/network/axios-instance.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index d0e1ae446..e919412e7 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -93,14 +93,12 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) { if (redirectResponseCodes.includes(error.response.status)) { if (redirectCount >= requestMaxRedirects) { // todo: needs to be discussed whether the original error response message should be modified or not - error.response.data = `Maximum redirects (${requestMaxRedirects}) exceeded`; return Promise.reject(error); } const locationHeader = error.response.headers.location; if (!locationHeader) { // todo: needs to be discussed whether the original error response message should be modified or not - error.response.data = 'Redirect location header missing'; return Promise.reject(error); } diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index fbb06af13..e86d06fea 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -275,8 +275,6 @@ function makeAxiosInstance({ type: 'error', message: safeStringifyJSON(errorResponseData?.toString?.()) }); - // todo: needs to be discussed whether the original error response message should be modified or not - error.response.data = `Maximum redirects (${requestMaxRedirects}) exceeded`; return Promise.reject(error); } From cb611c65100aab5e6112c08d6ed07ea46bbf214a Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Wed, 28 May 2025 14:41:42 +0530 Subject: [PATCH 894/904] Fix: Special URI characters in proxy username/password is giving error URI-encoding the _username_ and _password_ before creating the proxy URI which then gets passed to `HttpsProxyAgent` and `HttpProxyAgent` respectively. --- packages/bruno-cli/src/runner/run-single-request.js | 4 ++-- packages/bruno-electron/src/utils/proxy-util.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 2d5c5fdea..c660367e8 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -211,8 +211,8 @@ const runSingleRequest = async function ( let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; let proxyUri; if (proxyAuthEnabled) { - const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions); - const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions); + const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions)); + const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions)); proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; } else { diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js index b44c702db..2a9ef26cb 100644 --- a/packages/bruno-electron/src/utils/proxy-util.js +++ b/packages/bruno-electron/src/utils/proxy-util.js @@ -324,8 +324,8 @@ function setupProxyAgents({ let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; let proxyUri; if (proxyAuthEnabled) { - const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions); - const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions); + const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions)); + const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions)); proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; } else { proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; From 6e890018254b9ab74a6e2660838ab1895af1c260 Mon Sep 17 00:00:00 2001 From: lohit Date: Thu, 29 May 2025 17:45:42 +0530 Subject: [PATCH 895/904] fix: collection auth default value access fix and validations --- packages/bruno-app/src/components/CollectionSettings/index.js | 4 ++-- .../Collection/CollectionItem/GenerateCodeItem/index.js | 2 +- packages/bruno-lang/v2/src/collectionBruToJson.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js index 7d5d60574..a4d011be3 100644 --- a/packages/bruno-app/src/components/CollectionSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/index.js @@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => { const requestVars = get(collection, 'root.request.vars.req', []); const responseVars = get(collection, 'root.request.vars.res', []); const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length; - const auth = get(collection, 'root.request.auth', {}).mode; + const authMode = get(collection, 'root.request.auth', {}).mode || 'none'; const proxyConfig = get(collection, 'brunoConfig.proxy', {}); const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []); @@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
setTab('auth')}> Auth - {auth !== 'none' && } + {authMode !== 'none' && }
setTab('script')}> Script diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index 5c5640ca0..f31caf9ab 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -36,7 +36,7 @@ const resolveInheritedAuth = (item, collection) => { const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid); // Default to collection auth - const collectionAuth = get(collection, 'root.request.auth'); + const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' }); let effectiveAuth = collectionAuth; let source = 'collection'; diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 16a0c8d79..e92dcaa88 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -163,7 +163,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { return { auth: { - mode: auth ? auth.mode : 'none' + mode: auth?.mode || 'none' } }; }, From 577d54b43225034a037f909e8583f8dec36011b4 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Thu, 15 May 2025 16:24:30 +0530 Subject: [PATCH 896/904] Added Playwright test for bruno-testbench, few sanity tests and improvements - Trace will capture snapshots now - Added ability to add init Electron user-data, preferences and other app settings. - Improved test Fixtures - Use tempdir for Electron user-data - Ability to reuse app instance for a given init user-data by placing them in a folder(`pageWithUserData` Fixture) - Ability to create tests with fresh user-data(`newPage` Fixture) - Improved logging - Improved the env vars to customize the Electron user-data-path --- contributing.md | 7 +- .../001-sanity-tests/001-home-screen.spec.ts | 5 + .../002-create-new-collection.spec.ts | 31 +++ .../init-user-data/preferences.json | 4 + .../run-testbench-requests.spec.ts | 49 +++++ e2e-tests/test-app-start.spec.ts | 5 - packages/bruno-electron/src/index.js | 13 +- playwright.config.ts | 25 ++- playwright/electron.ts | 8 +- playwright/index.ts | 182 ++++++++++++++++-- 10 files changed, 287 insertions(+), 42 deletions(-) create mode 100644 e2e-tests/001-sanity-tests/001-home-screen.spec.ts create mode 100644 e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts create mode 100644 e2e-tests/bruno-testbench/init-user-data/preferences.json create mode 100644 e2e-tests/bruno-testbench/run-testbench-requests.spec.ts delete mode 100644 e2e-tests/test-app-start.spec.ts diff --git a/contributing.md b/contributing.md index 7656eb5fa..b72d71293 100644 --- a/contributing.md +++ b/contributing.md @@ -99,14 +99,13 @@ npm run dev ``` #### Customize Electron `userData` path -If `ELECTRON_APP_NAME` env-variable is present and its development mode, then the `appName` and `userData` path is modified accordingly. +If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly. e.g. ```sh -ELECTRON_APP_NAME=bruno-dev npm run dev:electron +ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron ``` - -> This doesn't change the name of the window or the names in lot of other places, only the name used by Electron internally. +This will create a `bruno-test` folder on your Desktop and use it as the `userData` path. ### Troubleshooting diff --git a/e2e-tests/001-sanity-tests/001-home-screen.spec.ts b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts new file mode 100644 index 000000000..d993fb7bc --- /dev/null +++ b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts @@ -0,0 +1,5 @@ +import { test, expect } from '../../playwright'; + +test('Check if the logo on top left is visible', async ({ page }) => { + await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible(); +}); \ No newline at end of file diff --git a/e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts b/e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts new file mode 100644 index 000000000..7d1b1e73d --- /dev/null +++ b/e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../../playwright'; + +test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => { + await page.getByLabel('Create Collection').click(); + await page.getByLabel('Name').click(); + await page.getByLabel('Name').fill('test-collection'); + await page.getByLabel('Name').press('Tab'); + await page.getByLabel('Location').fill(await createTmpDir('test-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await page.getByText('test-collection').click(); + await page.getByLabel('Safe ModeBETA').check(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.locator('#create-new-tab').getByRole('img').click(); + await page.getByPlaceholder('Request Name').fill('r1'); + await page.getByPlaceholder('Request URL').click(); + await page.getByPlaceholder('Request URL').fill('http://localhost:8081'); + await page.getByRole('button', { name: 'Create' }).click(); + await page.locator('pre').filter({ hasText: 'http://localhost:' }).click(); + await page.locator('textarea').fill('/ping'); + await page.locator('#send-request').getByRole('img').nth(2).click(); + + await expect(page.getByRole('main')).toContainText('200 OK'); + + await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click(); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + await page.getByText('GETr1').click(); + await page.getByRole('button', { name: 'Clear response' }).click(); + await page.locator('body').press('ControlOrMeta+Enter'); + + await expect(page.getByRole('main')).toContainText('200 OK'); +}); \ No newline at end of file diff --git a/e2e-tests/bruno-testbench/init-user-data/preferences.json b/e2e-tests/bruno-testbench/init-user-data/preferences.json new file mode 100644 index 000000000..4ab7e9620 --- /dev/null +++ b/e2e-tests/bruno-testbench/init-user-data/preferences.json @@ -0,0 +1,4 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"] +} \ No newline at end of file diff --git a/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts b/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts new file mode 100644 index 000000000..f6bca6510 --- /dev/null +++ b/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '../../playwright'; + +test.describe.parallel('Run Testbench Requests', () => { + test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => { + test.setTimeout(2 * 60 * 1000); + + await page.getByText('bruno-testbench').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.locator('.environment-selector').nth(1).click(); + await page.locator('.dropdown-item').getByText('Prod').click(); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const [totalRequests, passed, failed, skipped] = result + .match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/) + .slice(1); + + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1); + }); + + test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => { + test.setTimeout(2 * 60 * 1000); + + await page.getByText('bruno-testbench').click(); + await page.getByLabel('Safe ModeBETA').check(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.locator('.environment-selector').nth(1).click(); + await page.locator('.dropdown-item').getByText('Prod').click(); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const [totalRequests, passed, failed, skipped] = result + .match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/) + .slice(1); + + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1); + }); +}); \ No newline at end of file diff --git a/e2e-tests/test-app-start.spec.ts b/e2e-tests/test-app-start.spec.ts deleted file mode 100644 index 891c7ce3b..000000000 --- a/e2e-tests/test-app-start.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { test, expect } from '../playwright'; - -test('test-app-start', async ({ page }) => { - await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible(); -}); \ No newline at end of file diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 4ed47352c..436403a49 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -14,16 +14,11 @@ const { format } = require('url'); const { BrowserWindow, app, session, Menu, ipcMain } = require('electron'); const { setContentSecurityPolicy } = require('electron-util'); -if (isDev && process.env.ELECTRON_APP_NAME) { - const appName = process.env.ELECTRON_APP_NAME; - const userDataPath = path.join(app.getPath("appData"), appName); +if (isDev && process.env.ELECTRON_USER_DATA_PATH) { + console.debug("`ELECTRON_USER_DATA_PATH` found, modifying `userData` path: \n" + + `\t${app.getPath("userData")} -> ${process.env.ELECTRON_USER_DATA_PATH}`); - console.log("`ELECTRON_APP_NAME` found, overriding `appName` and `userData` path: \n" - + `\t${app.getName()} -> ${appName}\n` - + `\t${app.getPath("userData")} -> ${userDataPath}`); - - app.setName(appName); - app.setPath("userData", userDataPath); + app.setPath('userData', process.env.ELECTRON_USER_DATA_PATH); } const menuTemplate = require('./app/menu-template'); diff --git a/playwright.config.ts b/playwright.config.ts index 684477e40..df01a2ec8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,12 +1,11 @@ import { defineConfig, devices } from '@playwright/test'; -const reporter: string[][string] = [['list'], ['html']]; +const reporter: any[] = [['list'], ['html']]; if (process.env.CI) { - reporter.push(["github"]); + reporter.push(['github']); } - export default defineConfig({ testDir: './e2e-tests', fullyParallel: false, @@ -14,8 +13,9 @@ export default defineConfig({ retries: process.env.CI ? 1 : 0, workers: process.env.CI ? undefined : 1, reporter, + use: { - trace: 'on-first-retry' + trace: process.env.CI ? 'on-first-retry' : 'on' }, projects: [ @@ -24,9 +24,16 @@ export default defineConfig({ } ], - webServer: { - command: 'npm run dev:web', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI - } + webServer: [ + { + command: 'npm run dev:web', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI + }, + { + command: 'npm start --workspace=packages/bruno-tests', + url: 'http://localhost:8081/ping', + reuseExistingServer: !process.env.CI + } + ] }); diff --git a/playwright/electron.ts b/playwright/electron.ts index bc49363f1..4363f46e0 100644 --- a/playwright/electron.ts +++ b/playwright/electron.ts @@ -7,7 +7,11 @@ exports.startApp = async () => { const app = await electron.launch({ args: [electronAppPath] }); const context = await app.context(); - app.process().stdout.on('data', (data) => console.log(data.toString())); - app.process().stderr.on('data', (error) => console.error(error.toString())); + app.process().stdout.on('data', (data) => { + process.stdout.write(data.toString().replace(/^(?=.)/gm, '[Electron] |')); + }); + app.process().stderr.on('data', (error) => { + process.stderr.write(error.toString().replace(/^(?=.)/gm, '[Electron] |')); + }); return { app, context }; }; diff --git a/playwright/index.ts b/playwright/index.ts index ca865437d..549086326 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -1,23 +1,179 @@ -import { test as baseTest, ElectronApplication, Page } from '@playwright/test'; +import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test'; +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; -const { startApp } = require('./electron.ts'); +const electronAppPath = path.join(__dirname, '../packages/bruno-electron'); -export const test = baseTest.extend<{ page: Page }, { electronApp: ElectronApplication }>({ - electronApp: [ +export const test = baseTest.extend< + { + context: BrowserContext; + page: Page; + newPage: Page; + pageWithUserData: Page; + }, + { + createTmpDir: (tag?: string) => Promise; + launchElectronApp: (options?: { initUserDataPath?: string }) => Promise; + electronApp: ElectronApplication; + reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string }) => Promise; + } +>({ + createTmpDir: [ async ({}, use) => { - const { app: electronApp, context } = await startApp(); - - await use(electronApp); - await context.close(); - await electronApp.close(); + const dirs: string[] = []; + await use(async (tag?: string) => { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `pw-${tag || ''}-`)); + dirs.push(dir); + return dir; + }); + await Promise.all( + dirs.map((dir) => fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch((e) => e)) + ); }, { scope: 'worker' } ], - page: async ({ electronApp }, use) => { + + launchElectronApp: [ + async ({ playwright, createTmpDir }, use, workerInfo) => { + const apps: ElectronApplication[] = []; + await use(async ({ initUserDataPath } = {}) => { + const userDataPath = await createTmpDir('electron-userdata'); + + if (initUserDataPath) { + const replacements = { + projectRoot: path.join(__dirname, '..') + }; + + for (const file of await fs.promises.readdir(initUserDataPath)) { + let content = await fs.promises.readFile(path.join(initUserDataPath, file), 'utf-8'); + content = content.replace(/{{(\w+)}}/g, (_, key) => { + if (replacements[key]) { + return replacements[key]; + } else { + throw new Error(`\tNo replacement for {{${key}}} in ${path.join(initUserDataPath, file)}`); + } + }); + await fs.promises.writeFile(path.join(userDataPath, file), content, 'utf-8'); + } + } + + const app = await playwright._electron.launch({ + args: [electronAppPath], + env: { + ...process.env, + ELECTRON_USER_DATA_PATH: userDataPath, + } + }); + + const { workerIndex } = workerInfo; + app.process().stdout.on('data', (data) => { + process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); + }); + app.process().stderr.on('data', (error) => { + process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`)); + }); + + apps.push(app); + return app; + }); + for (const app of apps) { + await app.context().close(); + await app.close(); + } + }, + { scope: 'worker' } + ], + + electronApp: [ + async ({ launchElectronApp }, use) => { + const app = await launchElectronApp(); + await use(app); + }, + { scope: 'worker' } + ], + + context: async ({ electronApp }, use, testInfo) => { + const context = await electronApp.context(); + const tracingOptions = (testInfo as any)._tracing.traceOptions(); + if (tracingOptions) { + try { + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + } catch (e) {} + } + await use(context); + }, + + page: async ({ electronApp, context }, use, testInfo) => { const page = await electronApp.firstWindow(); - await use(page); - await page.reload(); + const tracingOptions = (testInfo as any)._tracing.traceOptions(); + if (tracingOptions) { + const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); + await context.tracing.startChunk(); + await use(page); + await context.tracing.stopChunk({ path: tracePath }); + await testInfo.attach('trace', { path: tracePath }); + } else { + await use(page); + } + }, + + newPage: async ({ launchElectronApp }, use, testInfo) => { + const app = await launchElectronApp(); + const context = await app.context(); + const page = await app.firstWindow(); + const tracingOptions = (testInfo as any)._tracing.traceOptions(); + if (tracingOptions) { + const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + await use(page); + await context.tracing.stop({ path: tracePath }); + await testInfo.attach('trace', { path: tracePath }); + } else { + await use(page); + } + }, + + reuseOrLaunchElectronApp: [ + async ({ launchElectronApp }, use, testInfo) => { + const apps: Record = {}; + await use(async ({ initUserDataPath } = {}) => { + const key = initUserDataPath; + if (key && apps[key]) { + return apps[key]; + } + const app = await launchElectronApp({ initUserDataPath }); + apps[key] = app; + return app; + }); + }, + { scope: 'worker' } + ], + + pageWithUserData: async ({ reuseOrLaunchElectronApp }, use, testInfo) => { + const testDir = path.dirname(testInfo.file); + const initUserDataPath = path.join(testDir, 'init-user-data'); + + const app = await reuseOrLaunchElectronApp( + (await fs.promises.stat(initUserDataPath).catch(() => false)) ? { initUserDataPath } : {} + ); + + const context = await app.context(); + const page = await app.firstWindow(); + const tracingOptions = (testInfo as any)._tracing.traceOptions(); + if (tracingOptions) { + const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); + try { + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + } catch (e) {} + await context.tracing.startChunk(); + await use(page); + await context.tracing.stopChunk({ path: tracePath }); + await testInfo.attach('trace', { path: tracePath }); + } else { + await use(page); + } } }); -export * from '@playwright/test' +export * from '@playwright/test'; From a006fe82303b4d42c8dcd8182ceeef518ce11c02 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Tue, 20 May 2025 19:43:24 +0530 Subject: [PATCH 897/904] Move Playwright tests to Tests Workflow itself Currently the test-results and annotations form jobs are getting added to random Workflow and there is no fix for it right now Ref: https://github.com/EnricoMi/publish-unit-test-result-action/issues/12 Also Playwright tests have the same triggers as Tests, so no need to keep it separate. --- .github/workflows/playwright.yml | 44 -------------------------------- .github/workflows/tests.yml | 38 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/playwright.yml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index f630b56cb..000000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Playwright E2E Tests -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - e2e-test: - timeout-minutes: 60 - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: v22.11.x - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get --no-install-recommends install -y \ - libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ - xvfb - npm ci --legacy-peer-deps - sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox - sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox - - - name: Build libraries - run: | - npm run build:graphql-docs - npm run build:bruno-query - npm run build:bruno-common - npm run sandbox:bundle-libraries --workspace=packages/bruno-js - npm run build:bruno-converters - npm run build:bruno-requests - - - name: Run Playwright tests - run: | - xvfb-run npm run test:e2e - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 144cdee5b..44ff08cce 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -91,5 +91,43 @@ jobs: uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: + check_name: CLI Test Results files: packages/bruno-tests/collection/junit.xml comment_mode: always + e2e-test: + name: Playwright E2E Tests + timeout-minutes: 60 + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: v22.11.x + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get --no-install-recommends install -y \ + libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ + xvfb + npm ci --legacy-peer-deps + sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + + - name: Build libraries + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + npm run sandbox:bundle-libraries --workspace=packages/bruno-js + npm run build:bruno-converters + npm run build:bruno-requests + + - name: Run Playwright tests + run: | + xvfb-run npm run test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 From 1cc94e8ffe8f5329730fbb7e5cc384d9b2fc3182 Mon Sep 17 00:00:00 2001 From: Maintainer Bruno Date: Wed, 4 Jun 2025 16:56:22 +0530 Subject: [PATCH 898/904] feat(dev): enhance hot reload development setup --- package.json | 1 + packages/bruno-graphql-docs/package.json | 3 +- packages/bruno-query/package.json | 1 + packages/bruno-requests/package.json | 1 + scripts/dev-hot-reload.js | 240 +++++++++++++++++++++++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 scripts/dev-hot-reload.js diff --git a/package.json b/package.json index aba14755d..55873fff4 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "setup": "node ./scripts/setup.js", "watch:converters": "npm run watch --workspace=packages/bruno-converters", "dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"", + "dev:watch": "node ./scripts/dev-hot-reload.js", "dev:web": "npm run dev --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app", diff --git a/packages/bruno-graphql-docs/package.json b/packages/bruno-graphql-docs/package.json index fde24713b..548f212c9 100644 --- a/packages/bruno-graphql-docs/package.json +++ b/packages/bruno-graphql-docs/package.json @@ -9,7 +9,8 @@ "package.json" ], "scripts": { - "build": "rollup -c" + "build": "rollup -c", + "watch": "rollup -c -w" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", diff --git a/packages/bruno-query/package.json b/packages/bruno-query/package.json index 437513a78..fda74c693 100644 --- a/packages/bruno-query/package.json +++ b/packages/bruno-query/package.json @@ -15,6 +15,7 @@ "test": "jest", "prebuild": "npm run clean", "build": "rollup -c", + "watch": "rollup -c -w", "prepack": "npm run test && npm run build" }, "devDependencies": { diff --git a/packages/bruno-requests/package.json b/packages/bruno-requests/package.json index 1b4fc8311..1aed11446 100644 --- a/packages/bruno-requests/package.json +++ b/packages/bruno-requests/package.json @@ -14,6 +14,7 @@ "clean": "rimraf dist", "prebuild": "npm run clean", "build": "rollup -c", + "watch": "rollup -c -w", "prepack": "npm run test && npm run build" }, "devDependencies": { diff --git a/scripts/dev-hot-reload.js b/scripts/dev-hot-reload.js new file mode 100644 index 000000000..52440f279 --- /dev/null +++ b/scripts/dev-hot-reload.js @@ -0,0 +1,240 @@ +#!/usr/bin/env node + +/** +# Bruno Development Script +# +# This script sets up and runs the Bruno development environment with hot-reloading. +# It manages concurrent processes for various packages and provides cleanup on exit. +# +# Usage: +# From the root of the project, run: +# node ./scripts/dev-hot-reload.js [options] +# or +# npm run dev:watch -- [options] +*/ + +const { execSync } = require('child_process'); +const { readFileSync } = require('fs'); + +// Get major version from .nvmrc (e.g. v22.1.0 -> v22) +const NODE_VERSION = readFileSync('.nvmrc', 'utf8').trim().split('.')[0]; + +// Configuration +const CONFIG = { + NODE_VERSION, + ELECTRON_WATCH_PATHS: [ + 'packages/**/dist/', + 'packages/bruno-electron/src/', + 'packages/bruno-lang/src/', + 'packages/bruno-lang/v2/src/', + 'packages/bruno-js/src/', + 'packages/bruno-schema/src/' + ], + ELECTRON_START_DELAY: 10, // seconds + NODEMON_WATCH_DELAY: 1000 // milliseconds +}; + +const COLORS = { + red: '\x1b[0;31m', + green: '\x1b[0;32m', + yellow: '\x1b[1;33m', + blue: '\x1b[0;34m', + nc: '\x1b[0m' // No Color +}; + +const LOG_LEVELS = { + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR', + DEBUG: 'DEBUG', + SUCCESS: 'SUCCESS' +}; + +function log(level, msg) { + let color = COLORS.nc; + switch (level) { + case LOG_LEVELS.INFO: + case LOG_LEVELS.SUCCESS: color = COLORS.green; break; + case LOG_LEVELS.WARN: color = COLORS.yellow; break; + case LOG_LEVELS.ERROR: color = COLORS.red; break; + case LOG_LEVELS.DEBUG: color = COLORS.blue; break; + } + + const output = `${color}[${level}]${COLORS.nc} ${msg}`; + if (level === LOG_LEVELS.ERROR) { + console.error(output); + } else { + console.log(output); + } +} + +// Show help documentation +function showHelp() { + console.log(` + Development Environment Setup for Bruno + + Usage: + From the root of the project, run: + npm run dev:watch -- [options] + or + node scripts/dev-hot-reload.js [options] + + Options: + -s, --setup Clean all node_modules folders and re-install dependencies before starting + -h, --help Show this help message + + Examples: + # Start development environment + npm run dev:watch + + # Start after cleaning node_modules + npm run dev:watch -- --setup + + # Show this help + npm run dev:watch -- --help +`); +} + +function commandExists(command) { + try { + execSync(`command -v ${command}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +// Install global NPM package if not present +function ensureGlobalPackage(packageName) { + if (!commandExists(packageName)) { + log(LOG_LEVELS.INFO, `Installing ${packageName} globally...`); + execSync(`npm install -g ${packageName}`, { stdio: 'inherit' }); + } +} + +// Ensure correct node version +function ensureNodeVersion(requiredVersion) { + const currentVersion = process.version; + if (!currentVersion.includes(requiredVersion)) { + log(LOG_LEVELS.ERROR, `Node ${requiredVersion} is required but currently installed version is ${currentVersion}`); + log(LOG_LEVELS.ERROR, `Please install node ${requiredVersion} and try again.`); + log(LOG_LEVELS.ERROR, `You can run 'nvm install ${requiredVersion}' to install it, or 'nvm use ${requiredVersion}' if it's already installed.`); + + process.exit(1); + } +} + +function cleanNodeModules() { + log(LOG_LEVELS.INFO, 'Removing all node_modules directories...'); + execSync('find . -name "node_modules" -type d -prune -exec rm -rf {} +', { stdio: 'inherit' }); + log(LOG_LEVELS.SUCCESS, 'Node modules cleanup completed'); +} + +function reinstallDependencies() { + log(LOG_LEVELS.INFO, 'Re-installing dependencies...'); + execSync('npm install --legacy-peer-deps', { stdio: 'inherit' }); + log(LOG_LEVELS.SUCCESS, 'Dependencies re-installation completed'); +} + +// Setup development environment +function startDevelopment() { + log(LOG_LEVELS.INFO, 'Starting development servers...'); + + const concurrently = require('concurrently'); + const watchPaths = CONFIG.ELECTRON_WATCH_PATHS.map(path => `--watch "${path}"`).join(' '); + + // concurrently command objects: { command, name, prefixColor, env, cwd, ipc } + const commandObjects = [ + { + command: 'npm run watch --workspace=packages/bruno-common', + name: 'common', + prefixColor: 'magenta' + }, + { + command: 'npm run watch --workspace=packages/bruno-converters', + name: 'converters', + prefixColor: 'green' + }, + { + command: 'npm run watch --workspace=packages/bruno-query', + name: 'query', + prefixColor: 'blue' + }, + { + command: 'npm run watch --workspace=packages/bruno-graphql-docs', + name: 'graphql', + prefixColor: 'white' + }, + { + command: 'npm run watch --workspace=packages/bruno-requests', + name: 'requests', + prefixColor: 'gray' + }, + { + command: 'npm run dev:web', + name: 'react', + prefixColor: 'cyan' + }, + { + command: `sleep ${CONFIG.ELECTRON_START_DELAY} && nodemon ${watchPaths} --ext js,jsx,ts,tsx --delay ${CONFIG.NODEMON_WATCH_DELAY}ms --exec "npm run dev --workspace=packages/bruno-electron"`, + name: 'electron', + prefixColor: 'yellow', + delay: CONFIG.ELECTRON_START_DELAY + } + ]; + + const { result } = concurrently(commandObjects, { + prefix: '[{name}: {pid}]', + killOthers: ['failure', 'success'], + restartTries: 3, + restartDelay: 1000 + }); + + result + .then(() => log(LOG_LEVELS.SUCCESS, 'All processes completed successfully')) + .catch(err => { + log(LOG_LEVELS.ERROR, 'Development environment failed to start'); + console.error(err); + process.exit(1); + }); +} + +// Main function +(async function main() { + const args = process.argv.slice(2); + let runSetup = false; + + // Parse command line arguments + for (const arg of args) { + if (arg === '-s' || arg === '--setup') { + runSetup = true; + } else if (arg === '-h' || arg === '--help') { + showHelp(); + process.exit(0); + } else { + log(LOG_LEVELS.ERROR, `Unknown parameter: ${arg}`); + showHelp(); + process.exit(1); + } + } + + log(LOG_LEVELS.INFO, 'Initializing Bruno development environment...'); + + // Ensure required global packages and node version + ensureNodeVersion(CONFIG.NODE_VERSION); + ensureGlobalPackage('nodemon'); + ensureGlobalPackage('concurrently'); + + // Run setup if requested + if (runSetup) { + cleanNodeModules(); + reinstallDependencies(); + } + + // Start development environment + startDevelopment(); +})().catch(err => { + log(LOG_LEVELS.ERROR, 'An error occurred:'); + console.error(err); + process.exit(1); +}); \ No newline at end of file From 1089a52171c403c2b09f8822ff255680f147dee3 Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Fri, 6 Jun 2025 01:54:01 +0530 Subject: [PATCH 899/904] Tests for responseSize component (#4750) --------- Co-authored-by: lohit --- package-lock.json | 2990 ++++++++++++++++- packages/bruno-app/babel.config.js | 9 + packages/bruno-app/jest.config.js | 11 +- packages/bruno-app/package.json | 13 +- .../ResponseSize/ResponseSize.spec.js | 110 + 5 files changed, 3071 insertions(+), 62 deletions(-) create mode 100644 packages/bruno-app/babel.config.js create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseSize/ResponseSize.spec.js diff --git a/package-lock.json b/package-lock.json index 9e78ac936..fe4a15fd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,13 @@ "ts-jest": "^29.2.6" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -1451,14 +1458,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -1527,13 +1534,13 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1543,12 +1550,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1595,6 +1602,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1612,6 +1620,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1628,6 +1637,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1645,6 +1655,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -1661,13 +1672,13 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1703,9 +1714,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1715,6 +1726,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1759,27 +1771,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1789,6 +1801,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -1813,12 +1826,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -1831,6 +1844,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1847,6 +1861,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1862,6 +1877,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1877,6 +1893,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1894,6 +1911,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1928,6 +1946,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2026,6 +2045,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2041,6 +2061,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2079,12 +2100,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2222,6 +2243,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -2238,6 +2260,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2253,6 +2276,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2270,6 +2294,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -2287,6 +2312,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2302,6 +2328,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2333,6 +2360,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -2349,6 +2377,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2369,6 +2398,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2378,6 +2408,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2394,6 +2425,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2409,6 +2441,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2425,6 +2458,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2440,6 +2474,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2456,6 +2491,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2471,6 +2507,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2486,6 +2523,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2517,6 +2555,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2533,6 +2572,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2550,6 +2590,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2565,6 +2606,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2580,6 +2622,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2595,6 +2638,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2610,6 +2654,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2642,6 +2687,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2660,6 +2706,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2676,6 +2723,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2692,6 +2740,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2722,6 +2771,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2737,6 +2787,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2754,6 +2805,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2770,6 +2822,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2801,6 +2854,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2832,6 +2886,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2849,6 +2904,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2860,10 +2916,80 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", + "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2880,6 +3006,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2896,6 +3023,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2911,6 +3039,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2926,6 +3055,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2942,6 +3072,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2957,6 +3088,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2972,6 +3104,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3006,6 +3139,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3021,6 +3155,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3037,6 +3172,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3053,6 +3189,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3069,6 +3206,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.0", @@ -3169,6 +3307,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -3179,6 +3318,27 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", + "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-typescript": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", @@ -3261,30 +3421,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -3325,13 +3485,13 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -8049,6 +8209,125 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@tippyjs/react": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", @@ -8082,6 +8361,13 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -8269,6 +8555,44 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8376,6 +8700,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/react-redux": { "version": "7.1.34", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", @@ -8421,6 +8755,13 @@ "@types/estree": "*" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -8834,6 +9175,14 @@ "dev": true, "license": "MIT" }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -8885,6 +9234,17 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9263,6 +9623,33 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -9619,6 +10006,7 @@ "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", @@ -9633,6 +10021,7 @@ "version": "0.10.6", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", @@ -9646,6 +10035,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" @@ -11709,6 +12099,7 @@ "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.24.2" @@ -12129,6 +12520,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -12234,6 +12632,33 @@ "node": ">=8.0.0" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -12252,6 +12677,68 @@ "node": ">=0.10" } }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -12304,6 +12791,13 @@ "ms": "2.0.0" } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -12382,6 +12876,46 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -12732,6 +13266,13 @@ "redux": "^4.2.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -12792,6 +13333,30 @@ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "license": "BSD-2-Clause" }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", @@ -13387,6 +13952,34 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", @@ -13442,6 +14035,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint": { "version": "9.26.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", @@ -13809,6 +14434,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -14976,6 +15602,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -15093,6 +15729,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -15431,6 +16081,19 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -15595,6 +16258,32 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -16220,6 +16909,21 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -16269,12 +16973,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -16287,6 +17025,23 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -16317,6 +17072,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -16328,6 +17084,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -16434,6 +17207,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -16467,6 +17253,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -16488,6 +17291,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-primitive": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", @@ -16514,6 +17324,25 @@ "@types/estree": "*" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -16523,6 +17352,35 @@ "node": ">=0.10.0" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -16536,6 +17394,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -16583,6 +17476,36 @@ "node": ">=0.10.0" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -17153,6 +18076,34 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -17864,6 +18815,205 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -18343,6 +19493,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { @@ -18492,6 +19643,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", @@ -19047,6 +20208,16 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", @@ -19751,6 +20922,13 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -20329,6 +21507,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -22488,6 +23667,20 @@ "node": ">= 0.10" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reduce-configs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reduce-configs/-/reduce-configs-1.1.0.tgz", @@ -22517,12 +23710,14 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -22541,15 +23736,38 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -22567,12 +23785,14 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -22585,6 +23805,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -22758,6 +23979,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -23259,6 +24481,24 @@ "dev": true, "license": "ISC" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -23730,6 +24970,19 @@ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -23928,6 +25181,22 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", @@ -24446,6 +25715,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", @@ -24682,6 +25965,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", @@ -24965,6 +26261,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -25005,6 +26302,13 @@ "node": ">= 10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/sync-child-process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", @@ -25815,6 +27119,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -25824,6 +27129,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -25837,6 +27143,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -25846,6 +27153,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -26153,6 +27461,19 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -26379,6 +27700,45 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", @@ -26483,6 +27843,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-formatter": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.3.tgz", @@ -26495,6 +27877,16 @@ "node": ">= 16" } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, "node_modules/xml-parser-xo": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.3.tgz", @@ -26535,6 +27927,13 @@ "node": ">=8.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -26668,8 +28067,8 @@ "packages/bruno-app": { "name": "@usebruno/app", "version": "2.0.0", + "license": "MIT", "dependencies": { - "@babel/preset-env": "^7.26.0", "@fontsource/inter": "^5.0.15", "@prantlf/jsonlint": "^16.0.0", "@reduxjs/toolkit": "^1.8.0", @@ -26740,19 +28139,28 @@ "yup": "^0.32.11" }, "devDependencies": { + "@babel/core": "^7.27.1", + "@babel/preset-env": "^7.27.2", + "@babel/preset-react": "^7.27.1", "@rsbuild/core": "^1.1.2", "@rsbuild/plugin-babel": "^1.0.3", "@rsbuild/plugin-node-polyfill": "^1.2.0", "@rsbuild/plugin-react": "^1.0.7", "@rsbuild/plugin-sass": "^1.1.0", "@rsbuild/plugin-styled-components": "1.1.0", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", "autoprefixer": "10.4.20", + "babel-jest": "^29.7.0", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", + "babel-plugin-styled-components": "^2.1.4", "cross-env": "^7.0.3", "css-loader": "7.1.2", "file-loader": "^6.2.0", "html-loader": "^3.0.1", "html-webpack-plugin": "^5.5.0", + "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.4.5", "postcss": "8.4.47", "style-loader": "^3.3.1", @@ -26761,6 +28169,1383 @@ "webpack-cli": "^4.9.1" } }, + "packages/bruno-app/node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "packages/bruno-app/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/bruno-app/node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/bruno-app/node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/bruno-app/node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/bruno-app/node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-classes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-destructuring": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", + "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-regenerator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "packages/bruno-app/node_modules/@babel/preset-env": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "packages/bruno-app/node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "packages/bruno-app/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "packages/bruno-app/node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "packages/bruno-app/node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "packages/bruno-app/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -26770,6 +29555,55 @@ "node": ">= 0.6" } }, + "packages/bruno-app/node_modules/core-js-compat": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "packages/bruno-app/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/bruno-app/node_modules/electron-to-chromium": { + "version": "1.5.157", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", + "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", + "dev": true, + "license": "ISC" + }, + "packages/bruno-app/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "packages/bruno-app/node_modules/jsonpath-plus": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", @@ -26788,6 +29622,13 @@ "node": ">=18.0.0" } }, + "packages/bruno-app/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "packages/bruno-app/node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -26817,6 +29658,37 @@ "node": ">=10" } }, + "packages/bruno-app/node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "packages/bruno-cli": { "name": "@usebruno/cli", "version": "1.16.0", diff --git a/packages/bruno-app/babel.config.js b/packages/bruno-app/babel.config.js new file mode 100644 index 000000000..e04b84e5e --- /dev/null +++ b/packages/bruno-app/babel.config.js @@ -0,0 +1,9 @@ +module.exports = { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { + runtime: 'automatic' + }] + ], + plugins: ['babel-plugin-styled-components'] +}; \ No newline at end of file diff --git a/packages/bruno-app/jest.config.js b/packages/bruno-app/jest.config.js index 5d94a67b7..6a59e2d4c 100644 --- a/packages/bruno-app/jest.config.js +++ b/packages/bruno-app/jest.config.js @@ -12,5 +12,14 @@ module.exports = { }, clearMocks: true, moduleDirectories: ['node_modules', 'src'], - testEnvironment: 'node' + testEnvironment: 'jsdom', + transform: { + '^.+\\.[jt]sx?$': 'babel-jest' + }, + transformIgnorePatterns: [ + '/node_modules/(?!(nanoid|xml-formatter)/)' + ], + testMatch: [ + '/src/**/*.spec.[jt]s?(x)' + ] }; diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 2ed9f445e..51db3c032 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -1,6 +1,7 @@ { "name": "@usebruno/app", "version": "2.0.0", + "license": "MIT", "private": true, "scripts": { "dev": "rsbuild dev", @@ -11,7 +12,6 @@ "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" }, "dependencies": { - "@babel/preset-env": "^7.26.0", "@fontsource/inter": "^5.0.15", "@prantlf/jsonlint": "^16.0.0", "@reduxjs/toolkit": "^1.8.0", @@ -82,19 +82,28 @@ "yup": "^0.32.11" }, "devDependencies": { + "@babel/core": "^7.27.1", + "@babel/preset-env": "^7.27.2", + "@babel/preset-react": "^7.27.1", "@rsbuild/core": "^1.1.2", "@rsbuild/plugin-babel": "^1.0.3", "@rsbuild/plugin-node-polyfill": "^1.2.0", "@rsbuild/plugin-react": "^1.0.7", "@rsbuild/plugin-sass": "^1.1.0", "@rsbuild/plugin-styled-components": "1.1.0", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", "autoprefixer": "10.4.20", + "babel-jest": "^29.7.0", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", + "babel-plugin-styled-components": "^2.1.4", "cross-env": "^7.0.3", "css-loader": "7.1.2", "file-loader": "^6.2.0", "html-loader": "^3.0.1", "html-webpack-plugin": "^5.5.0", + "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.4.5", "postcss": "8.4.47", "style-loader": "^3.3.1", @@ -102,4 +111,4 @@ "webpack": "^5.64.4", "webpack-cli": "^4.9.1" } -} +} \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSize/ResponseSize.spec.js b/packages/bruno-app/src/components/ResponsePane/ResponseSize/ResponseSize.spec.js new file mode 100644 index 000000000..ef46d4361 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSize/ResponseSize.spec.js @@ -0,0 +1,110 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import ResponseSize from './index'; + +// Create minimal theme with only the properties needed for the component +const theme = { + requestTabPanel: { + responseStatus: '#666' + } +}; + +// Wrap component with theme provider for styled-components +const renderWithTheme = (component) => { + return render( + + {component} + + ); +}; + +describe('ResponseSize', () => { + describe('Invalid or excluded size values', () => { + it('should not render when size is undefined', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is null', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is NaN', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is Infinity', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is -Infinity', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is a string', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render when size is an object', () => { + const { container } = renderWithTheme(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe('Valid size values', () => { + it('should handle zero bytes', () => { + renderWithTheme(); + const element = screen.getByText(/0B/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^0B$/); + expect(element).toHaveAttribute('title', '0B'); + }); + + it('should render bytes when size is less than 1024', () => { + renderWithTheme(); + const element = screen.getByText(/500B/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^500B$/); + expect(element).toHaveAttribute('title', '500B'); + }); + + it('should handle exactly 1024 bytes as size', () => { + renderWithTheme(); + const element = screen.getByText(/1024B/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^1024B$/); + expect(element).toHaveAttribute('title', '1,024B'); + }); + + it('should render kilobytes when size is greater than 1024', () => { + renderWithTheme(); + const element = screen.getByText(/1\.46KB/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^\d+\.\d+KB$/); + expect(element).toHaveAttribute('title', '1,500B'); + }); + + it('should handle large size numbers', () => { + renderWithTheme(); + const element = screen.getByText(/10\.0KB/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^\d+\.\d+KB$/); + expect(element).toHaveAttribute('title', '10,240B'); + }); + + it('should handle decimal size numbers', () => { + renderWithTheme(); + const element = screen.getByText(/1\.10KB/); + expect(element).toBeInTheDocument(); + expect(element.textContent).toMatch(/^\d+\.\d+KB$/); + expect(element).toHaveAttribute('title', '1,126.5B'); + }); + }); +}); \ No newline at end of file From e4ae857df35a4944b6817df5810260fdde17e808 Mon Sep 17 00:00:00 2001 From: Pooja Date: Mon, 9 Jun 2025 13:50:25 +0530 Subject: [PATCH 900/904] Merge pull request #4693 from pooja-bruno/mv/isValidValue-in-common-file Fixed a bug causing secrets to appear as null instead of an empty value. rm isValidValue and directly handle it in encryptString and `decryptString` function --- .../src/components/SingleLineEditor/index.js | 4 ++-- packages/bruno-electron/src/store/env-secrets.js | 6 +----- .../src/store/global-environments.js | 8 ++------ packages/bruno-electron/src/utils/encryption.js | 8 +++++++- .../tests/utils/encryption.spec.js | 12 +++++++++++- packages/bruno-lang/v2/src/envToJson.js | 2 +- packages/bruno-lang/v2/tests/envToJson.spec.js | 16 ++++++++-------- 7 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 99084602f..e34085657 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -83,7 +83,7 @@ class SingleLineEditor extends Component { } }); } - this.editor.setValue(String(this.props.value) || ''); + this.editor.setValue(String(this.props.value ?? '')); this.editor.on('change', this._onEdit); this.addOverlay(variables); this._enableMaskedEditor(this.props.isSecret); @@ -129,7 +129,7 @@ class SingleLineEditor extends Component { } if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) { this.cachedValue = String(this.props.value); - this.editor.setValue(String(this.props.value) || ''); + this.editor.setValue(String(this.props.value ?? '')); } if (!isEqual(this.props.isSecret, prevProps.isSecret)) { // If the secret flag has changed, update the editor to reflect the change diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js index 8ded05ae9..894f7bc7a 100644 --- a/packages/bruno-electron/src/store/env-secrets.js +++ b/packages/bruno-electron/src/store/env-secrets.js @@ -27,17 +27,13 @@ class EnvironmentSecretsStore { }); } - isValidValue(val) { - return typeof val === 'string' && val.length >= 0; - } - storeEnvSecrets(collectionPathname, environment) { const envVars = []; _.each(environment.variables, (v) => { if (v.secret) { envVars.push({ name: v.name, - value: this.isValidValue(v.value) ? encryptString(v.value) : '' + value: encryptString(v.value) }); } }); diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js index e8c03c434..f9e9f175f 100644 --- a/packages/bruno-electron/src/store/global-environments.js +++ b/packages/bruno-electron/src/store/global-environments.js @@ -10,15 +10,11 @@ class GlobalEnvironmentsStore { }); } - isValidValue(val) { - return typeof val === 'string' && val.length >= 0; - } - encryptGlobalEnvironmentVariables({ globalEnvironments }) { return globalEnvironments?.map(env => { const variables = env.variables?.map(v => ({ ...v, - value: v?.secret ? (this.isValidValue(v.value) ? encryptString(v.value) : '') : v?.value + value: v?.secret ? encryptString(v.value) : v?.value })) || []; return { @@ -32,7 +28,7 @@ class GlobalEnvironmentsStore { return globalEnvironments?.map(env => { const variables = env.variables?.map(v => ({ ...v, - value: v?.secret ? (this.isValidValue(v.value) ? decryptString(v.value) : '') : v?.value + value: v?.secret ? decryptString(v.value) : v?.value })) || []; return { diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js index 36c53e7ef..7e7b0b4b7 100644 --- a/packages/bruno-electron/src/utils/encryption.js +++ b/packages/bruno-electron/src/utils/encryption.js @@ -89,6 +89,9 @@ function encryptString(str) { if (typeof str !== 'string') { throw new Error('Encrypt failed: invalid string'); } + if (str.length === 0) { + return ''; + } let encryptedString = ''; @@ -104,9 +107,12 @@ function encryptString(str) { } function decryptString(str) { - if (!str) { + if (typeof str !== 'string') { throw new Error('Decrypt failed: unrecognized string format'); } + if (str.length === 0) { + return ''; + } // Find the index of the first colon const colonIndex = str.indexOf(':'); diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js index 44388fb07..ae13e8ee2 100644 --- a/packages/bruno-electron/tests/utils/encryption.spec.js +++ b/packages/bruno-electron/tests/utils/encryption.spec.js @@ -12,13 +12,23 @@ describe('Encryption and Decryption Tests', () => { expect(decrypted).toBe(plaintext); }); + it('should handle empty strings in encryptString', () => { + const result = encryptString(''); + expect(result).toBe(''); + }); + + it('should handle empty strings in decryptString', () => { + const result = decryptString(''); + expect(result).toBe(''); + }); + it('encrypt should throw an error for invalid string', () => { expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string'); + expect(() => encryptString(undefined)).toThrow('Encrypt failed: invalid string'); }); it('decrypt should throw an error for invalid string', () => { expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format'); - expect(() => decryptString('')).toThrow('Decrypt failed: unrecognized string format'); expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format'); }); diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index eef4de375..e88c593c1 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -68,7 +68,7 @@ const mapArrayListToKeyValPairs = (arrayList = []) => { return { name, - value: null, + value: '', enabled }; }); diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js index fbb74f2b9..9c97cd1fa 100644 --- a/packages/bruno-lang/v2/tests/envToJson.spec.js +++ b/packages/bruno-lang/v2/tests/envToJson.spec.js @@ -185,7 +185,7 @@ vars:secret [ }, { name: 'token', - value: null, + value: '', enabled: true, secret: true } @@ -220,19 +220,19 @@ vars:secret [ }, { name: 'access_token', - value: null, + value: '', enabled: true, secret: true }, { name: 'access_secret', - value: null, + value: '', enabled: true, secret: true }, { name: 'access_password', - value: null, + value: '', enabled: false, secret: true } @@ -262,7 +262,7 @@ vars:secret [access_key] }, { name: 'access_key', - value: null, + value: '', enabled: true, secret: true } @@ -292,19 +292,19 @@ vars:secret [access_key,access_secret, access_password ] }, { name: 'access_key', - value: null, + value: '', enabled: true, secret: true }, { name: 'access_secret', - value: null, + value: '', enabled: true, secret: true }, { name: 'access_password', - value: null, + value: '', enabled: true, secret: true } From 9bc07afc77e0f76b619b2f2b0d6e6ee31f818272 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 10 Jun 2025 21:05:39 +0530 Subject: [PATCH 901/904] `initRunRequestEvent` function for initializing request execution related details (#4863) added a initRunRequestEvent function resetting and initializing request run event related details --- .../ReduxStore/slices/collections/actions.js | 39 +++++++++-------- .../ReduxStore/slices/collections/index.js | 43 +++++++++++-------- .../bruno-electron/src/ipc/network/index.js | 7 +-- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index d55923df4..bbbc9a61e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -35,9 +35,9 @@ import { responseReceived, updateLastAction, setCollectionSecurityConfig, - setRequestStartTime, collectionAddOauth2CredentialsByUrl, - collectionClearOauth2CredentialsByUrl + collectionClearOauth2CredentialsByUrl, + initRunRequestEvent } from './index'; import { each } from 'lodash'; @@ -221,21 +221,26 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { const state = getState(); const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments; const collection = findCollectionByUid(state.collections.collections, collectionUid); + const itemUid = item?.uid; - dispatch(setRequestStartTime({ - itemUid: item.uid, - collectionUid: collectionUid, - timestamp: Date.now() - })); - - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { if (!collection) { return reject(new Error('Collection not found')); } - - const itemCopy = cloneDeep(item || {}); + let collectionCopy = cloneDeep(collection); + const itemCopy = cloneDeep(item); + + const requestUid = uuid(); + itemCopy.requestUid = requestUid; + + await dispatch(initRunRequestEvent({ + requestUid, + itemUid, + collectionUid + })); + // add selected global env variables to the collection object const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables; @@ -254,8 +259,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { return dispatch( responseReceived({ - itemUid: item.uid, - collectionUid: collectionUid, + itemUid, + collectionUid, response: serializedResponse }) ); @@ -266,8 +271,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { console.log('>> request cancelled'); dispatch( responseReceived({ - itemUid: item.uid, - collectionUid: collectionUid, + itemUid, + collectionUid, response: null }) ); @@ -284,8 +289,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => { dispatch( responseReceived({ - itemUid: item.uid, - collectionUid: collectionUid, + itemUid, + collectionUid, response: errorResponse }) ); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 5ac2ef838..67c099428 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -276,6 +276,8 @@ export const collectionsSlice = createSlice({ if (item) { item.response = null; item.cancelTokenUid = null; + item.requestUid = null; + item.requestStartTime = null; } } }, @@ -288,6 +290,7 @@ export const collectionsSlice = createSlice({ item.requestState = 'received'; item.response = action.payload.response; item.cancelTokenUid = null; + item.requestStartTime = null; if (!collection.timeline) { collection.timeline = []; @@ -1954,26 +1957,40 @@ export const collectionsSlice = createSlice({ collection.runnerResult = null; } }, + initRunRequestEvent: (state, action) => { + const { requestUid, itemUid, collectionUid } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) return; + + const item = findItemInCollection(collection, itemUid); + if (!item) return; + + item.requestState = null; + item.requestUid = requestUid; + item.requestStartTime = Date.now(); + }, runRequestEvent: (state, action) => { - const { itemUid, collectionUid, type, requestUid, hasError } = action.payload; + const { itemUid, collectionUid, type, requestUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (collection) { const item = findItemInCollection(collection, itemUid); if (item) { + // ignore outdated updates in case multiple requests are fired rapidly to avoid state inconsistency + if (item.requestUid !== requestUid) return; + if (type === 'pre-request-script-execution') { - item.requestUid = requestUid; item.preRequestScriptErrorMessage = action.payload.errorMessage; } if(type === 'post-response-script-execution') { - item.requestUid = requestUid; item.postResponseScriptErrorMessage = action.payload.errorMessage; } if (type === 'request-queued') { const { cancelTokenUid } = action.payload; - item.requestUid = requestUid; + // ignore if request is already in progress or completed + if (['sending', 'received'].includes(item.requestState)) return; item.requestState = 'queued'; item.cancelTokenUid = cancelTokenUid; } @@ -1981,10 +1998,9 @@ export const collectionsSlice = createSlice({ if (type === 'request-sent') { const { cancelTokenUid, requestSent } = action.payload; item.requestSent = requestSent; - + // sometimes the response is received before the request-sent event arrives - if (item.requestUid === requestUid && item.requestState === 'queued') { - item.requestUid = requestUid; + if (item.requestState === 'queued') { item.requestState = 'sending'; item.cancelTokenUid = cancelTokenUid; } @@ -2105,17 +2121,6 @@ export const collectionsSlice = createSlice({ } } }, - setRequestStartTime: (state, action) => { - const { itemUid, collectionUid, timestamp } = action.payload; - const collection = findCollectionByUid(state.collections, collectionUid); - - if (collection) { - const item = findItemInCollection(collection, itemUid); - if (item) { - item.requestStartTime = timestamp; - } - } - }, collectionAddOauth2CredentialsByUrl: (state, action) => { const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -2309,13 +2314,13 @@ export const { collectionAddEnvFileEvent, collectionRenamedEvent, resetRunResults, + initRunRequestEvent, runRequestEvent, runFolderEvent, resetCollectionRunner, updateRequestDocs, updateFolderDocs, moveCollection, - setRequestStartTime, collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByUrl, collectionGetOauth2CredentialsByUrl, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c97b7f63c..f5a49ca1d 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -557,7 +557,8 @@ const registerNetworkIpc = (mainWindow) => { const collectionUid = collection.uid; const collectionPath = collection.pathname; const cancelTokenUid = uuid(); - const requestUid = uuid(); + // requestUid is passed when a request is triggered; defaults to uuid() if not provided (e.g., bru.runRequest()) + const requestUid = item.requestUid || uuid(); const runRequestByItemPathname = async (relativeItemPathname) => { return new Promise(async (resolve, reject) => { @@ -699,7 +700,7 @@ const registerNetworkIpc = (mainWindow) => { // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise return { statusText: error.statusText, - error: error.message, + error: error.message || 'Error occured while executing the request!', timeline: error.timeline } } @@ -837,7 +838,7 @@ const registerNetworkIpc = (mainWindow) => { // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise return { status: error?.status, - error: error?.message || 'an error ocurred: debug', + error: error?.message || 'Error occured while executing the request!', timeline: error?.timeline }; } From fc697bf81b39de3cbaf3781376abee4c75e787d9 Mon Sep 17 00:00:00 2001 From: Pooja Date: Tue, 10 Jun 2025 22:41:11 +0530 Subject: [PATCH 902/904] feat: support chai in scripts (#4552) feat: support chai in scripts --- .../ResponsePane/TestResults/StyledWrapper.js | 28 ++- .../ResponsePane/TestResults/index.js | 190 +++++++++++++----- .../ResponsePane/TestResultsLabel/index.js | 18 +- .../src/components/ResponsePane/index.js | 28 ++- .../RunnerResults/ResponsePane/index.js | 16 +- .../ReduxStore/slices/collections/index.js | 10 + packages/bruno-cli/src/commands/run.js | 76 ++++--- .../src/runner/run-single-request.js | 64 ++++-- .../bruno-common/src/runner/runner-summary.ts | 32 ++- .../bruno-common/src/runner/types/index.ts | 8 + .../src/postman/postman-to-bruno.js | 31 ++- .../scripts/translate-postman-scripts.js | 7 +- .../bruno-electron/src/ipc/network/index.js | 24 ++- .../bruno-js/src/runtime/script-runtime.js | 27 ++- packages/bruno-js/src/runtime/test-runtime.js | 55 +---- packages/bruno-js/src/utils.js | 1 + packages/bruno-js/src/utils/results.js | 80 ++++++++ 17 files changed, 501 insertions(+), 194 deletions(-) create mode 100644 packages/bruno-js/src/utils/results.js diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js index 001b4dc29..5b029386e 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js @@ -1,6 +1,18 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + color: ${(props) => props.theme.text}; + + .test-summary { + transition: background-color 0.2s; + border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder}; + color: ${(props) => props.theme.text}; + + &:hover { + background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } + .test-success { color: ${(props) => props.theme.colors.text.green}; } @@ -9,12 +21,24 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.colors.text.danger}; } + .test-success-count { + color: ${(props) => props.theme.colors.text.green}; + } + + .test-failure-count { + color: ${(props) => props.theme.colors.text.danger}; + } + .error-message { color: ${(props) => props.theme.colors.text.muted}; } - .skipped-request { - color: ${(props) => props.theme.colors.text.muted}; + .test-results-list { + transition: all 0.3s ease; + } + + .dropdown-icon { + color: ${(props) => props.theme.sidebar.dropdownIcon.color}; } `; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js index 074fac9e1..8157df2ee 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResults/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResults/index.js @@ -1,63 +1,151 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import StyledWrapper from './StyledWrapper'; +import { + IconChevronDown, + IconChevronRight, + IconCircleCheck, + IconCircleX +} from '@tabler/icons'; -const TestResults = ({ results, assertionResults }) => { +const ResultIcon = ({ status }) => ( + + {status === 'pass' ? ( + + ) : ( + + )} + +); + +const ErrorMessage = ({ error }) => error && ( + <> +
+ + {error} + + +); + +const ResultItem = ({ result, type }) => ( +
+ + + {type === 'assertion' + ? `${result.lhsExpr}: ${result.rhsExpr}` + : result.description + } + + +
+); + +const TestSection = ({ + title, + results, + isExpanded, + onToggle, + type = 'test' +}) => { + const passedResults = results.filter((result) => result.status === 'pass'); + const failedResults = results.filter((result) => result.status === 'fail'); + + if (results.length === 0) return null; + + return ( +
+
+ + {isExpanded ? + : + + } + + + {title} ({results.length}), Passed: {passedResults.length}, Failed: {failedResults.length} + +
+ {isExpanded && ( +
    + {results.map((result) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +}; + +const TestResults = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => { results = results || []; assertionResults = assertionResults || []; - if (!results.length && !assertionResults.length) { + preRequestTestResults = preRequestTestResults || []; + postResponseTestResults = postResponseTestResults || []; + + const [expandedSections, setExpandedSections] = useState({ + preRequest: true, + tests: true, + postResponse: true, + assertions: true + }); + + useEffect(() => { + setExpandedSections({ + preRequest: preRequestTestResults.length > 0, + tests: results.length > 0, + postResponse: postResponseTestResults.length > 0, + assertions: assertionResults.length > 0 + }); + }, [results.length, assertionResults.length, preRequestTestResults.length, postResponseTestResults.length]); + + const toggleSection = (section) => { + setExpandedSections({ + ...expandedSections, + [section]: !expandedSections[section] + }); + }; + + if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) { return
No tests found
; } - const passedTests = results.filter((result) => result.status === 'pass'); - const failedTests = results.filter((result) => result.status === 'fail'); - - const passedAssertions = assertionResults.filter((result) => result.status === 'pass'); - const failedAssertions = assertionResults.filter((result) => result.status === 'fail'); - return ( - -
- Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length} -
-
    - {results.map((result) => ( -
  • - {result.status === 'pass' ? ( - ✔  {result.description} - ) : ( - <> - ✘  {result.description} -
    - {result.error} - - )} -
  • - ))} -
+ + toggleSection('preRequest')} + type="test" + /> -
- Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '} - {failedAssertions.length} -
-
    - {assertionResults.map((result) => ( -
  • - {result.status === 'pass' ? ( - - ✔  {result.lhsExpr}: {result.rhsExpr} - - ) : ( - <> - - ✘  {result.lhsExpr}: {result.rhsExpr} - -
    - {result.error} - - )} -
  • - ))} -
+ toggleSection('postResponse')} + type="test" + /> + + toggleSection('tests')} + type="test" + /> + + toggleSection('assertions')} + type="assertion" + />
); }; diff --git a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js index f894d1f76..51d6f94cc 100644 --- a/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js +++ b/packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js @@ -1,9 +1,13 @@ import React from 'react'; +import { IconCircleCheck, IconCircleX } from '@tabler/icons'; -const TestResultsLabel = ({ results, assertionResults }) => { +const TestResultsLabel = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => { results = results || []; assertionResults = assertionResults || []; - if (!results.length && !assertionResults.length) { + preRequestTestResults = preRequestTestResults || []; + postResponseTestResults = postResponseTestResults || []; + + if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) { return 'Tests'; } @@ -13,8 +17,14 @@ const TestResultsLabel = ({ results, assertionResults }) => { const numberOfAssertions = assertionResults.length; const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length; - const totalNumberOfTests = numberOfTests + numberOfAssertions; - const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions; + const numberOfPreRequestTests = preRequestTestResults.length; + const numberOfFailedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail').length; + + const numberOfPostResponseTests = postResponseTestResults.length; + const numberOfFailedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail').length; + + const totalNumberOfTests = numberOfTests + numberOfAssertions + numberOfPreRequestTests + numberOfPostResponseTests; + const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions + numberOfFailedPreRequestTests + numberOfFailedPostResponseTests; return (
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 1fb120ae9..71e55cdd5 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -73,7 +73,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { return ; } case 'tests': { - return ; + return ; } default: { @@ -122,7 +127,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { }; const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0; - + const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage; return ( @@ -139,14 +144,19 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { Timeline
selectTab('tests')}> - +
{!isLoading ? (
{hasScriptError && !showScriptErrorCard && ( - setShowScriptErrorCard(true)} + setShowScriptErrorCard(true)} /> )} {focusedTab?.responsePaneTab === "timeline" ? ( @@ -168,9 +178,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { > {isLoading ? : null} {hasScriptError && showScriptErrorCard && ( - setShowScriptErrorCard(false)} + setShowScriptErrorCard(false)} /> )} {!item?.response ? ( diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index 5591dbfea..21f02406e 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -16,7 +16,7 @@ import RunnerTimeline from 'components/ResponsePane/RunnerTimeline'; const ResponsePane = ({ rightPaneWidth, item, collection }) => { const [selectedTab, setSelectedTab] = useState('response'); - const { requestSent, responseReceived, testResults, assertionResults, error } = item; + const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item; const headers = get(item, 'responseReceived.headers', []); const status = get(item, 'responseReceived.status', 0); @@ -49,7 +49,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { return ; } case 'tests': { - return ; + return ; } default: { @@ -86,7 +91,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { Timeline
selectTab('tests')}> - +
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 67c099428..15ce34f62 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2015,6 +2015,16 @@ export const collectionsSlice = createSlice({ const { results } = action.payload; item.testResults = results; } + + if (type === 'test-results-pre-request') { + const { results } = action.payload; + item.preRequestTestResults = results; + } + + if (type === 'test-results-post-response') { + const { results } = action.payload; + item.postResponseTestResults = results; + } } } }, diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index af4cf4ae3..5cf4f67f6 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -16,6 +16,20 @@ const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPa const command = 'run [filename]'; const desc = 'Run a request'; +const formatTestSummary = (label, maxLength, passed, failed, total, errorCount = 0, skippedCount = 0) => { + const parts = [ + `${rpad(label, maxLength)} ${chalk.green(`${passed} passed`)}` + ]; + + if (failed > 0) parts.push(chalk.red(`${failed} failed`)); + if (errorCount > 0) parts.push(chalk.red(`${errorCount} error`)); + if (skippedCount > 0) parts.push(chalk.magenta(`${skippedCount} skipped`)); + + parts.push(`${total} total`); + + return parts.join(', '); +}; + const printRunSummary = (results) => { const { totalRequests, @@ -28,38 +42,40 @@ const printRunSummary = (results) => { failedAssertions, totalTests, passedTests, - failedTests + failedTests, + totalPreRequestTests, + passedPreRequestTests, + failedPreRequestTests, + totalPostResponseTests, + passedPostResponseTests, + failedPostResponseTests } = getRunnerSummary(results); const maxLength = 12; - let requestSummary = `${rpad('Requests:', maxLength)} ${chalk.green(`${passedRequests} passed`)}`; - if (failedRequests > 0) { - requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`; - } - if (errorRequests > 0) { - requestSummary += `, ${chalk.red(`${errorRequests} error`)}`; - } - if (skippedRequests > 0) { - requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`; - } - requestSummary += `, ${totalRequests} total`; + const requestSummary = formatTestSummary('Requests:', maxLength, passedRequests, failedRequests, totalRequests, errorRequests, skippedRequests); + const testSummary = formatTestSummary('Tests:', maxLength, passedTests, failedTests, totalTests); + const assertSummary = formatTestSummary('Assertions:', maxLength, passedAssertions, failedAssertions, totalAssertions); - let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`; - if (failedTests > 0) { - assertSummary += `, ${chalk.red(`${failedTests} failed`)}`; + let preRequestTestSummary = ''; + if (totalPreRequestTests > 0) { + preRequestTestSummary = formatTestSummary('Pre-Request Tests:', maxLength, passedPreRequestTests, failedPreRequestTests, totalPreRequestTests); } - assertSummary += `, ${totalTests} total`; - let testSummary = `${rpad('Assertions:', maxLength)} ${chalk.green(`${passedAssertions} passed`)}`; - if (failedAssertions > 0) { - testSummary += `, ${chalk.red(`${failedAssertions} failed`)}`; + let postResponseTestSummary = ''; + if (totalPostResponseTests > 0) { + postResponseTestSummary = formatTestSummary('Post-Response Tests:', maxLength, passedPostResponseTests, failedPostResponseTests, totalPostResponseTests); } - testSummary += `, ${totalAssertions} total`; console.log('\n' + chalk.bold(requestSummary)); - console.log(chalk.bold(assertSummary)); + if (preRequestTestSummary) { + console.log(chalk.bold(preRequestTestSummary)); + } + if (postResponseTestSummary) { + console.log(chalk.bold(postResponseTestSummary)); + } console.log(chalk.bold(testSummary)); + console.log(chalk.bold(assertSummary)); return { totalRequests, @@ -72,7 +88,13 @@ const printRunSummary = (results) => { failedAssertions, totalTests, passedTests, - failedTests + failedTests, + totalPreRequestTests, + passedPreRequestTests, + failedPreRequestTests, + totalPostResponseTests, + passedPostResponseTests, + failedPostResponseTests } }; @@ -498,7 +520,7 @@ const handler = async function (argv) { if(Number.isNaN(delay) && !isLastRun){ console.log(chalk.red(`Ignoring delay because it's not a valid number.`)); } - + results.push({ ...result, runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9, @@ -539,7 +561,9 @@ const handler = async function (argv) { const requestFailure = result?.error && !result?.skipped; const testFailure = result?.testResults?.find((iter) => iter.status === 'fail'); const assertionFailure = result?.assertionResults?.find((iter) => iter.status === 'fail'); - if (requestFailure || testFailure || assertionFailure) { + const preRequestTestFailure = result?.preRequestTestResults?.find((iter) => iter.status === 'fail'); + const postResponseTestFailure = result?.postResponseTestResults?.find((iter) => iter.status === 'fail'); + if (requestFailure || testFailure || assertionFailure || preRequestTestFailure || postResponseTestFailure) { break; } } @@ -550,7 +574,7 @@ const handler = async function (argv) { if (result?.shouldStopRunnerExecution) { break; } - + if (nextRequestName !== undefined) { nJumps++; if (nJumps > 10000) { @@ -617,7 +641,7 @@ const handler = async function (argv) { } } - if ((summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) { + if ((summary.failedAssertions + summary.failedTests + summary.failedPreRequestTests + summary.failedPostResponseTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) { process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION); } } catch (err) { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index c660367e8..6fd575f90 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -45,10 +45,33 @@ const runSingleRequest = async function ( ) { const { pathname: itemPathname } = item; const relativeItemPathname = path.relative(collectionPath, itemPathname); + + const logResults = (results, title) => { + if (results?.length) { + if (title) { + console.log(chalk.dim(title)); + } + each(results, (r) => { + const message = r.description || `${r.lhsExpr}: ${r.rhsExpr}`; + if (r.status === 'pass') { + console.log(chalk.green(` ✓ `) + chalk.dim(message)); + } else { + console.log(chalk.red(` ✕ `) + chalk.red(message)); + if (r.error) { + console.log(chalk.red(` ${r.error}`)); + } + } + }); + } + }; + try { let request; let nextRequestName; let shouldStopRunnerExecution = false; + let preRequestTestResults = []; + let postResponseTestResults = []; + request = prepareRequest(item, collection); request.__bruno__executionMode = 'cli'; @@ -103,9 +126,13 @@ const runSingleRequest = async function ( skipped: true, assertionResults: [], testResults: [], + preRequestTestResults: result?.results || [], + postResponseTestResults: [], shouldStopRunnerExecution }; } + + preRequestTestResults = result?.results || []; } // interpolate variables inside request @@ -428,6 +455,8 @@ const runSingleRequest = async function ( status: 'error', assertionResults: [], testResults: [], + preRequestTestResults, + postResponseTestResults, nextRequestName: nextRequestName, shouldStopRunnerExecution }; @@ -441,6 +470,9 @@ const runSingleRequest = async function ( chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`) ); + // Log pre-request test results + logResults(preRequestTestResults, 'Pre-Request Tests'); + // run post-response vars const postResponseVars = get(item, 'request.vars.res'); if (postResponseVars?.length) { @@ -480,9 +512,11 @@ const runSingleRequest = async function ( if (result?.stopExecution) { shouldStopRunnerExecution = true; } + + postResponseTestResults = result?.results || []; + logResults(postResponseTestResults, 'Post-Response Tests'); } - // run assertions let assertionResults = []; const assertions = get(item, 'request.assertions'); if (assertions) { @@ -495,15 +529,6 @@ const runSingleRequest = async function ( runtimeVariables, processEnvVars ); - - each(assertionResults, (r) => { - if (r.status === 'pass') { - console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); - } else { - console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); - console.log(chalk.red(` ${r.error}`)); - } - }); } // run tests @@ -533,17 +558,12 @@ const runSingleRequest = async function ( if (result?.stopExecution) { shouldStopRunnerExecution = true; } + + logResults(testResults, 'Tests'); } - if (testResults?.length) { - each(testResults, (testResult) => { - if (testResult.status === 'pass') { - console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description)); - } else { - console.log(chalk.red(` ✕ `) + chalk.red(testResult.description)); - } - }); - } + + logResults(assertionResults, 'Assertions'); return { test: { @@ -566,6 +586,8 @@ const runSingleRequest = async function ( status: 'pass', assertionResults, testResults, + preRequestTestResults, + postResponseTestResults, nextRequestName: nextRequestName, shouldStopRunnerExecution }; @@ -591,7 +613,9 @@ const runSingleRequest = async function ( status: 'error', error: err.message, assertionResults: [], - testResults: [] + testResults: [], + preRequestTestResults: [], + postResponseTestResults: [] }; } }; diff --git a/packages/bruno-common/src/runner/runner-summary.ts b/packages/bruno-common/src/runner/runner-summary.ts index 5862729bb..4fb276a29 100644 --- a/packages/bruno-common/src/runner/runner-summary.ts +++ b/packages/bruno-common/src/runner/runner-summary.ts @@ -13,12 +13,20 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R let totalTests = 0; let passedTests = 0; let failedTests = 0; + let totalPreRequestTests = 0; + let passedPreRequestTests = 0; + let failedPreRequestTests = 0; + let totalPostResponseTests = 0; + let passedPostResponseTests = 0; + let failedPostResponseTests = 0; for (const result of results || []) { - const { status, testResults, assertionResults } = result; + const { status, testResults, assertionResults, preRequestTestResults, postResponseTestResults } = result; totalRequests += 1; totalTests += Number(testResults?.length) || 0; totalAssertions += Number(assertionResults?.length) || 0; + totalPreRequestTests += Number(preRequestTestResults?.length) || 0; + totalPostResponseTests += Number(postResponseTestResults?.length) || 0; if (status === 'skipped') { skippedRequests += 1; @@ -42,6 +50,22 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R failedAssertions += 1; } } + for (const preRequestTestResult of preRequestTestResults || []) { + if (preRequestTestResult.status === "pass") { + passedPreRequestTests += 1; + } else { + anyFailed = true; + failedPreRequestTests += 1; + } + } + for (const postResponseTestResult of postResponseTestResults || []) { + if (postResponseTestResult.status === "pass") { + passedPostResponseTests += 1; + } else { + anyFailed = true; + failedPostResponseTests += 1; + } + } if (!anyFailed && status !== "error") { passedRequests += 1; @@ -64,5 +88,11 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R totalTests, passedTests, failedTests, + totalPreRequestTests, + passedPreRequestTests, + failedPreRequestTests, + totalPostResponseTests, + passedPostResponseTests, + failedPostResponseTests, }; }; \ No newline at end of file diff --git a/packages/bruno-common/src/runner/types/index.ts b/packages/bruno-common/src/runner/types/index.ts index 33840f0ad..3565930f2 100644 --- a/packages/bruno-common/src/runner/types/index.ts +++ b/packages/bruno-common/src/runner/types/index.ts @@ -89,6 +89,8 @@ export type T_RunnerRequestExecutionResult = { error: null | undefined | string; assertionResults?: T_AssertionResult[]; testResults?: T_TestResult[]; + preRequestTestResults?: T_TestResult[]; + postResponseTestResults?: T_TestResult[]; runDuration: number; } @@ -112,4 +114,10 @@ export type T_RunSummary = { totalTests: number; passedTests: number; failedTests: number; + totalPreRequestTests: number; + passedPreRequestTests: number; + failedPreRequestTests: number; + totalPostResponseTests: number; + passedPostResponseTests: number; + failedPostResponseTests: number; } \ 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 8ae8e195e..bef21b46e 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -103,14 +103,14 @@ const importScriptsFromEvents = (events, requestObject) => { } if (event.listen === 'test') { - if (!requestObject.tests) { - requestObject.tests = {}; + if (!requestObject.script) { + requestObject.script = {}; } if (event.script.exec && event.script.exec.length > 0) { - requestObject.tests = postmanTranslation(event.script.exec) + requestObject.script.res = postmanTranslation(event.script.exec) } else { - requestObject.tests = ''; + requestObject.script.res = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } @@ -376,16 +376,17 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorke } } if (event.listen === 'test' && event.script && event.script.exec) { - if (!brunoRequestItem.request?.tests) { - brunoRequestItem.request.tests = {}; + if (!brunoRequestItem.request?.script) { + brunoRequestItem.request.script = {}; } if (event.script.exec && event.script.exec.length > 0) { - brunoRequestItem.request.tests = postmanTranslation(event.script.exec) + brunoRequestItem.request.script.res = postmanTranslation(event.script.exec) } else { - brunoRequestItem.request.tests = ''; + brunoRequestItem.request.script.res = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } + }); } } @@ -581,15 +582,12 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) => 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; + const tests = translatedScripts.get(item.uid).request?.script?.res; item.root.request.script.req = script && script.length > 0 ? script : ''; - item.root.request.tests = tests && tests.length > 0 ? tests : ''; + item.root.request.script.res = tests && tests.length > 0 ? tests : ''; } // Recursively apply to nested items @@ -601,15 +599,12 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) => 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; + const tests = translatedScripts.get(item.uid).request?.script?.res; item.request.script.req = script && script.length > 0 ? script : ''; - item.request.tests = tests && tests.length > 0 ? tests : ''; + item.request.script.res = tests && tests.length > 0 ? tests : ''; } } }); diff --git a/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js index 31b7d9008..816a08f03 100644 --- a/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js +++ b/packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js @@ -6,8 +6,7 @@ parentPort.on('message', (workerData) => { const { scripts } = workerData; const modScripts = scripts.map(([uid, { events }]) => { const requestObject = { - script: {}, - tests: {} + script: {} } if (events && Array.isArray(events)) { @@ -23,9 +22,9 @@ parentPort.on('message', (workerData) => { if(event.listen === 'test') { if(event.script.exec && event.script.exec.length > 0) { - requestObject.tests = postmanTranslation(event.script.exec); + requestObject.script.res = postmanTranslation(event.script.exec); } else { - requestObject.tests = ''; + requestObject.script.res = ''; } } } diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index f5a49ca1d..674cb5754 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -596,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => { try { - await runPreRequest( + const preRequestScriptResult = await runPreRequest( request, requestUid, envVars, @@ -609,6 +609,16 @@ const registerNetworkIpc = (mainWindow) => { runRequestByItemPathname ); + if (preRequestScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-pre-request', + results: preRequestScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } + !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'pre-request-script-execution', requestUid, @@ -724,7 +734,7 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); try { - await runPostResponse( + const postResponseScriptResult = await runPostResponse( request, response, requestUid, @@ -737,6 +747,16 @@ const registerNetworkIpc = (mainWindow) => { scriptingConfig, runRequestByItemPathname ); + + if (postResponseScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-post-response', + results: postResponseScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'post-response-script-execution', requestUid, diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index a8f2abbee..a18c94917 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -12,7 +12,10 @@ const { get } = require('lodash'); const Bru = require('../bru'); const BrunoRequest = require('../bruno-request'); const BrunoResponse = require('../bruno-response'); +const Test = require('../test'); +const TestResults = require('../test-results'); const { cleanJson } = require('../utils'); +const { createBruTestResultMethods } = require('../utils/results'); // Inbuilt Library Support const ajv = require('ajv'); @@ -57,6 +60,7 @@ class ScriptRuntime { const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; + const assertionResults = request?.assertionResults || []; const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName); const req = new BrunoRequest(request); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); @@ -78,9 +82,16 @@ class ScriptRuntime { } } + // extend bru with result getter methods + const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai); + const context = { bru, - req + req, + test, + expect: chai.expect, + assert: chai.assert, + __brunoTestResults: __brunoTestResults }; if (onConsoleLog && typeof onConsoleLog === 'function') { @@ -114,6 +125,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -168,6 +180,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -192,6 +205,7 @@ class ScriptRuntime { const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; + const assertionResults = request?.assertionResults || []; const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName); const req = new BrunoRequest(request); const res = new BrunoResponse(response); @@ -214,10 +228,17 @@ class ScriptRuntime { } } + // extend bru with result getter methods + const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai); + const context = { bru, req, - res + res, + test, + expect: chai.expect, + assert: chai.assert, + __brunoTestResults: __brunoTestResults }; if (onConsoleLog && typeof onConsoleLog === 'function') { @@ -251,6 +272,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution @@ -305,6 +327,7 @@ class ScriptRuntime { envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index b172fc7b7..bed0589ca 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -16,6 +16,7 @@ const BrunoResponse = require('../bruno-response'); const Test = require('../test'); const TestResults = require('../test-results'); const { cleanJson } = require('../utils'); +const { createBruTestResultMethods } = require('../utils/results'); // Inbuilt Library Support const ajv = require('ajv'); @@ -35,25 +36,6 @@ const cheerio = require('cheerio'); const tv4 = require('tv4'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); -const getResultsSummary = (results) => { - const summary = { - total: results.length, - passed: 0, - failed: 0, - skipped: 0, - }; - - results.forEach((r) => { - const passed = r.status === "pass"; - if (passed) summary.passed += 1; - else if (r.status === "fail") summary.failed += 1; - else summary.skipped += 1; - }); - - return summary; -} - - class TestRuntime { constructor(props) { this.runtime = props?.runtime || 'vm2'; @@ -99,9 +81,8 @@ class TestRuntime { } } - const __brunoTestResults = new TestResults(); - - const test = Test(__brunoTestResults, chai); + // extend bru with result getter methods + const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai); if (!testsFile || !testsFile.length) { return { @@ -114,36 +95,6 @@ class TestRuntime { }; } - bru.getTestResults = async () => { - let results = await __brunoTestResults.getResults(); - const summary = getResultsSummary(results); - return { - summary, - results: results?.map?.(r => ({ - status: r?.status, - description: r?.description, - expected: r?.expected, - actual: r?.actual, - error: r?.error - })) - }; - } - bru.getAssertionResults = async () => { - let results = assertionResults; - const summary = getResultsSummary(results); - return { - summary, - results: results?.map?.(r => ({ - status: r?.status, - lhsExpr: r?.lhsExpr, - rhsExpr: r?.rhsExpr, - operator: r?.operator, - rhsOperand: r?.rhsOperand, - error: r?.error - })) - }; - } - const context = { test, bru, diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 55b454d02..289bf8dcc 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -144,6 +144,7 @@ const cleanJson = (data) => { } }; + module.exports = { evaluateJsExpression, evaluateJsTemplateLiteral, diff --git a/packages/bruno-js/src/utils/results.js b/packages/bruno-js/src/utils/results.js new file mode 100644 index 000000000..0ed38638a --- /dev/null +++ b/packages/bruno-js/src/utils/results.js @@ -0,0 +1,80 @@ +const TestResults = require('../test-results'); +const Test = require('../test'); + +// Calculate summary statistics for test results +const getResultsSummary = (results) => { + const summary = { + total: results.length, + passed: 0, + failed: 0, + skipped: 0, + }; + + results.forEach((r) => { + const passed = r.status === 'pass'; + if (passed) summary.passed += 1; + else if (r.status === 'fail') summary.failed += 1; + else summary.skipped += 1; + }); + + return summary; +}; + +const createBruTestResultMethods = (bru, assertionResults, chai) => { + const __brunoTestResults = new TestResults(); + const test = Test(__brunoTestResults, chai); + setupBruTestMethods(bru, __brunoTestResults, assertionResults); + + return { __brunoTestResults, test }; +}; + +const setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => { + const getTestResults = async () => { + let results = await __brunoTestResults.getResults(); + const summary = getResultsSummary(results); + return { + summary, + results: results.map(r => ({ + status: r.status, + description: r.description, + expected: r.expected, + actual: r.actual, + error: r.error + })) + }; + }; + + const getAssertionResults = async () => { + let results = assertionResults; + const summary = getResultsSummary(results); + return { + summary, + results: results.map(r => ({ + status: r.status, + lhsExpr: r.lhsExpr, + rhsExpr: r.rhsExpr, + operator: r.operator, + rhsOperand: r.rhsOperand, + error: r.error + })) + }; + }; + + // Set methods on bru object if provided + if (bru) { + bru.getTestResults = getTestResults; + bru.getAssertionResults = getAssertionResults; + } + + // Also return the methods for direct use + return { + getTestResults, + getAssertionResults + }; +}; + +module.exports = { + getResultsSummary, + createBruTestResultMethods, + setupBruTestMethods +}; \ No newline at end of file From 5c9981aca23787974c7d64d3a4fe9e821876aef5 Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 11 Jun 2025 14:27:45 +0530 Subject: [PATCH 903/904] Fix: AWS v4 auth empty fields displaying "undefined" after save (#4814) * Fix: AWS v4 auth empty fields displaying "undefined" after save --- .../Auth/AwsV4Auth/index.js | 72 +++++++++---------- .../Auth/BasicAuth/index.js | 8 +-- .../Auth/DigestAuth/index.js | 8 +-- .../CollectionSettings/Auth/NTLMAuth/index.js | 18 ++--- .../CollectionSettings/Auth/WsseAuth/index.js | 8 +-- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js index 38fae3447..0f14a4dfa 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js @@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: accessKeyId, - secretAccessKey: awsv4Auth.secretAccessKey, - sessionToken: awsv4Auth.sessionToken, - service: awsv4Auth.service, - region: awsv4Auth.region, - profileName: awsv4Auth.profileName + accessKeyId: accessKeyId || '', + secretAccessKey: awsv4Auth.secretAccessKey || '', + sessionToken: awsv4Auth.sessionToken || '', + service: awsv4Auth.service || '', + region: awsv4Auth.region || '', + profileName: awsv4Auth.profileName || '' } }) ); @@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: awsv4Auth.accessKeyId, - secretAccessKey: secretAccessKey, - sessionToken: awsv4Auth.sessionToken, - service: awsv4Auth.service, - region: awsv4Auth.region, - profileName: awsv4Auth.profileName + accessKeyId: awsv4Auth.accessKeyId || '', + secretAccessKey: secretAccessKey || '', + sessionToken: awsv4Auth.sessionToken || '', + service: awsv4Auth.service || '', + region: awsv4Auth.region || '', + profileName: awsv4Auth.profileName || '' } }) ); @@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: awsv4Auth.accessKeyId, - secretAccessKey: awsv4Auth.secretAccessKey, - sessionToken: sessionToken, - service: awsv4Auth.service, - region: awsv4Auth.region, - profileName: awsv4Auth.profileName + accessKeyId: awsv4Auth.accessKeyId || '', + secretAccessKey: awsv4Auth.secretAccessKey || '', + sessionToken: sessionToken || '', + service: awsv4Auth.service || '', + region: awsv4Auth.region || '', + profileName: awsv4Auth.profileName || '' } }) ); @@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: awsv4Auth.accessKeyId, - secretAccessKey: awsv4Auth.secretAccessKey, - sessionToken: awsv4Auth.sessionToken, - service: service, - region: awsv4Auth.region, - profileName: awsv4Auth.profileName + accessKeyId: awsv4Auth.accessKeyId || '', + secretAccessKey: awsv4Auth.secretAccessKey || '', + sessionToken: awsv4Auth.sessionToken || '', + service: service || '', + region: awsv4Auth.region || '', + profileName: awsv4Auth.profileName || '' } }) ); @@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: awsv4Auth.accessKeyId, - secretAccessKey: awsv4Auth.secretAccessKey, - sessionToken: awsv4Auth.sessionToken, - service: awsv4Auth.service, - region: region, - profileName: awsv4Auth.profileName + accessKeyId: awsv4Auth.accessKeyId || '', + secretAccessKey: awsv4Auth.secretAccessKey || '', + sessionToken: awsv4Auth.sessionToken || '', + service: awsv4Auth.service || '', + region: region || '', + profileName: awsv4Auth.profileName || '' } }) ); @@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => { mode: 'awsv4', collectionUid: collection.uid, content: { - accessKeyId: awsv4Auth.accessKeyId, - secretAccessKey: awsv4Auth.secretAccessKey, - sessionToken: awsv4Auth.sessionToken, - service: awsv4Auth.service, - region: awsv4Auth.region, - profileName: profileName + accessKeyId: awsv4Auth.accessKeyId || '', + secretAccessKey: awsv4Auth.secretAccessKey || '', + sessionToken: awsv4Auth.sessionToken || '', + service: awsv4Auth.service || '', + region: awsv4Auth.region || '', + profileName: profileName || '' } }) ); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js index 3c29895ed..9ea532646 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js @@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => { mode: 'basic', collectionUid: collection.uid, content: { - username: username, - password: basicAuth.password + username: username || '', + password: basicAuth.password || '' } }) ); @@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => { mode: 'basic', collectionUid: collection.uid, content: { - username: basicAuth.username, - password: password + username: basicAuth.username || '', + password: password || '' } }) ); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js index 5ac6b1e26..582b17b82 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js @@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => { mode: 'digest', collectionUid: collection.uid, content: { - username: username, - password: digestAuth.password + username: username || '', + password: digestAuth.password || '' } }) ); @@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => { mode: 'digest', collectionUid: collection.uid, content: { - username: digestAuth.username, - password: password + username: digestAuth.username || '', + password: password || '' } }) ); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js index 341c805dc..173c99a12 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js @@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => { mode: 'ntlm', collectionUid: collection.uid, content: { - username: username, - password: ntlmAuth.password, - domain: ntlmAuth.domain + username: username || '', + password: ntlmAuth.password || '', + domain: ntlmAuth.domain || '' } }) @@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => { mode: 'ntlm', collectionUid: collection.uid, content: { - username: ntlmAuth.username, - password: password, - domain: ntlmAuth.domain + username: ntlmAuth.username || '', + password: password || '', + domain: ntlmAuth.domain || '' } }) ); @@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => { mode: 'ntlm', collectionUid: collection.uid, content: { - username: ntlmAuth.username, - password: ntlmAuth.password, - domain: domain + username: ntlmAuth.username || '', + password: ntlmAuth.password || '', + domain: domain || '' } }) ); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js index 45efc7b1e..2e1a2c65c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js @@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => { mode: 'wsse', collectionUid: collection.uid, content: { - username, - password: wsseAuth.password + username: username || '', + password: wsseAuth.password || '' } }) ); @@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => { mode: 'wsse', collectionUid: collection.uid, content: { - username: wsseAuth.username, - password + username: wsseAuth.username || '', + password: password || '' } }) ); From 364fb45e973d45a49362b8a3e85d8780d3155d2e Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 11 Jun 2025 22:38:58 +0530 Subject: [PATCH 904/904] add: pre and post tests in runner (#4878) --- .../src/components/RunnerResults/index.jsx | 97 ++++++++++++++----- .../ReduxStore/slices/collections/index.js | 10 ++ .../bruno-electron/src/ipc/network/index.js | 26 ++++- 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index cfe3c0f1a..cbf099e5b 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -16,6 +16,28 @@ const getDisplayName = (fullPath, pathname, name = '') => { return path.join(dir, name); }; +const getTestStatus = (results) => { + if (!results || !results.length) return 'pass'; + const failed = results.filter((result) => result.status === 'fail'); + return failed.length ? 'fail' : 'pass'; +}; + +const allTestsPassed = (item) => { + return item.status !== 'error' && + item.testStatus === 'pass' && + item.assertionStatus === 'pass' && + item.preRequestTestStatus === 'pass' && + item.postResponseTestStatus === 'pass'; +}; + +const anyTestFailed = (item) => { + return item.status === 'error' || + item.testStatus === 'fail' || + item.assertionStatus === 'fail' || + item.preRequestTestStatus === 'fail' || + item.postResponseTestStatus === 'fail'; +}; + export default function RunnerResults({ collection }) { const dispatch = useDispatch(); const [selectedItem, setSelectedItem] = useState(null); @@ -56,19 +78,10 @@ export default function RunnerResults({ collection }) { displayName: getDisplayName(collection.pathname, info.pathname, info.name) }; if (newItem.status !== 'error' && newItem.status !== 'skipped') { - if (newItem.testResults) { - const failed = newItem.testResults.filter((result) => result.status === 'fail'); - newItem.testStatus = failed.length ? 'fail' : 'pass'; - } else { - newItem.testStatus = 'pass'; - } - - if (newItem.assertionResults) { - const failed = newItem.assertionResults.filter((result) => result.status === 'fail'); - newItem.assertionStatus = failed.length ? 'fail' : 'pass'; - } else { - newItem.assertionStatus = 'pass'; - } + newItem.testStatus = getTestStatus(newItem.testResults); + newItem.assertionStatus = getTestStatus(newItem.assertionResults); + newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults); + newItem.postResponseTestStatus = getTestStatus(newItem.postResponseTestResults); } return newItem; }) @@ -95,12 +108,8 @@ export default function RunnerResults({ collection }) { }; const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy); - const passedRequests = items.filter((item) => { - return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass'; - }); - const failedRequests = items.filter((item) => { - return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail'; - }); + const passedRequests = items.filter(allTestsPassed); + const failedRequests = items.filter(anyTestFailed); const skippedRequests = items.filter((item) => { return item.status === 'skipped'; @@ -176,18 +185,18 @@ export default function RunnerResults({ collection }) {
- {item.testStatus === 'pass' && item.assertionStatus === 'pass' ? + {allTestsPassed(item) ? : null} {item.status === 'skipped' ? :null} - {item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ? + {anyTestFailed(item) ? :null} {item.displayName} @@ -208,6 +217,46 @@ export default function RunnerResults({ collection }) { {item.status == 'error' ?
{item.error}
: null}
    + {item.preRequestTestResults + ? item.preRequestTestResults.map((result) => ( +
  • + {result.status === 'pass' ? ( + + + {result.description} + + ) : ( + <> + + + {result.description} + + {result.error} + + )} +
  • + )) + : null} + {item.postResponseTestResults + ? item.postResponseTestResults.map((result) => ( +
  • + {result.status === 'pass' ? ( + + + {result.description} + + ) : ( + <> + + + {result.description} + + {result.error} + + )} +
  • + )) + : null} {item.testResults ? item.testResults.map((result) => (
  • @@ -271,10 +320,10 @@ export default function RunnerResults({ collection }) {
    {selectedItem.displayName} - {selectedItem.testStatus === 'pass' && selectedItem.assertionStatus === 'pass' ? + {allTestsPassed(selectedItem) ? : null} - {selectedItem.status === 'error' || selectedItem.testStatus === 'fail' || selectedItem.assertionStatus === 'fail' ? + {anyTestFailed(selectedItem) ? : null} {selectedItem.status === 'skipped' ? diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 15ce34f62..64a72a43a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2081,6 +2081,16 @@ export const collectionsSlice = createSlice({ item.testResults = action.payload.testResults; } + if (type === 'test-results-pre-request') { + const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid); + item.preRequestTestResults = action.payload.preRequestTestResults; + } + + if (type === 'test-results-post-response') { + const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid); + item.postResponseTestResults = action.payload.postResponseTestResults; + } + if (type === 'assertion-results') { const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid); item.assertionResults = action.payload.assertionResults; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 674cb5754..4b4937d52 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1018,6 +1018,15 @@ const registerNetworkIpc = (mainWindow) => { stopRunnerExecution = true; } + // Send pre-request test results if available + if (preRequestScriptResult?.results) { + mainWindow.webContents.send('main:run-folder-event', { + type: 'test-results-pre-request', + preRequestTestResults: preRequestScriptResult.results, + ...eventData + }); + } + if (preRequestScriptResult?.skipRequest) { mainWindow.webContents.send('main:run-folder-event', { type: 'runner-request-skipped', @@ -1149,7 +1158,7 @@ const registerNetworkIpc = (mainWindow) => { } } - const postRequestScriptResult = await runPostResponse( + const postResponseScriptResult = await runPostResponse( request, response, requestUid, @@ -1163,14 +1172,23 @@ const registerNetworkIpc = (mainWindow) => { runRequestByItemPathname ); - if (postRequestScriptResult?.nextRequestName !== undefined) { - nextRequestName = postRequestScriptResult.nextRequestName; + if (postResponseScriptResult?.nextRequestName !== undefined) { + nextRequestName = postResponseScriptResult.nextRequestName; } - if (postRequestScriptResult?.stopExecution) { + if (postResponseScriptResult?.stopExecution) { stopRunnerExecution = true; } + // Send post-response test results if available + if (postResponseScriptResult?.results) { + mainWindow.webContents.send('main:run-folder-event', { + type: 'test-results-post-response', + postResponseTestResults: postResponseScriptResult.results, + ...eventData + }); + } + // run assertions const assertions = get(item, 'request.assertions'); if (assertions) {