From 5f9c21d00f0e6bb76bc62e5d0335966610dcfa78 Mon Sep 17 00:00:00 2001 From: Nikolai2038 Date: Wed, 22 May 2024 22:38:16 +0300 Subject: [PATCH 001/214] Update linux installation instructions via apt - Add instructions to install gpg; - Use "gpg --list-keys" to let gpg create ".gnupg" directory with correct rights; - Use "arch=amd64" - see commit 6c8c87fe28fca64cdd8a4c9b6159054577a387f7. --- docs/readme/readme_ar.md | 9 ++++----- docs/readme/readme_bn.md | 9 ++++----- docs/readme/readme_cn.md | 9 ++++----- docs/readme/readme_de.md | 9 ++++----- docs/readme/readme_es.md | 9 ++++----- docs/readme/readme_fr.md | 9 ++++----- docs/readme/readme_it.md | 9 ++++----- docs/readme/readme_ja.md | 9 ++++----- docs/readme/readme_kr.md | 9 ++++----- docs/readme/readme_pl.md | 9 ++++----- docs/readme/readme_pt_br.md | 9 ++++----- docs/readme/readme_ro.md | 9 ++++----- docs/readme/readme_tr.md | 9 ++++----- docs/readme/readme_zhtw.md | 9 ++++----- readme.md | 7 +++---- 15 files changed, 59 insertions(+), 74 deletions(-) diff --git a/docs/readme/readme_ar.md b/docs/readme/readme_ar.md index 614d60a14..7f0eb8004 100644 --- a/docs/readme/readme_ar.md +++ b/docs/readme/readme_ar.md @@ -57,12 +57,11 @@ flatpak install com.usebruno.Bruno # على نظام Linux عبر Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt +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 ``` ### التشغيل عبر منصات متعددة 🖥️ diff --git a/docs/readme/readme_bn.md b/docs/readme/readme_bn.md index d353201f0..588dc3b24 100644 --- a/docs/readme/readme_bn.md +++ b/docs/readme/readme_bn.md @@ -42,12 +42,11 @@ snap install bruno # Apt এর মাধ্যমে লিনাক্সে sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### একাধিক প্ল্যাটফর্মে চালান 🖥️ diff --git a/docs/readme/readme_cn.md b/docs/readme/readme_cn.md index 196ee2ff9..5fa138a16 100644 --- a/docs/readme/readme_cn.md +++ b/docs/readme/readme_cn.md @@ -46,12 +46,11 @@ snap install bruno # 在 Linux 上用 Apt 安装 sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### 在 Mac 上通过 Homebrew 安装 🖥️ diff --git a/docs/readme/readme_de.md b/docs/readme/readme_de.md index ac1a75fe6..17658fe92 100644 --- a/docs/readme/readme_de.md +++ b/docs/readme/readme_de.md @@ -61,12 +61,11 @@ flatpak install com.usebruno.Bruno # Auf Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Einsatz auf verschiedensten Plattformen 🖥️ diff --git a/docs/readme/readme_es.md b/docs/readme/readme_es.md index b1efdafa3..3c790c763 100644 --- a/docs/readme/readme_es.md +++ b/docs/readme/readme_es.md @@ -58,12 +58,11 @@ flatpak install com.usebruno.Bruno # En Linux con Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Ejecútalo en múltiples plataformas 🖥️ diff --git a/docs/readme/readme_fr.md b/docs/readme/readme_fr.md index 54a735c76..5efeced51 100644 --- a/docs/readme/readme_fr.md +++ b/docs/readme/readme_fr.md @@ -46,12 +46,11 @@ snap install bruno # Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Fonctionne sur de multiples plateformes 🖥️ diff --git a/docs/readme/readme_it.md b/docs/readme/readme_it.md index ff29804aa..2122cd1e0 100644 --- a/docs/readme/readme_it.md +++ b/docs/readme/readme_it.md @@ -40,12 +40,11 @@ snap install bruno # Su Linux tramite Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Funziona su diverse piattaforme 🖥️ diff --git a/docs/readme/readme_ja.md b/docs/readme/readme_ja.md index bd8571969..b5d278bb9 100644 --- a/docs/readme/readme_ja.md +++ b/docs/readme/readme_ja.md @@ -61,12 +61,11 @@ flatpak install com.usebruno.Bruno # LinuxでAptを使ってインストール sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### マルチプラットフォームでの実行に対応 🖥️ diff --git a/docs/readme/readme_kr.md b/docs/readme/readme_kr.md index 451e11ec0..2fd5f0f5b 100644 --- a/docs/readme/readme_kr.md +++ b/docs/readme/readme_kr.md @@ -40,12 +40,11 @@ snap install bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### 여러 플랫폼에서 실행하세요. 🖥️ diff --git a/docs/readme/readme_pl.md b/docs/readme/readme_pl.md index e8462f655..fd434a39a 100644 --- a/docs/readme/readme_pl.md +++ b/docs/readme/readme_pl.md @@ -46,12 +46,11 @@ snap install bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Uruchom na wielu platformach 🖥️ diff --git a/docs/readme/readme_pt_br.md b/docs/readme/readme_pt_br.md index 84bf2b2b8..4fd7b42d3 100644 --- a/docs/readme/readme_pt_br.md +++ b/docs/readme/readme_pt_br.md @@ -57,12 +57,11 @@ flatpak install com.usebruno.Bruno # No Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Execute em várias plataformas 🖥️ diff --git a/docs/readme/readme_ro.md b/docs/readme/readme_ro.md index de160f70d..53eb44d84 100644 --- a/docs/readme/readme_ro.md +++ b/docs/readme/readme_ro.md @@ -42,12 +42,11 @@ snap install bruno # Pe Linux cu Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Utilizați pe mai multe platforme 🖥️ diff --git a/docs/readme/readme_tr.md b/docs/readme/readme_tr.md index 7c4e2747e..96b9a7a0a 100644 --- a/docs/readme/readme_tr.md +++ b/docs/readme/readme_tr.md @@ -46,12 +46,11 @@ snap install bruno # Apt aracılığıyla Linux'ta sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### Birden fazla platformda çalıştırın 🖥️ diff --git a/docs/readme/readme_zhtw.md b/docs/readme/readme_zhtw.md index 222c75b97..6d08f5854 100644 --- a/docs/readme/readme_zhtw.md +++ b/docs/readme/readme_zhtw.md @@ -46,12 +46,11 @@ snap install bruno # 在 Linux 上使用 Apt 安裝 sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys 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 - -sudo apt update -sudo apt install bruno +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 ``` ### 跨多個平台運行 🖥️ diff --git a/readme.md b/readme.md index aae5d2f17..3e6248bdc 100644 --- a/readme.md +++ b/readme.md @@ -61,12 +61,11 @@ flatpak install com.usebruno.Bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings +sudo apt update && sudo apt install gpg +sudo gpg --list-keys sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266 - 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 +sudo apt update && sudo apt install bruno ``` ### Run across multiple platforms 🖥️ From d0419edb9289aaa4f959ade71bec551f5a2f7a40 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 4 Feb 2025 17:49:40 +0530 Subject: [PATCH 002/214] fix: correct variable used in collection name update --- packages/bruno-electron/src/ipc/collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c7454c113..472a31bc5 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -141,7 +141,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Change new name of collection let brunoConfig = JSON.parse(content); brunoConfig.name = collectionName; - const cont = await stringifyJson(json); + const cont = await stringifyJson(brunoConfig); // write the bruno.json to new dir await writeFile(path.join(dirPath, 'bruno.json'), cont); From f6ab59ceda4837d59ed54caaf03b48db3b6b5970 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Thu, 6 Mar 2025 17:40:15 +0530 Subject: [PATCH 003/214] feat: update Windows build configuration to support custom installation path from GUI installer --- .../bruno-electron/electron-builder-config.js | 20 +++++++++++++++++-- packages/bruno-electron/package.json | 4 ++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/bruno-electron/electron-builder-config.js b/packages/bruno-electron/electron-builder-config.js index 57d3f6a0d..a6a99c112 100644 --- a/packages/bruno-electron/electron-builder-config.js +++ b/packages/bruno-electron/electron-builder-config.js @@ -37,8 +37,24 @@ const config = { win: { artifactName: '${name}_${version}_${arch}_win.${ext}', icon: 'resources/icons/png', - certificateFile: `${process.env.WIN_CERT_FILEPATH}`, - certificatePassword: `${process.env.WIN_CERT_PASSWORD}` + publisherName: 'Anoop MD', + target: [ + { + target: 'nsis', + arch: ['x64'] + } + ] + }, + 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 } }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index c3d2cc2e5..48965ad6b 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -3,6 +3,10 @@ "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", + "repository": { + "type": "git", + "url": "https://github.com/usebruno/bruno.git" + }, "private": true, "main": "src/index.js", "author": "Anoop M D (https://helloanoop.com/)", From b5861dae398fd21fddc7a3acb593f2283293a260 Mon Sep 17 00:00:00 2001 From: Jonathan Perlman Date: Tue, 15 Apr 2025 14:31:08 -0400 Subject: [PATCH 004/214] Fix Digest auth header field key value extraction --- packages/bruno-requests/src/auth/digestauth-helper.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/bruno-requests/src/auth/digestauth-helper.js b/packages/bruno-requests/src/auth/digestauth-helper.js index 25911a6b3..186281a6e 100644 --- a/packages/bruno-requests/src/auth/digestauth-helper.js +++ b/packages/bruno-requests/src/auth/digestauth-helper.js @@ -9,6 +9,13 @@ function stripQuotes(str) { return str.replace(/"/g, ''); } +function splitAuthHeaderKeyValue(str) { + const indexOfEqual = str.indexOf('='); + const key = str.substring(0, indexOfEqual).trim(); + const value = str.substring(indexOfEqual + 1); + return [key, value]; +} + function containsDigestHeader(response) { const authHeader = response?.headers?.['www-authenticate']; return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false; @@ -55,7 +62,7 @@ export function addDigestInterceptor(axiosInstance, request) { const authDetails = error.response.headers['www-authenticate'] .split(',') - .map((pair) => pair.split('=').map((item) => item.trim()).map(stripQuotes)) + .map((pair) => splitAuthHeaderKeyValue(pair).map((item) => item.trim()).map(stripQuotes)) .reduce((acc, [key, value]) => { const normalizedKey = key.toLowerCase().replace('digest ', ''); if (normalizedKey && value !== undefined) { From cb92e46f8d091cc4b7d5d10ab30249057b9bca49 Mon Sep 17 00:00:00 2001 From: poojabela Date: Wed, 30 Apr 2025 15:14:36 +0530 Subject: [PATCH 005/214] feat: add `req.getName` api --- .../bruno-converters/src/postman/postman-translations.js | 1 + .../postman-translations/postman-request.spec.js | 2 ++ .../bruno-electron/src/ipc/network/prepare-request.js | 1 + packages/bruno-js/src/bruno-request.js | 6 +++++- .../bruno-js/src/sandbox/quickjs/shims/bruno-request.js | 9 +++++++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index b741cd3b2..00b3b945e 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -26,6 +26,7 @@ const replacements = { 'pm\\.request\\.method': 'req.getMethod()', 'pm\\.request\\.headers': 'req.getHeaders()', 'pm\\.request\\.body': 'req.getBody()', + 'pm\\.info\\.requestName': 'req.getName()', // deprecated translations 'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(', 'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js index 3ee071640..93eed719a 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js @@ -7,6 +7,7 @@ describe('postmanTranslations - request commands', () => { const requestMethod = pm.request.method; const requestHeaders = pm.request.headers; const requestBody = pm.request.body; + const requestName = pm.info.requestName; pm.test('Request method is POST', function() { pm.expect(pm.request.method).to.equal('POST'); @@ -17,6 +18,7 @@ describe('postmanTranslations - request commands', () => { const requestMethod = req.getMethod(); const requestHeaders = req.getHeaders(); const requestBody = req.getBody(); + const requestName = req.getName(); test('Request method is POST', function() { expect(req.getMethod()).to.equal('POST'); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 749e32a6d..9efa56e45 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -301,6 +301,7 @@ const prepareRequest = async (item, collection = {}, abortController) => { method: request.method, url, headers, + name: item.name, pathParams: request?.params?.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index 32e40c19e..3ee2127ca 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -17,7 +17,7 @@ class BrunoRequest { this.method = req.method; this.headers = req.headers; this.timeout = req.timeout; - + this.name = req.name; /** * We automatically parse the JSON body if the content type is JSON * This is to make it easier for the user to access the body directly @@ -177,6 +177,10 @@ class BrunoRequest { getExecutionMode() { return this.req.__bruno__executionMode; } + + getName() { + return this.req.name; + } } module.exports = BrunoRequest; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js index e3f364fe7..4a51ae580 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js @@ -8,18 +8,21 @@ const addBrunoRequestShimToContext = (vm, req) => { const headers = marshallToVm(req.getHeaders(), vm); const body = marshallToVm(req.getBody(), vm); const timeout = marshallToVm(req.getTimeout(), vm); + const name = marshallToVm(req.getName(), vm); vm.setProp(reqObject, 'url', url); vm.setProp(reqObject, 'method', method); vm.setProp(reqObject, 'headers', headers); vm.setProp(reqObject, 'body', body); vm.setProp(reqObject, 'timeout', timeout); + vm.setProp(reqObject, 'name', name); url.dispose(); method.dispose(); headers.dispose(); body.dispose(); timeout.dispose(); + name.dispose(); let getUrl = vm.newFunction('getUrl', function () { return marshallToVm(req.getUrl(), vm); @@ -45,6 +48,12 @@ const addBrunoRequestShimToContext = (vm, req) => { vm.setProp(reqObject, 'getAuthMode', getAuthMode); getAuthMode.dispose(); + let getName = vm.newFunction('getName', function () { + return marshallToVm(req.getName(), vm); + }); + vm.setProp(reqObject, 'getName', getName); + getName.dispose(); + let setMethod = vm.newFunction('setMethod', function (method) { req.setMethod(vm.dump(method)); }); From 261a36c4350441542f11d97c69453ae3b762258a Mon Sep 17 00:00:00 2001 From: poojabela Date: Wed, 30 Apr 2025 15:29:10 +0530 Subject: [PATCH 006/214] add: getName in hint --- packages/bruno-app/src/components/CodeEditor/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index f574cf82f..330662256 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -58,6 +58,7 @@ if (!SERVER_RENDERED) { 'req.getTimeout()', 'req.setTimeout(timeout)', 'req.getExecutionMode()', + 'req.getName()', 'bru', 'bru.cwd()', 'bru.getEnvName()', From ba9362ccb2c9a0d54d0ef31f0595d4bfc44eef3e Mon Sep 17 00:00:00 2001 From: poojabela Date: Wed, 30 Apr 2025 15:36:44 +0530 Subject: [PATCH 007/214] add: getName in collection --- .../collection/scripting/api/req/getName.bru | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/bruno-tests/collection/scripting/api/req/getName.bru diff --git a/packages/bruno-tests/collection/scripting/api/req/getName.bru b/packages/bruno-tests/collection/scripting/api/req/getName.bru new file mode 100644 index 000000000..95e0369f5 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/req/getName.bru @@ -0,0 +1,17 @@ +meta { + name: getName + type: http + seq: 11 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + test("Check if request name is getName", function () { + expect(req.getName()).to.eql("getName"); + }); +} From e0fb379511cdff1c13dd846ac58f759db592023c Mon Sep 17 00:00:00 2001 From: poojabela Date: Wed, 30 Apr 2025 17:25:42 +0530 Subject: [PATCH 008/214] add: `bru.collectionName` api --- .../src/components/CodeEditor/index.js | 1 + .../bruno-cli/src/runner/prepare-request.js | 1 + .../bruno-cli/src/runner/run-single-request.js | 10 +++++++--- .../bruno-electron/src/ipc/network/index.js | 16 ++++++++++++---- packages/bruno-js/src/bru.js | 7 ++++++- packages/bruno-js/src/runtime/script-runtime.js | 10 ++++++---- packages/bruno-js/src/runtime/test-runtime.js | 5 +++-- .../bruno-js/src/sandbox/quickjs/shims/bru.js | 6 ++++++ .../scripting/api/bru/getCollectionName.bru | 17 +++++++++++++++++ 9 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 packages/bruno-tests/collection/scripting/api/bru/getCollectionName.bru diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 330662256..f8c13462e 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -81,6 +81,7 @@ if (!SERVER_RENDERED) { 'bru.getAssertionResults()', 'bru.getTestResults()', 'bru.sleep(ms)', + 'bru.getCollectionName()', 'bru.getGlobalEnvVar(key)', 'bru.setGlobalEnvVar(key, value)', 'bru.runner', diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 7e7f5d3ac..1725b53b7 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -31,6 +31,7 @@ const prepareRequest = (item = {}, collection = {}) => { method: request.method, url: request.url, headers: headers, + name: item.name, pathParams: request?.params?.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 774587614..a8e5aee01 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -60,6 +60,7 @@ const runSingleRequest = async function ( // run pre request script const requestScriptFile = get(request, 'script.req'); + const collectionName = collection?.brunoConfig?.name if (requestScriptFile?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); const result = await scriptRuntime.runRequestScript( @@ -71,7 +72,8 @@ const runSingleRequest = async function ( onConsoleLog, processEnvVars, scriptingConfig, - runSingleRequestByPathname + runSingleRequestByPathname, + collectionName ); if (result?.nextRequestName !== undefined) { nextRequestName = result.nextRequestName; @@ -425,7 +427,8 @@ const runSingleRequest = async function ( null, processEnvVars, scriptingConfig, - runSingleRequestByPathname + runSingleRequestByPathname, + collectionName ); if (result?.nextRequestName !== undefined) { nextRequestName = result.nextRequestName; @@ -475,7 +478,8 @@ const runSingleRequest = async function ( null, processEnvVars, scriptingConfig, - runSingleRequestByPathname + runSingleRequestByPathname, + collectionName ); testResults = get(result, 'results', []); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8fddd2d98..ab8d146e0 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -341,6 +341,7 @@ const registerNetworkIpc = (mainWindow) => { ) => { // run pre-request script let scriptResult; + const collectionName = collection?.name const requestScript = get(request, 'script.req'); if (requestScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); @@ -353,7 +354,8 @@ const registerNetworkIpc = (mainWindow) => { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ); mainWindow.webContents.send('main:script-environment-update', { @@ -447,6 +449,7 @@ const registerNetworkIpc = (mainWindow) => { // run post-response script const responseScript = get(request, 'script.res'); let scriptResult; + const collectionName = collection?.name if (responseScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); scriptResult = await scriptRuntime.runResponseScript( @@ -459,7 +462,8 @@ const registerNetworkIpc = (mainWindow) => { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ); mainWindow.webContents.send('main:script-environment-update', { @@ -706,6 +710,7 @@ const registerNetworkIpc = (mainWindow) => { } const testFile = get(request, 'tests'); + const collectionName = collection?.name if (typeof testFile === 'string') { const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); const testResults = await testRuntime.runTests( @@ -718,7 +723,8 @@ const registerNetworkIpc = (mainWindow) => { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ); !runInBackground && mainWindow.webContents.send('main:run-request-event', { @@ -1171,6 +1177,7 @@ const registerNetworkIpc = (mainWindow) => { } const testFile = get(request, 'tests'); + const collectionName = collection?.name if (typeof testFile === 'string') { const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); const testResults = await testRuntime.runTests( @@ -1183,7 +1190,8 @@ const registerNetworkIpc = (mainWindow) => { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ); if (testResults?.nextRequestName !== undefined) { diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 76360a97c..77255b3a1 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -4,7 +4,7 @@ const { interpolate } = require('@usebruno/common'); const variableNameRegex = /^[\w-.]*$/; class Bru { - constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables) { + constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName) { this.envVariables = envVariables || {}; this.runtimeVariables = runtimeVariables || {}; this.processEnvVars = cloneDeep(processEnvVars || {}); @@ -14,6 +14,7 @@ class Bru { this.globalEnvironmentVariables = globalEnvironmentVariables || {}; this.oauth2CredentialVariables = oauth2CredentialVariables || {}; this.collectionPath = collectionPath; + this.collectionName = collectionName; this.runner = { skipRequest: () => { this.skipRequest = true; @@ -159,6 +160,10 @@ class Bru { sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } + + getCollectionName() { + return this.collectionName; + } } module.exports = Bru; diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2a8d02a87..165dc2f34 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -48,14 +48,15 @@ class ScriptRuntime { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; - const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); + 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); const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []); @@ -181,14 +182,15 @@ class ScriptRuntime { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; - const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); + const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName); const req = new BrunoRequest(request); const res = new BrunoResponse(response); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index e2d1f4865..101dd4333 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -68,14 +68,15 @@ class TestRuntime { onConsoleLog, processEnvVars, scriptingConfig, - runRequestByItemPathname + runRequestByItemPathname, + collectionName ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; 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); + const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName); const req = new BrunoRequest(request); const res = new BrunoResponse(response); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index b5b807ec2..b8ffa76ab 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -17,6 +17,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getEnvName', getEnvName); getEnvName.dispose(); + let getCollectionName = vm.newFunction('getCollectionName', function () { + return marshallToVm(bru.getCollectionName(), vm); + }); + vm.setProp(bruObject, 'getCollectionName', getCollectionName); + getCollectionName.dispose(); + let getProcessEnv = vm.newFunction('getProcessEnv', function (key) { return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm); }); diff --git a/packages/bruno-tests/collection/scripting/api/bru/getCollectionName.bru b/packages/bruno-tests/collection/scripting/api/bru/getCollectionName.bru new file mode 100644 index 000000000..3302d38fd --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/getCollectionName.bru @@ -0,0 +1,17 @@ +meta { + name: getCollectionName + type: http + seq: 13 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + test("Check if collection name is bruno-testbench", function () { + expect(bru.getCollectionName()).to.eql("bruno-testbench"); + }); +} From 9e2982101288e77f627034e7f8fbd29f07e78f2a Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Tue, 6 May 2025 17:56:34 +0530 Subject: [PATCH 009/214] 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 010/214] 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 2de7ba0d0cd5ccf164466bc1b8db24c3d04f119a Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Tue, 13 May 2025 16:06:20 +0530 Subject: [PATCH 011/214] 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 012/214] 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 013/214] 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 56497991675a0cec37782bcf37ac56fc3a8a1c6b Mon Sep 17 00:00:00 2001 From: sanjai0py Date: Tue, 13 May 2025 20:02:29 +0530 Subject: [PATCH 014/214] 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 015/214] 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 99274248269ba90013325ac05df545fc7e98d2f4 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Wed, 14 May 2025 12:22:39 +0530 Subject: [PATCH 016/214] 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 017/214] 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 018/214] 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 019/214] 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 c85d9bcd84326ed9a9aa3a983196106bb6624037 Mon Sep 17 00:00:00 2001 From: pooja-bruno Date: Wed, 14 May 2025 16:01:42 +0530 Subject: [PATCH 020/214] 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 021/214] 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 022/214] 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 023/214] 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 1d12bebce4d89a19566206c6b0b8392e930b1dc3 Mon Sep 17 00:00:00 2001 From: betawait <40029520+betawait@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:13:30 +0900 Subject: [PATCH 024/214] Fix: Allow empty Content-Type when no body (#1693) By default Axios will set the Content-Type for POST/PUT/PATCH requests to "application/x-www-form-urlencoded" if the Content-Type header is not specified. This explicitly sets the content type to "false" when there the body mode is set to "none", and the user has not set an explicit content type themselves. Setting the content type to false directs Axios not to send a Content-Type header. --- .../src/ipc/network/interpolate-vars.js | 72 ++++++++++--------- .../src/ipc/network/prepare-request.js | 7 ++ .../tests/network/interpolate-vars.spec.js | 9 +++ .../tests/network/prepare-request.spec.js | 18 +++++ 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 78c5454ed..a7206ec22 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -69,45 +69,47 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc const contentType = getContentType(request.headers); - /* - We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode. - Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated. - */ - if (contentType.includes('json') && !Buffer.isBuffer(request.data)) { - if (typeof request.data === 'string') { - if (request.data.length) { - request.data = _interpolate(request.data, { + if (typeof contentType === 'string') { + /* + We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode. + Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated. + */ + if (contentType.includes('json') && !Buffer.isBuffer(request.data)) { + if (typeof request.data === 'string') { + if (request.data.length) { + request.data = _interpolate(request.data, { escapeJSONStrings: true }); + } + } else if (typeof request.data === 'object') { + try { + const jsonDoc = JSON.stringify(request.data); + const parsed = _interpolate(jsonDoc, { + escapeJSONStrings: true + }); + request.data = JSON.parse(parsed); + } catch (err) {} } - } else if (typeof request.data === 'object') { - try { - const jsonDoc = JSON.stringify(request.data); - const parsed = _interpolate(jsonDoc, { - escapeJSONStrings: true - }); - request.data = JSON.parse(parsed); - } catch (err) {} + } else if (contentType === 'application/x-www-form-urlencoded') { + if (typeof request.data === 'object') { + try { + forOwn(request?.data, (value, key) => { + request.data[key] = _interpolate(value); + }); + } catch (err) {} + } + } else if (contentType === 'multipart/form-data') { + if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { + try { + request.data = request?.data?.map(d => ({ + ...d, + value: _interpolate(d?.value) + })); + } catch (err) {} + } + } else { + request.data = _interpolate(request.data); } - } else if (contentType === 'application/x-www-form-urlencoded') { - if (typeof request.data === 'object') { - try { - forOwn(request?.data, (value, key) => { - request.data[key] = _interpolate(value); - }); - } catch (err) {} - } - } else if (contentType === 'multipart/form-data') { - if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { - try { - request.data = request?.data?.map(d => ({ - ...d, - value: _interpolate(d?.value) - })); - } catch (err) {} - } - } else { - request.data = _interpolate(request.data); } each(request.pathParams, (param) => { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 749e32a6d..d1c5b4b10 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -392,6 +392,13 @@ const prepareRequest = async (item, collection = {}, abortController) => { axiosRequest.data = graphqlQuery; } + // if the mode is 'none' then set the content-type header to false. #1693 + if (request.body.mode === 'none') { + if(!contentTypeDefined) { + axiosRequest.headers['content-type'] = false; + } + } + if (request.script) { axiosRequest.script = request.script; } diff --git a/packages/bruno-electron/tests/network/interpolate-vars.spec.js b/packages/bruno-electron/tests/network/interpolate-vars.spec.js index 7a769ea3a..a29029687 100644 --- a/packages/bruno-electron/tests/network/interpolate-vars.spec.js +++ b/packages/bruno-electron/tests/network/interpolate-vars.spec.js @@ -100,4 +100,13 @@ describe('interpolate-vars: interpolateVars', () => { }); }); }); + + describe('Handles content-type header set to false', () => { + it('Should result empty data', async () => { + const request = { method: 'POST', url: 'test', data: undefined, headers: { 'content-type': false } }; + + const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null); + expect(result.data).toEqual(undefined); + }); + }); }); diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js index 67ed5ea87..6fbd745a7 100644 --- a/packages/bruno-electron/tests/network/prepare-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-request.spec.js @@ -59,4 +59,22 @@ describe('prepare-request: prepareRequest', () => { expect(result).toEqual(expected); }); }); + + describe.each(['POST', 'PUT', 'PATCH'])('POST request with no body', (method) => { + it('Should set content-type header to false if method is ' + method + ' and there is no data in the body', async () => { + const request = { method: method, url: 'test-domain', body: { mode: 'none' } }; + const result = await prepareRequest({ request, collection: { pathname: '' } }); + expect(result.headers['content-type']).toEqual(false); + }); + it('Should respect the content-type header if explicitly set', async () => { + const request = { + method: method, + url: 'test-domain', + body: { mode: 'none' }, + headers: [{ name: 'content-type', value: 'application/json', enabled: true }] + }; + const result = await prepareRequest({ request, collection: { pathname: '' } }); + expect(result.headers['content-type']).toEqual('application/json'); + }); + }); }); \ No newline at end of file From d2eb2d2941531ea5805e9cfdd0e016f58408e3da Mon Sep 17 00:00:00 2001 From: anusree-bruno Date: Thu, 15 May 2025 14:11:53 +0530 Subject: [PATCH 025/214] 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 026/214] 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 027/214] 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 028/214] 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 0f318c26c2859ae21016a08d2607d1d844947398 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Fri, 16 May 2025 00:42:27 +0530 Subject: [PATCH 029/214] 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 030/214] 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 031/214] 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 032/214] 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 033/214] 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 034/214] 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 035/214] 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 036/214] 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 037/214] 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 038/214] 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 039/214] 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 040/214] 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 041/214] 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 042/214] 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 043/214] 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 044/214] 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 045/214] 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 046/214] 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 047/214] 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 048/214] 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 049/214] 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 050/214] 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 051/214] 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 052/214] 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 053/214] 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 054/214] 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 055/214] 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 056/214] 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 057/214] 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 058/214] 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 059/214] 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 060/214] 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 061/214] 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 1b52bb27f7d0325dfe92ca4b22061ed4091d002c Mon Sep 17 00:00:00 2001 From: Shruti Shahi Date: Sat, 24 May 2025 01:52:54 +0530 Subject: [PATCH 062/214] Added Hindi translation of Readme file --- docs/readme/readme_hi.md | 151 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/readme/readme_hi.md diff --git a/docs/readme/readme_hi.md b/docs/readme/readme_hi.md new file mode 100644 index 000000000..395f38dec --- /dev/null +++ b/docs/readme/readme_hi.md @@ -0,0 +1,151 @@ +
+ + +### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण। + +[![GitHub संस्करण](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) +[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) +[![कमिट गतिविधि](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) +[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) +[![वेबसाइट](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) +| [简体中文](./readme_cn.md) +| [正體中文](./readme_zhtw.md) +| [العربية](./readme_ar.md) +| [日本語](./readme_ja.md) +| [ქართული](./readme_ka.md) +| **हिन्दी** + +ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है। + +ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं। + +आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं। + +ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें। + +📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें। + +![bruno](/assets/images/landing-2.png)

+ +### गोल्डन संस्करण ✨ + +हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं। +हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं। + +[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी!
+[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें। + +### स्थापना + +ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है। + +आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं। + +```sh +# Mac पर Homebrew के माध्यम से +brew install bruno + +# Windows पर Chocolatey के माध्यम से +choco install bruno + +# Windows पर Scoop के माध्यम से +scoop bucket add extras +scoop install bruno + +# Linux पर Snap के माध्यम से +snap install bruno + +# Linux पर Flatpak के माध्यम से +flatpak install com.usebruno.Bruno + +# Linux पर 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 + +sudo apt update +sudo apt install bruno + +कई प्लेटफार्मों पर चलाएं 🖥️ +

+ +Git के माध्यम से सहयोग करें 👩‍💻🧑‍💻 +या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें + +

+ +महत्वपूर्ण लिंक 📌 +हमारी दीर्घकालिक दृष्टि + +रोडमैप + +प्रलेखन + +Stack Overflow + +वेबसाइट + +मूल्य निर्धारण + +डाउनलोड + +GitHub प्रायोजक + +प्रस्तुतियाँ 🎥 +प्रशंसापत्र + +ज्ञान केंद्र + +Scriptmania + +समर्थन ❤️ +यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें। + +प्रशंसापत्र साझा करें 📣 +यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें + +नए पैकेज प्रबंधकों में प्रकाशित करना +अधिक जानकारी के लिए कृपया यहाँ देखें। + +हमसे संपर्क करें 🌐 +𝕏 (ट्विटर)
+वेबसाइट
+डिस्कॉर्ड
+लिंक्डइन + +ट्रेडमार्क +नाम + +ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है। + +लोगो + +लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0 + +योगदान 👩‍💻🧑‍💻 +हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें। + +यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए। + +लेखक + + +लाइसेंस 📄 +MIT + From 0948964677dc6c8c8995587ea525c6a5c2d66776 Mon Sep 17 00:00:00 2001 From: devendra-bruno Date: Mon, 26 May 2025 09:47:43 +0530 Subject: [PATCH 063/214] 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 064/214] 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 065/214] 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 066/214] 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 067/214] 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 068/214] 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 069/214] 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 070/214] 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 071/214] 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 072/214] 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 073/214] 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 074/214] 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 075/214] 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 076/214] 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 077/214] 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 078/214] 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 079/214] 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 080/214] 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 081/214] 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 082/214] 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 083/214] 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 084/214] 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 085/214] 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 086/214] 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 087/214] 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 088/214] 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 089/214] 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 090/214] 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 9e628fa6be2c0dc65e5108129be103e6c85a7e09 Mon Sep 17 00:00:00 2001 From: georgegiosue Date: Sun, 8 Jun 2025 12:36:55 -0500 Subject: [PATCH 091/214] docs(contributing): update Spanish contribution guide for clarity and accuracy --- docs/contributing/contributing_es.md | 133 ++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/docs/contributing/contributing_es.md b/docs/contributing/contributing_es.md index f080a765c..fc1d9866b 100644 --- a/docs/contributing/contributing_es.md +++ b/docs/contributing/contributing_es.md @@ -1,4 +1,4 @@ -[English](../../contributing.md) +[Inglés](../../contributing.md) ## ¡Juntos, hagamos a Bruno mejor! @@ -6,58 +6,111 @@ Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encont ### Tecnologías utilizadas -Bruno está construido con NextJs y React. También usamos electron para distribuir una versión de escritorio (que soporta colecciones locales). +Bruno está construido con React y Electron Librerías que utilizamos: -- CSS - Tailwind -- Editores de código - Codemirror +- CSS - Tailwind CSS +- Editores de código - CodeMirror - Manejo del estado - Redux - Íconos - Tabler Icons - Formularios - formik - Validación de esquemas - Yup - Cliente de peticiones - axios - Monitor del sistema de archivos - chokidar +- i18n (internacionalización) - i18next ### Dependencias -Necesitarás [Node v20.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto. +> [!IMPORTANT] +> Necesitarás [Node v22.x o la última versión LTS](https://nodejs.org/es/). Ten en cuenta que Bruno usa los espacios de trabajo de npm ## Desarrollo -Bruno está siendo desarrollado como una aplicación de escritorio. Para ejecutarlo, primero debes ejecutar la aplicación de nextjs en una terminal y luego ejecutar la aplicación de electron en otra terminal. +Bruno es una aplicación de escritorio. A continuación se detallan las instrucciones paso a paso para ejecutar Bruno. -### Dependencias +> Nota: Utilizamos React para el frontend y rsbuild para el servidor de desarrollo. -- NodeJS v18 +### Instalar dependencias + +```bash +# Use la versión 22.x o LTS (Soporte a Largo Plazo) de Node.js +nvm use 22.11.0 + +# instalar las dependencias +npm i --legacy-peer-deps +``` + +> ¿Por qué `--legacy-peer-deps`?: Fuerza la instalación ignorando conflictos en dependencias “peer”, evitando errores de árbol de dependencias. ### Desarrollo local +#### Construir paquetes + +##### Opción 1 + ```bash -# Utiliza la versión 18 de nodejs -nvm use - -# Instala las dependencias -npm i --legacy-peer-deps - -# Construye la documentación de graphql +# construir paquetes npm run build:graphql-docs - -# Construye bruno-query npm run build:bruno-query +npm run build:bruno-common +npm run build:bruno-converters +npm run build:bruno-requests -# Ejecuta la aplicación de nextjs (terminal 1) +# empaquetar bibliotecas JavaScript del entorno de pruebas aislado +npm run sandbox:bundle-libraries --workspace=packages/bruno-js +``` + +##### Opción 2 + +```bash +# instalar dependencias y configurar el entorno +npm run setup +``` + +#### Ejecutar la aplicación + +```bash +# ejecutar aplicación react (terminal 1) npm run dev:web -# Ejecuta la aplicación de electron (terminal 2) +# ejecutar aplicación electron (terminal 2) npm run dev:electron ``` +##### Opción 1 + +```bash +# ejecutar aplicación react (terminal 1) +npm run dev:web + +# ejecutar aplicación electron (terminal 2) +npm run dev:electron +``` + +##### Opción 2 + +```bash +# ejecutar aplicación electron y react de forma concurrente +npm run dev +``` + +#### Personalizar la ruta `userData` de Electron + +Si la variable de entorno `ELECTRON_USER_DATA_PATH` está presente y se encuentra en modo de desarrollo, entonces la ruta `userData` se modifica en consecuencia. +ejemplo: + +```sh +ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron +``` + +Esto creará una carpeta llamada `bruno-test` en tu escritorio y la usará como la ruta userData. + ### Solución de problemas -Es posible que encuentres un error de `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, debes eliminar la carpeta `node_modules` y el archivo `package-lock.json`, luego, ejecuta `npm install`. Lo anterior debería instalar todos los paquetes necesarios para ejecutar la aplicación. +Es posible que te encuentres con un error `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, tendrás que eliminar las carpetas `node_modules` y el archivo `package-lock.json`, y luego volver a ejecutar `npm install`. Esto debería instalar todos los paquetes necesarios para que la aplicación funcione. -```shell +```sh # Elimina la carpeta node_modules en los subdirectorios find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" @@ -69,10 +122,42 @@ find . -type f -name "package-lock.json" -delete ### Pruebas -```bash -# ejecutar pruebas de esquema bruno -npm test --workspace=packages/bruno-schema +#### Pruebas individuales +```bash +# ejecutar pruebas de bruno-app +npm run test --workspace=packages/bruno-app + +# ejecutar pruebas de bruno-electron +npm run test --workspace=packages/bruno-electron + +# ejecutar pruebas de bruno-cli +npm run test --workspace=packages/bruno-cli + +# ejecutar pruebas de bruno-common +npm run test --workspace=packages/bruno-common + +# ejecutar pruebas de bruno-converters +npm run test --workspace=packages/bruno-converters + +# ejecutar pruebas de bruno-schema +npm run test --workspace=packages/bruno-schema + +# ejecutar pruebas de bruno-query +npm run test --workspace=packages/bruno-query + +# ejecutar pruebas de bruno-js +npm run test --workspace=packages/bruno-js + +# ejecutar pruebas de bruno-lang +npm run test --workspace=packages/bruno-lang + +# ejecutar pruebas de bruno-toml +npm run test --workspace=packages/bruno-toml +``` +#### Pruebas en conjunto + +```bash # ejecutar pruebas en todos los espacios de trabajo npm test --workspaces --if-present ``` From e4ae857df35a4944b6817df5810260fdde17e808 Mon Sep 17 00:00:00 2001 From: Pooja Date: Mon, 9 Jun 2025 13:50:25 +0530 Subject: [PATCH 092/214] 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 093/214] `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 094/214] 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 095/214] 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 096/214] 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) { From 9801e91720be606c9a81417941e4d8f5cf9b2593 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Mon, 9 Jun 2025 17:54:52 +0545 Subject: [PATCH 097/214] feat: add prompt for handling large responses - Add `formatSize` utility function to format response size - Add unit tests for `formatSize` utility function --- .../LargeResponseWarning/StyledWrapper.js | 65 +++++++++++++ .../LargeResponseWarning/index.js | 92 +++++++++++++++++++ .../ResponsePane/QueryResult/index.js | 27 ++++++ packages/bruno-app/src/utils/common/index.js | 19 ++++ .../bruno-app/src/utils/common/index.spec.js | 38 +++++++- 5 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js diff --git a/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js new file mode 100644 index 000000000..0a5339df8 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + .warning-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 1.5rem; + margin-top: 10%; + text-align: center; + max-width: 480px; + } + + .warning-icon { + margin-bottom: 1rem; + color: ${(props) => props.theme.colors.text.yellow}; + } + + .warning-title { + font-weight: 600; + color: ${(props) => props.theme.text}; + margin-bottom: 1rem; + } + + .warning-description { + color: ${(props) => props.theme.colors.text.muted}; + + .size-highlight { + padding: 0.125rem 0.375rem; + border-radius: 4px; + font-size: 0.8rem; + } + + .current-size { + color: ${(props) => props.theme.colors.text.danger}; + background: ${(props) => props.theme.colors.text.danger}15; + } + + .supported-size { + color: ${(props) => props.theme.colors.text.yellow}; + background: ${(props) => props.theme.colors.text.yellow}15; + } + } + + .warning-actions { + display: flex; + gap: 0.75rem; + } + + button { + align-items: center; + display: flex; + gap: 0.5rem; + background: ${(props) => props.theme.button.secondary.bg}; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js new file mode 100644 index 000000000..1686b4a38 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { IconDownload, IconCopy, IconEye, IconAlertTriangle } from '@tabler/icons'; +import toast from 'react-hot-toast'; +import get from 'lodash/get'; +import StyledWrapper from './StyledWrapper'; +import { formatSize } from 'utils/common/index'; + +const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => { + const { ipcRenderer } = window; + const response = item.response || {}; + + const saveResponseToFile = () => { + return new Promise((resolve, reject) => { + ipcRenderer + .invoke('renderer:save-response-to-file', response, item.requestSent.url) + .then(() => { + toast.success('Response saved to file'); + resolve(); + }) + .catch((err) => { + toast.error(get(err, 'error.message') || 'Something went wrong!'); + reject(err); + }); + }); + }; + + const copyResponse = () => { + try { + const textToCopy = typeof response.data === 'string' + ? response.data + : JSON.stringify(response.data, null, 2); + + navigator.clipboard.writeText(textToCopy).then(() => { + toast.success('Response copied to clipboard'); + }).catch(() => { + toast.error('Failed to copy response'); + }); + } catch (error) { + toast.error('Failed to copy response'); + } + }; + + return ( + +
    +
    + +
    +
    +
    + Large Response Warning +
    +
    + Handling responses over {formatSize(10 * 1024 * 1024)} could degrade performance. +
    + Size of current response: {formatSize(responseSize)} +
    +
    +
    +
    + + + +
    +
    + ); +}; + +export default LargeResponseWarning; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index 1407e69ae..229a40072 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -11,6 +11,7 @@ import StyledWrapper from './StyledWrapper'; import { useState, useMemo, useEffect } from 'react'; import { useTheme } from 'providers/Theme/index'; import { getEncoding, uuid } from 'utils/common/index'; +import LargeResponseWarning from '../LargeResponseWarning'; const formatResponse = (data, dataBuffer, encoding, mode, filter) => { if (data === undefined || !dataBuffer || !mode) { @@ -77,6 +78,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven const contentType = getContentType(headers); const mode = getCodeMirrorModeBasedOnContentType(contentType, data); const [filter, setFilter] = useState(null); + const [showLargeResponse, setShowLargeResponse] = useState(false); const responseEncoding = getEncoding(headers); const formattedData = useMemo( () => formatResponse(data, dataBuffer, responseEncoding, mode, filter), @@ -84,6 +86,25 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ); const { displayedTheme } = useTheme(); + const responseSize = useMemo(() => { + const response = item.response || {}; + if (typeof response.size === 'number') { + return response.size; + } + + if (!dataBuffer) return 0; + + try { + // dataBuffer is base64 encoded, so we need to calculate the actual size + const buffer = Buffer.from(dataBuffer, 'base64'); + return buffer.length; + } catch (error) { + return 0; + } + }, [dataBuffer, item.response]); + + const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB + const debouncedResultFilterOnChange = debounce((e) => { setFilter(e.target.value); }, 250); @@ -160,6 +181,12 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
    ) : null}
+ ) : isLargeResponse && !showLargeResponse ? ( + setShowLargeResponse(true)} + /> ) : (
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 937e1a3b5..f839ba850 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -196,4 +196,23 @@ export const getEncoding = (headers) => { export const multiLineMsg = (...messages) => { return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n'); +} + +export const formatSize = (bytes) => { + // Handle invalid inputs + if (isNaN(bytes) || typeof bytes !== 'number') { + return '0B'; + } + + if (bytes < 1024) { + return bytes + 'B'; + } + if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + 'KB'; + } + if (bytes < 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(1) + 'MB'; + } + + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB'; } \ No newline at end of file diff --git a/packages/bruno-app/src/utils/common/index.spec.js b/packages/bruno-app/src/utils/common/index.spec.js index 81153674a..89eeebf2e 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, getContentType } from './index'; +import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType, formatSize } from './index'; describe('common utils', () => { describe('normalizeFileName', () => { @@ -148,4 +148,40 @@ describe('common utils', () => { expect(getContentType(undefined)).toBe(''); }); }); + + describe('formatSize', () => { + it('should format bytes', () => { + expect(formatSize(0)).toBe('0B'); + expect(formatSize(1023)).toBe('1023B'); + }); + + it('should format kilobytes', () => { + expect(formatSize(1024)).toBe('1.0KB'); + expect(formatSize(1048575)).toBe('1024.0KB'); + }); + + it('should format megabytes', () => { + expect(formatSize(1048576)).toBe('1.0MB'); + expect(formatSize(1073741823)).toBe('1024.0MB'); + }); + + it('should format gigabytes', () => { + expect(formatSize(1073741824)).toBe('1.0GB'); + expect(formatSize(1099511627776)).toBe('1024.0GB'); + }); + + it('should format decimal values', () => { + expect(formatSize(1126.5)).toBe('1.1KB'); + expect(formatSize(1153433.6)).toBe('1.1MB'); + expect(formatSize(1153433600)).toBe('1.1GB'); + expect(formatSize(1024.1)).toBe('1.0KB'); + expect(formatSize(1048576.1)).toBe('1.0MB'); + }); + + it('should format invalid inputs', () => { + expect(formatSize(null)).toBe('0B'); + expect(formatSize(undefined)).toBe('0B'); + expect(formatSize(NaN)).toBe('0B'); + }); + }); }); From e1c12ea6998eebdc06de9074b4e9375e781c7476 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Wed, 11 Jun 2025 20:59:50 +0545 Subject: [PATCH 098/214] fix: update danger color in light theme --- packages/bruno-app/src/themes/light.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index e95b0e45e..55b1d0eaf 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -7,7 +7,7 @@ const lightTheme = { colors: { text: { green: '#047857', - danger: 'rgb(185, 28, 28)', + danger: '#B91C1C', muted: '#838383', purple: '#8e44ad', yellow: '#d97706' From a598cda62464096dd018ad3d301a6a89661e77aa Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 13 Jun 2025 14:16:02 +0530 Subject: [PATCH 099/214] fix: handle undefined bearer token to send an empty string instead --- packages/bruno-cli/src/runner/prepare-request.js | 4 ++-- packages/bruno-electron/src/ipc/network/prepare-request.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 5de2f3a42..9c2493a35 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -47,7 +47,7 @@ const prepareRequest = (item = {}, collection = {}) => { } if (collectionAuth.mode === 'bearer') { - axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; + axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`; } if (collectionAuth.mode === 'apikey') { @@ -174,7 +174,7 @@ const prepareRequest = (item = {}, collection = {}) => { } if (request.auth.mode === 'bearer') { - axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`; } if (request.auth.mode === 'wsse') { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 9efa56e45..25926689b 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -27,7 +27,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { }; break; case 'bearer': - axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; + axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`; break; case 'digest': axiosRequest.digestConfig = { @@ -152,7 +152,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { }; break; case 'bearer': - axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`; break; case 'digest': axiosRequest.digestConfig = { From 66fe1528dffd985e8cbd4bf15a77031df040602d Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 13 Jun 2025 14:42:57 +0530 Subject: [PATCH 100/214] add: new Bearer Auth undefined test case and update Authorization header format --- .../bearer/via auth/Bearer Auth undefined.bru | 27 +++++++++++++++++++ .../bearer/via headers/Bearer Auth 200.bru | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth undefined.bru diff --git a/packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth undefined.bru b/packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth undefined.bru new file mode 100644 index 000000000..43298cfd4 --- /dev/null +++ b/packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth undefined.bru @@ -0,0 +1,27 @@ +meta { + name: Bearer Auth undefined + type: http + seq: 2 +} + +get { + url: {{host}}/api/auth/bearer/protected + body: none + auth: bearer +} + +headers { + Authorization: Bearer {{bearer_auth_token}} +} + +assert { + res.body.message: eq Unauthorized + res.status: eq 401 +} + +tests { + test("selected auth overrides Authorization header always", function() { + const authHeader = req.getHeader("Authorization") + expect(authHeader).to.eql("Bearer ") + }) +} diff --git a/packages/bruno-tests/collection/auth/bearer/via headers/Bearer Auth 200.bru b/packages/bruno-tests/collection/auth/bearer/via headers/Bearer Auth 200.bru index a837bdd7e..0989e0fba 100644 --- a/packages/bruno-tests/collection/auth/bearer/via headers/Bearer Auth 200.bru +++ b/packages/bruno-tests/collection/auth/bearer/via headers/Bearer Auth 200.bru @@ -11,7 +11,7 @@ get { } headers { - Authorization: Bearer your_secret_token + Authorization: Bearer {{bearer_auth_token}} } vars:pre-request { From 5313704d846066e559093601b61d33dbac5a92da Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 14 Jun 2025 13:25:21 +0530 Subject: [PATCH 101/214] Fix watcher error message typo --- packages/bruno-electron/src/app/watcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index a2f91d953..05fe9ebe8 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -565,7 +565,7 @@ class Watcher { `\nCould not start watcher for ${watchPath}:`, 'ENOSPC: System limit for number of file watchers reached!', 'Trying again with polling, this will be slower!\n', - 'Update you system config to allow more concurrently watched files with:', + 'Update your system config to allow more concurrently watched files with:', '"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"' ); this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread); From f03047a2f98a79e8d187b5609b50683c0777cfa4 Mon Sep 17 00:00:00 2001 From: lohit Date: Sat, 14 Jun 2025 22:18:31 +0530 Subject: [PATCH 102/214] feat: `bru.sendRequest` api (#4867) * feat: bru.sendRequest api * updated the postman-translations logic to handle `pm.sendRequest` to `bru.sendRequest` translations, and added unit tests * ~ removed `maxRedirects` and `proxy` values for sendRequest axios-instance ~ fixed the imports for the `send-request-transformer` function ~ `sendRequest` and `runRequest` will return same response object in both safe and developer mode ~ sendRequest function optimization * revert sendRequest to async function, added a testcase for sendRequest with url string * sendRequest callback errors handling * updated tests and added await for the callbacks --------- Co-authored-by: lohit --- package-lock.json | 14 +- .../src/utils/jscode-shift-translator.js | 8 + .../src/utils/send-request-transformer.js | 284 +++++++++ .../transformers/send-request.test.js | 592 ++++++++++++++++++ packages/bruno-js/src/bru.js | 2 + .../bruno-js/src/sandbox/quickjs/index.js | 2 +- .../bruno-js/src/sandbox/quickjs/shims/bru.js | 48 +- packages/bruno-js/src/utils.js | 28 +- packages/bruno-requests/package.json | 3 +- packages/bruno-requests/src/index.ts | 4 + .../src/network/axios-instance.ts | 48 ++ packages/bruno-requests/src/network/index.ts | 1 + .../bruno-requests/src/scripting/index.ts | 1 + .../src/scripting/send-request.ts | 30 + .../scripting/api/bru/send-request/folder.bru | 8 + .../api/bru/send-request/get-url-string.bru | 18 + .../api/bru/send-request/usage-patterns.bru | 80 +++ 17 files changed, 1164 insertions(+), 7 deletions(-) create mode 100644 packages/bruno-converters/src/utils/send-request-transformer.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js create mode 100644 packages/bruno-requests/src/network/axios-instance.ts create mode 100644 packages/bruno-requests/src/network/index.ts create mode 100644 packages/bruno-requests/src/scripting/index.ts create mode 100644 packages/bruno-requests/src/scripting/send-request.ts create mode 100644 packages/bruno-tests/collection/scripting/api/bru/send-request/folder.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/send-request/get-url-string.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/send-request/usage-patterns.bru diff --git a/package-lock.json b/package-lock.json index fe4a15fd8..2e4a5e517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32748,7 +32748,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@types/qs": "^6.9.18" + "@types/qs": "^6.9.18", + "axios": "^1.9.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", @@ -32761,6 +32762,17 @@ "typescript": "^4.8.4" } }, + "packages/bruno-requests/node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-schema": { "name": "@usebruno/schema", "version": "0.7.0", diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 92ccf97ba..4d68c2b68 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -1,3 +1,4 @@ +import sendRequestTransformer from './send-request-transformer'; const j = require('jscodeshift'); const cloneDeep = require('lodash/cloneDeep'); @@ -99,7 +100,14 @@ const simpleTranslations = { * 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.sendRequest transformation + { + pattern: 'pm.sendRequest', + transform: sendRequestTransformer + }, + // pm.environment.has requires special handling { pattern: 'pm.environment.has', diff --git a/packages/bruno-converters/src/utils/send-request-transformer.js b/packages/bruno-converters/src/utils/send-request-transformer.js new file mode 100644 index 000000000..cb5ecd19b --- /dev/null +++ b/packages/bruno-converters/src/utils/send-request-transformer.js @@ -0,0 +1,284 @@ +/** + * Convert Postman header array format to Bruno headers object + * @param {Object} j - jscodeshift API + * @param {Object} arrayValue - Array expression of key-value pair objects + * @returns {Object} - Object expression with key-value pairs + */ +const convertArrayToObject = (j, arrayValue) => { + const obj = j.objectExpression([]); + + if (arrayValue.type === 'ArrayExpression') { + arrayValue.elements.forEach(elem => { + if (elem.type === 'ObjectExpression') { + const keyProp = elem.properties.find(p => (p.key.name === 'key' || p.key.value === 'key')); + const valueProp = elem.properties.find(p => (p.key.name === 'value' || p.key.value === 'value')); + + if (keyProp && valueProp) { + obj.properties.push( + j.property( + 'init', + j.literal(keyProp.value.value), + valueProp.value + ) + ); + } + } + }); + } + + return obj; +}; + +/** + * Add or update a specific header in the request options + * @param {Object} j - jscodeshift API + * @param {Object} requestOptions - Request options object + * @param {string} headerName - Header name to add/update + * @param {string} headerValue - Header value + */ +const addOrUpdateHeader = (j, requestOptions, headerName, headerValue) => { + let headersProp = requestOptions.properties.find(p => (p.key.name === 'headers' || p.key.value === 'headers')); + + if (!headersProp) { + headersProp = j.property('init', j.identifier('headers'), j.objectExpression([])); + requestOptions.properties.push(headersProp); + } else if (headersProp.value.type !== 'ObjectExpression') { + headersProp.value = j.objectExpression([]); + } + + // filter out existing header with same name (case-insensitive) + headersProp.value.properties = headersProp.value.properties.filter(p => + p.key.type !== 'Literal' || + p.key.value.toLowerCase() !== headerName.toLowerCase() + ); + + headersProp.value.properties.push( + j.property( + 'init', + j.literal(headerName), + j.literal(headerValue) + ) + ); +}; + +/** + * Transform headers property from array to object format + * @param {Object} j - jscodeshift API + * @param {Object} requestOptions - Request options object + */ +const transformHeaders = (j, requestOptions) => { + if (requestOptions.type !== 'ObjectExpression') return; + + requestOptions.properties.forEach(prop => { + // find and rename 'header' property to 'headers' + if (prop.key.name === 'header' || prop.key.value === 'header') { + prop.key.name = 'headers'; + prop.key.value = 'headers'; + + // Handle array of header objects + if (prop.value.type === 'ArrayExpression') { + prop.value = convertArrayToObject(j, prop.value); + } + } + }); +}; + +/** + * Transform body property based on body mode + * @param {Object} j - jscodeshift API + * @param {Object} requestOptions - Request options object + * @returns {Array|null} - Array of statements if formdata is used, null otherwise + */ +const transformBody = (j, requestOptions) => { + if (requestOptions.type !== 'ObjectExpression') return null; + + requestOptions.properties.forEach(prop => { + if (prop.key.name === 'body' || prop.key.value === 'body') { + if (prop.value.type === 'ObjectExpression') { + const bodyProps = prop.value.properties; + const modeProp = bodyProps.find(p => (p.key.name === 'mode' || p.key.value === 'mode')); + + if (modeProp && modeProp.value.type === 'Literal') { + const bodyMode = modeProp.value.value; + + // Handle raw mode (text, json, xml, etc.) + if (bodyMode === 'raw') { + const rawProp = bodyProps.find(p => (p.key.name === 'raw' || p.key.value === 'raw')); + + if (rawProp) { + // Replace body with data + prop.key.name = 'data'; + prop.key.value = 'data'; + prop.value = rawProp.value; + } + } + // Handle urlencoded mode + else if (bodyMode === 'urlencoded') { + const urlencodedProp = bodyProps.find(p => (p.key.name === 'urlencoded' || p.key.value === 'urlencoded') && p.value.type === 'ArrayExpression'); + + if (urlencodedProp) { + // Replace the body property with a 'data' property + prop.key.name = 'data'; + prop.key.value = 'data'; + + // Transform the urlencoded array to an object + prop.value = convertArrayToObject(j, urlencodedProp.value); + + // Add Content-Type header for urlencoded + addOrUpdateHeader(j, requestOptions, 'Content-Type', 'application/x-www-form-urlencoded'); + } + } + // Handle formdata mode + else if (bodyMode === 'formdata') { + const formdataProp = bodyProps.find(p => (p.key.name === 'formdata' || p.key.value === 'formdata') && p.value.type === 'ArrayExpression'); + + if (formdataProp) { + // Replace the body property with a 'data' property + prop.key.name = 'data'; + prop.key.value = 'data'; + + // Transform the urlencoded array to an object + prop.value = convertArrayToObject(j, formdataProp.value); + + // Add Content-Type header for urlencoded + addOrUpdateHeader(j, requestOptions, 'Content-Type', 'multipart/form-data'); + } + } + } + } + } + }); +}; + +/** + * Transform callback function to Bruno format + * @param {Object} j - jscodeshift API + * @param {Object} callback - Callback function expression + * @returns {Object} - Transformed callback function + */ +const transformCallback = (j, callback) => { + if (!callback || callback.type !== 'FunctionExpression') return null; + + const params = callback.params; + const callbackBody = callback.body; + + // Get the response parameter name (typically the second param) + let responseVarName = 'response'; // Default if not found + if (params.length >= 2 && params[1].type === 'Identifier') { + responseVarName = params[1].name; + } + + let errorVarName = 'error'; // Default if not found + if (params.length >= 1 && params[0].type === 'Identifier') { + errorVarName = params[0].name; + } + + // Define translations for callback response properties + const responsePropertyMap = { + 'json': 'getBody', + 'text': 'getBody', + 'code': 'getStatus()', + 'status': 'statusText', + 'responseTime': 'getResponseTime()', + 'statusText': 'statusText', + 'headers': 'getHeaders()', + }; + + // Process the callback body to transform response property references + j(callbackBody).find(j.MemberExpression, { + object: { + type: 'Identifier', + name: responseVarName + } + }).forEach(memberPath => { + const property = memberPath.node.property; + + // Handle property access + if (property.type === 'Identifier' && responsePropertyMap[property.name]) { + const bruProperty = responsePropertyMap[property.name]; + + // If it's a method call (with parentheses) + if (bruProperty.endsWith('()')) { + // If it's already being called (e.g., response.json()) + if (memberPath.parent.node.type === 'CallExpression' && + memberPath.parent.node.callee === memberPath.node) { + // Replace with method call: res.getBody() + j(memberPath.parent).replaceWith( + j.callExpression( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(bruProperty.slice(0, -2)) + ), + [] + ) + ); + } else { + // Replace with method call: res.getBody() + j(memberPath).replaceWith( + j.callExpression( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(bruProperty.slice(0, -2)) + ), + [] + ) + ); + } + } else { + // Replace with property access: res.statusText + j(memberPath).replaceWith( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(bruProperty) + ) + ); + } + } + }); + + // Create the callback block + return j.functionExpression( + null, + [j.identifier(errorVarName), j.identifier(responseVarName)], + j.blockStatement(callbackBody.body) + ); +}; + +const sendRequestTransformer = (path, j) => { + const callExpr = path.parent.value; + if (callExpr.type !== 'CallExpression') return; + + // Clone the argument object for modification + const args = [...callExpr.arguments]; + if (!args.length) return; + + const requestOptions = args[0]; + const callback = args[1]; + + // transform the request config options + if (requestOptions.type === 'ObjectExpression') { + // Transform headers + transformHeaders(j, requestOptions); + // Transform body + transformBody(j, requestOptions); + } + + // Create the callback block and promise chain if there's a callback + if (callback) { + const transformedCallback = transformCallback(j, callback); + + // Create expression: bru.sendRequest(requestConfig, callback); + return j.callExpression( + j.identifier('bru.sendRequest'), + transformedCallback ? [requestOptions, transformedCallback] : [requestOptions] + ); + } + + // If there's no callback, just transform to bru.sendRequest + return j.callExpression( + j.identifier('bru.sendRequest'), + [requestOptions] + ); +}; + +export default sendRequestTransformer; \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js new file mode 100644 index 000000000..90f54e38b --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js @@ -0,0 +1,592 @@ +import translateCode from '../../../../../src/utils/jscode-shift-translator'; + +describe('Send Request Translation', () => { + describe('Raw Body Mode', () => { + it('should transform raw JSON string body', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: { + mode: 'raw', + raw: JSON.stringify({ + "x": 1 + }) + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + data: JSON.stringify({ + "x": 1 + }) + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform raw JSON object body', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: { + mode: 'raw', + raw: { + "x": 1 + } + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + data: { + "x": 1 + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform raw text body', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'text/plain', + }, + body: { + mode: 'raw', + raw: 'Hello World' + } + }, function (error, response) { + console.log(response.text()); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + data: 'Hello World' + }, function(error, response) { + console.log(response.getBody()); + }); + `); + }); + }); + + describe('URL-encoded Body Mode', () => { + it('should transform urlencoded body with single key-value pair', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + mode: 'urlencoded', + urlencoded: [ + { key: "key", value: "value" } + ] + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Accept': 'application/json', + "Content-Type": "application/x-www-form-urlencoded", + }, + data: { + "key": "value" + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform urlencoded body with multiple key-value pairs', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: {}, + body: { + mode: 'urlencoded', + urlencoded: [ + { key: "firstName", value: "John" }, + { key: "lastName", value: "Doe" }, + { key: "email", value: "john.doe@example.com" } + ] + } + }, function (error, response) { + console.log(response.json()); + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data: { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + }, function(error, response) { + console.log(response.getBody()); + }); + `); + }); + + it('should transform urlencoded body when no Content-Type header exists', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + body: { + mode: 'urlencoded', + urlencoded: [ + { key: "key1", value: "value1" }, + { key: "key2", value: "value2" } + ] + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + + data: { + "key1": "value1", + "key2": "value2" + }, + + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }); + `); + }); + + it('should transform urlencoded body with incorrect Content-Type header', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "text/plain" + }, + body: { + mode: 'urlencoded', + urlencoded: [ + { key: "key1", value: "value1" }, + { key: "key2", value: "value2" } + ] + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data: { + "key1": "value1", + "key2": "value2" + } + }); + `); + }); + }); + + describe('Multi-part Form Data Body Mode', () => { + it('should transform formdata body with single key-value pair', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'multipart/form-data', + }, + body: { + mode: 'formdata', + formdata: [ + { key: "key", value: "value" } + ] + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "multipart/form-data", + }, + data: { + "key": "value" + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform formdata body with multiple key-value pair', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'multipart/form-data', + }, + body: { + mode: 'formdata', + formdata: [ + { key: "firstName", value: "John" }, + { key: "lastName", value: "Doe" }, + { key: "email", value: "john.doe@example.com" } + ] + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "multipart/form-data", + }, + data: { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform formdata body when no Content-Type header exists', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + body: { + mode: 'formdata', + formdata: [ + { key: "firstName", value: "John" }, + { key: "lastName", value: "Doe" }, + { key: "email", value: "john.doe@example.com" } + ] + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + + data: { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + }, + + headers: { + "Content-Type": "multipart/form-data" + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + + it('should transform formdata body with incorrect Content-Type header', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "text/plain" + }, + body: { + mode: 'formdata', + formdata: [ + { key: "firstName", value: "John" }, + { key: "lastName", value: "Doe" }, + { key: "email", value: "john.doe@example.com" } + ] + } + }, function (error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.json(); + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + "Content-Type": "multipart/form-data" + }, + data: { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + }, function(error, response) { + if (error) { + const errorCode = error.code; + console.log(errorCode); + } + if (response) { + const response_body = response.getBody(); + const response_headers = response.getHeaders(); + console.log(response_body, response_headers); + } + }); + `); + }); + }); + + describe('Headers and Content-Type Handling', () => { + it('should rename header property to headers', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'X-Custom-Header': 'custom-value', + 'Authorization': 'Bearer token' + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'X-Custom-Header': 'custom-value', + 'Authorization': 'Bearer token' + } + }); + `); + }); + + it('should handle header array format', () => { + const code = ` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + header: [ + { key: 'X-Custom-Header', value: 'custom-value' }, + { key: 'Authorization', value: 'Bearer token' } + ] + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + "X-Custom-Header": 'custom-value', + "Authorization": 'Bearer token' + } + }); + `); + }); + }); + + describe('Response Handling', () => { + it('should transform response property access', () => { + const code = ` + pm.sendRequest('https://echo.usebruno.com', function (error, response) { + const status = response.code; + const statusText = response.status; + const headers = response.headers; + const body = response.json(); + const responseTime = response.responseTime; + const text = response.text(); + + if (status === 200) { + console.log('Success!'); + } + }); + `; + const translatedCode = translateCode(code); + expect(translatedCode).toContain('const status = response.getStatus()'); + expect(translatedCode).toContain('const statusText = response.statusText'); + expect(translatedCode).toContain('const headers = response.getHeaders()'); + expect(translatedCode).toContain('const body = response.getBody()'); + expect(translatedCode).toContain('const responseTime = response.getResponseTime()'); + expect(translatedCode).toContain('const text = response.getBody()'); + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index d38d28983..5dad6935e 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,5 +1,6 @@ const { cloneDeep } = require('lodash'); const { interpolate: _interpolate } = require('@usebruno/common'); +const { sendRequest } = require('@usebruno/requests').scripting; const variableNameRegex = /^[\w-.]*$/; @@ -15,6 +16,7 @@ class Bru { this.oauth2CredentialVariables = oauth2CredentialVariables || {}; this.collectionPath = collectionPath; this.collectionName = collectionName; + this.sendRequest = sendRequest; this.runner = { skipRequest: () => { this.skipRequest = true; diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js index 2c83c0e3f..d1340e92d 100644 --- a/packages/bruno-js/src/sandbox/quickjs/index.js +++ b/packages/bruno-js/src/sandbox/quickjs/index.js @@ -142,10 +142,10 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext; + consoleFn && addConsoleShimToContext(vm, consoleFn); bru && addBruShimToContext(vm, bru); req && addBrunoRequestShimToContext(vm, req); res && addBrunoResponseShimToContext(vm, res); - consoleFn && addConsoleShimToContext(vm, consoleFn); addLocalModuleLoaderShimToContext(vm, collectionPath); addPathShimToContext(vm); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 8439d7206..5be5e26d0 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -1,4 +1,4 @@ -const { cleanJson } = require('../../../utils'); +const { cleanJson, cleanCircularJson } = require('../../../utils'); const { marshallToVm } = require('../utils'); const addBruShimToContext = (vm, bru) => { @@ -210,8 +210,7 @@ const addBruShimToContext = (vm, bru) => { 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)); + promise.resolve(marshallToVm(cleanCircularJson(response), vm)); }) .catch((err) => { promise.resolve( @@ -228,6 +227,26 @@ const addBruShimToContext = (vm, bru) => { }); runRequestHandle.consume((handle) => vm.setProp(bruObject, 'runRequest', handle)); + let sendRequestHandle = vm.newFunction('_sendRequest', (args) => { + const promise = vm.newPromise(); + bru + .sendRequest(vm.dump(args)) + .then((response) => { + promise.resolve(marshallToVm(cleanCircularJson(response), vm)); + }) + .catch((err) => { + promise.reject( + marshallToVm( + cleanJson(err), + vm + ) + ); + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle)); + const sleep = vm.newFunction('sleep', (timer) => { const t = vm.getString(timer); const promise = vm.newPromise(); @@ -242,6 +261,29 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'runner', bruRunnerObject); vm.setProp(vm.global, 'bru', bruObject); bruObject.dispose(); + + vm.evalCode(` + globalThis.bru.sendRequest = async (requestConfig, callback) => { + if (!callback) return await globalThis.bru._sendRequest(requestConfig); + try { + const response = await globalThis.bru._sendRequest(requestConfig); + try { + await callback(null, response); + } + catch(error) { + return Promise.reject(error); + } + } + catch(error) { + try { + await callback(JSON.parse(JSON.stringify(error)), null); + } + catch(err) { + return Promise.reject(err); + } + } + } + `); }; module.exports = addBruShimToContext; diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 289bf8dcc..7ebfa795a 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -144,11 +144,37 @@ const cleanJson = (data) => { } }; +const cleanCircularJson = (data) => { + try { + // Handle circular references by keeping track of seen objects + const seen = new WeakSet(); + + const replacer = (key, value) => { + // Skip non-objects and null + if (typeof value !== 'object' || value === null) { + return value; + } + + // Detect circular reference + if (seen.has(value)) { + return '[Circular Reference]'; + } + + seen.add(value); + return value; + }; + + return JSON.parse(JSON.stringify(data, replacer)); + } catch (e) { + return data; + } +}; module.exports = { evaluateJsExpression, evaluateJsTemplateLiteral, createResponseParser, internalExpressionCache, - cleanJson + cleanJson, + cleanCircularJson }; diff --git a/packages/bruno-requests/package.json b/packages/bruno-requests/package.json index 1aed11446..f8b55f4cd 100644 --- a/packages/bruno-requests/package.json +++ b/packages/bruno-requests/package.json @@ -31,6 +31,7 @@ "rollup": "3.29.5" }, "dependencies": { - "@types/qs": "^6.9.18" + "@types/qs": "^6.9.18", + "axios": "^1.9.0" } } diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 01850f3e4..3bdef99a0 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1,3 +1,7 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; export * as utils from './utils'; + +export * as network from './network'; + +export * as scripting from './scripting'; \ No newline at end of file diff --git a/packages/bruno-requests/src/network/axios-instance.ts b/packages/bruno-requests/src/network/axios-instance.ts new file mode 100644 index 000000000..bede865d7 --- /dev/null +++ b/packages/bruno-requests/src/network/axios-instance.ts @@ -0,0 +1,48 @@ +import { default as axios, AxiosRequestConfig, AxiosRequestHeaders } from 'axios'; + +/** + * + * @param {Object} customRequestConfig options - partial AxiosRequestConfig + * + * @returns {import('axios').AxiosInstance} Configured Axios instance + * + * @example + * const instance = makeAxiosInstance({ + * maxRedirects: 0, + * proxy: false, + * headers: { + * "User-Agent": `bruno-runtime/_version_` + * }, + * }); + */ + +const baseRequestConfig: Partial = { + transformRequest: function transformRequest(data: any, headers: AxiosRequestHeaders) { + const contentType = headers.getContentType() || ''; + const hasJSONContentType = contentType.includes('json'); + if (typeof data === 'string' && hasJSONContentType) { + return data; + } + + if (Array.isArray(axios.defaults.transformRequest)) { + axios.defaults.transformRequest.forEach((tr) => { + data = tr.call(this, data, headers); + }); + } + + return data; + } +} + +const makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => { + customRequestConfig = customRequestConfig || {}; + const axiosInstance = axios.create({ + ...baseRequestConfig, + ...customRequestConfig + }); + return axiosInstance; +}; + +export { + makeAxiosInstance +}; diff --git a/packages/bruno-requests/src/network/index.ts b/packages/bruno-requests/src/network/index.ts new file mode 100644 index 000000000..7d72cb7d1 --- /dev/null +++ b/packages/bruno-requests/src/network/index.ts @@ -0,0 +1 @@ +export { makeAxiosInstance } from './axios-instance'; \ No newline at end of file diff --git a/packages/bruno-requests/src/scripting/index.ts b/packages/bruno-requests/src/scripting/index.ts new file mode 100644 index 000000000..5ec4ee97b --- /dev/null +++ b/packages/bruno-requests/src/scripting/index.ts @@ -0,0 +1 @@ +export { default as sendRequest } from './send-request'; \ No newline at end of file diff --git a/packages/bruno-requests/src/scripting/send-request.ts b/packages/bruno-requests/src/scripting/send-request.ts new file mode 100644 index 000000000..8afedeaf3 --- /dev/null +++ b/packages/bruno-requests/src/scripting/send-request.ts @@ -0,0 +1,30 @@ +import { AxiosRequestConfig } from 'axios'; +import { makeAxiosInstance } from '../network'; + +type T_SendRequestCallback = (error: any, response: any) => void; + +const sendRequest = async (requestConfig: AxiosRequestConfig, callback: T_SendRequestCallback) => { + const axiosInstance = makeAxiosInstance(); + if (!callback) { + return await axiosInstance(requestConfig); + } + try { + const response = await axiosInstance(requestConfig); + try { + await callback(null, response); + } + catch(error) { + return Promise.reject(error); + } + } + catch (error) { + try { + await callback(error, null); + } + catch(err) { + return Promise.reject(err); + } + } +}; + +export default sendRequest; diff --git a/packages/bruno-tests/collection/scripting/api/bru/send-request/folder.bru b/packages/bruno-tests/collection/scripting/api/bru/send-request/folder.bru new file mode 100644 index 000000000..02561d36e --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/send-request/folder.bru @@ -0,0 +1,8 @@ +meta { + name: send-request + seq: 16 +} + +auth { + mode: inherit +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/send-request/get-url-string.bru b/packages/bruno-tests/collection/scripting/api/bru/send-request/get-url-string.bru new file mode 100644 index 000000000..161f6d763 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/send-request/get-url-string.bru @@ -0,0 +1,18 @@ +meta { + name: get-url-string + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: none + auth: inherit +} + +tests { + await test("send request with a get url string", async () => { + const res = await bru.sendRequest("https://testbench-sanity.usebruno.com/ping"); + expect(res.data).to.eql('pong'); + }); +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/send-request/usage-patterns.bru b/packages/bruno-tests/collection/scripting/api/bru/send-request/usage-patterns.bru new file mode 100644 index 000000000..bb80b3346 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/send-request/usage-patterns.bru @@ -0,0 +1,80 @@ +meta { + name: usage-patterns + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: none + auth: inherit +} + +tests { + // pattern 1: using async/await + await test("post request with async/await - success case", async () => { + const res = await bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + data: 'ping' + }); + expect(res.data).to.eql('ping'); + }); + + await test("post request with async/await - error case", async () => { + try { + await bru.sendRequest({ + url: 'https://echo.usebruno.com/invalid', + method: 'POST', + data: 'ping' + }); + } + catch(err) { + expect(err.status).to.eql(404); + } + }); + + // pattern 2: using promise (.then/.catch) + await test("post request with promise chain - success case", async () => { + await bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + data: 'ping' + }) + .then(res => { + expect(res.data).to.eql('ping'); + }); + }); + + await test("post request with promise chain - error case", async () => { + await bru.sendRequest({ + url: 'https://echo.usebruno.com/invalid', + method: 'POST', + data: 'ping' + }) + .catch(err => { + expect(err.status).to.eql(404); + }); + }); + + // pattern 3: using callbacks + await test("post request with callback - success case", async () => { + await bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + data: 'ping' + }, function(error, response) { + expect(response.data).to.eql('ping'); + }); + }); + + await test("post request with callback - error case", async () => { + await bru.sendRequest({ + url: 'https://echo.usebruno.com/invalid', + method: 'POST', + data: 'ping' + }, function(error, response) { + expect(error.status).to.eql(404); + }); + }); +} From f2aedf780d8d8421924ca8a72ce764e2367dbc06 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Sat, 14 Jun 2025 22:20:24 +0530 Subject: [PATCH 103/214] Fix: showing test script errors (#4902) * fix: catch errors in tests --- .../ResponsePane/ScriptError/index.js | 44 +++-- .../src/components/ResponsePane/index.js | 36 +++-- .../ReduxStore/slices/collections/index.js | 4 + .../bruno-electron/src/ipc/network/index.js | 153 ++++++++++++------ packages/bruno-js/src/runtime/test-runtime.js | 108 +++++++------ 5 files changed, 227 insertions(+), 118 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js index 4af07c587..1db032d2e 100644 --- a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js @@ -6,22 +6,48 @@ import StyledWrapper from './StyledWrapper'; const ScriptError = ({ item, onClose }) => { const preRequestError = item?.preRequestScriptErrorMessage; const postResponseError = item?.postResponseScriptErrorMessage; + const testScriptError = item?.testScriptErrorMessage; - if (!preRequestError && !postResponseError) return null; + if (!preRequestError && !postResponseError && !testScriptError) return null; - const errorMessage = preRequestError || postResponseError; - const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error'; + const errors = []; + + if (preRequestError) { + errors.push({ + title: 'Pre-Request Script Error', + message: preRequestError + }); + } + + if (postResponseError) { + errors.push({ + title: 'Post-Response Script Error', + message: postResponseError + }); + } + + if (testScriptError) { + errors.push({ + title: 'Test Script Error', + message: testScriptError + }); + } return (
-
- {errorTitle} -
-
- {errorMessage} -
+ {errors.map((error, index) => ( +
+ {index > 0 &&
} +
+ {error.title} +
+
+ {error.message} +
+
+ ))}
{ }); useEffect(() => { - if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) { + if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) { setShowScriptErrorCard(true); } - }, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]); + }, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]); const selectTab = (tab) => { dispatch( @@ -128,7 +128,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0; - const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage; + const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage; return ( @@ -174,7 +174,11 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { ) : null}
{isLoading ? : null} {hasScriptError && showScriptErrorCard && ( @@ -183,17 +187,19 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { onClose={() => setShowScriptErrorCard(false)} /> )} - {!item?.response ? ( - focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? ( - - ) : null - ) : ( - <>{getTabPanel(focusedTab.responsePaneTab)} - )} +
+ {!item?.response ? ( + focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? ( + + ) : null + ) : ( + <>{getTabPanel(focusedTab.responsePaneTab)} + )} +
); 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 64a72a43a..df1fc63bc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1987,6 +1987,10 @@ export const collectionsSlice = createSlice({ item.postResponseScriptErrorMessage = action.payload.errorMessage; } + if(type === 'test-script-execution') { + item.testScriptErrorMessage = action.payload.errorMessage; + } + if (type === 'request-queued') { const { cancelTokenUid } = action.payload; // ignore if request is already in progress or completed diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4b4937d52..f63926991 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -805,19 +805,39 @@ const registerNetworkIpc = (mainWindow) => { const collectionName = collection?.name if (typeof testFile === 'string') { const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - const testResults = await testRuntime.runTests( - decomment(testFile), - request, - response, - envVars, - runtimeVariables, - collectionPath, - onConsoleLog, - processEnvVars, - scriptingConfig, - runRequestByItemPathname, - collectionName - ); + let testResults = null; + let testError = null; + + try { + testResults = await testRuntime.runTests( + decomment(testFile), + request, + response, + envVars, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runRequestByItemPathname, + collectionName + ); + } catch (error) { + testError = error; + + if (error.partialResults) { + testResults = error.partialResults; + } else { + testResults = { + request, + envVariables: envVars, + runtimeVariables, + globalEnvironmentVariables: request?.globalEnvironmentVariables || {}, + results: [], + nextRequestName: null + }; + } + } !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'test-results', @@ -839,6 +859,20 @@ const registerNetworkIpc = (mainWindow) => { }); collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + + const testScriptExecutionEvent = { + type: 'test-script-execution', + requestUid, + collectionUid, + itemUid: item.uid, + errorMessage: null, + } + + if (testError) { + const errorMessage = testError?.message || 'An error occurred in test script'; + testScriptExecutionEvent.errorMessage = errorMessage; + } + !runInBackground && mainWindow.webContents.send('main:run-request-event', testScriptExecutionEvent); } return { @@ -1213,42 +1247,67 @@ const registerNetworkIpc = (mainWindow) => { const testFile = get(request, 'tests'); const collectionName = collection?.name if (typeof testFile === 'string') { - const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - const testResults = await testRuntime.runTests( - decomment(testFile), - request, - response, - envVars, - runtimeVariables, - collectionPath, - onConsoleLog, - processEnvVars, - scriptingConfig, - runRequestByItemPathname, - collectionName - ); + try { + const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); + const testResults = await testRuntime.runTests( + decomment(testFile), + request, + response, + envVars, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runRequestByItemPathname, + collectionName + ); - if (testResults?.nextRequestName !== undefined) { - nextRequestName = testResults.nextRequestName; + if (testResults?.nextRequestName !== undefined) { + nextRequestName = testResults.nextRequestName; + } + + mainWindow.webContents.send('main:run-folder-event', { + type: 'test-results', + testResults: testResults.results, + ...eventData + }); + + mainWindow.webContents.send('main:script-environment-update', { + envVariables: testResults.envVariables, + runtimeVariables: testResults.runtimeVariables, + collectionUid + }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: testResults.globalEnvironmentVariables + }); + + collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + } catch (testError) { + + if (testError.partialResults && testError.partialResults.results.length > 0) { + + // Send the partial test results + mainWindow.webContents.send('main:run-folder-event', { + type: 'test-results', + testResults: testError.partialResults.results, + ...eventData + }); + + mainWindow.webContents.send('main:script-environment-update', { + envVariables: testError.partialResults.envVariables, + runtimeVariables: testError.partialResults.runtimeVariables, + collectionUid + }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: testError.partialResults.globalEnvironmentVariables + }); + + collection.globalEnvironmentVariables = testError.partialResults.globalEnvironmentVariables; + } } - - mainWindow.webContents.send('main:run-folder-event', { - type: 'test-results', - testResults: testResults.results, - ...eventData - }); - - mainWindow.webContents.send('main:script-environment-update', { - envVariables: testResults.envVariables, - runtimeVariables: testResults.runtimeVariables, - collectionUid - }); - - mainWindow.webContents.send('main:global-environment-variables-update', { - globalEnvironmentVariables: testResults.globalEnvironmentVariables - }); - - collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; } } catch (error) { mainWindow.webContents.send('main:run-folder-event', { diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index bed0589ca..163ca9aad 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -124,56 +124,63 @@ class TestRuntime { context.bru.runRequest = runRequestByItemPathname; } - if (this.runtime === 'quickjs') { - await executeQuickJsVmAsync({ - script: testsFile, - context: context - }); - } else { - // default runtime is vm2 - const vm = new NodeVM({ - sandbox: context, - require: { - context: 'sandbox', - external: true, - root: [collectionPath, ...additionalContextRootsAbsolute], - mock: { - // node libs - path, - stream, - util, - url, - http, - https, - punycode, - zlib, - // 3rd party libs - ajv, - 'ajv-formats': addFormats, - btoa, - atob, - lodash, - moment, - uuid, - nanoid, - axios, - chai, - 'node-fetch': fetch, - 'crypto-js': CryptoJS, - 'xml2js': xml2js, - cheerio, - tv4, - ...whitelistedModules, - fs: allowScriptFilesystemAccess ? fs : undefined, - 'node-vault': NodeVault + let scriptError = null; + + try { + if (this.runtime === 'quickjs') { + await executeQuickJsVmAsync({ + script: testsFile, + context: context + }); + } else { + // default runtime is vm2 + const vm = new NodeVM({ + sandbox: context, + require: { + context: 'sandbox', + external: true, + root: [collectionPath, ...additionalContextRootsAbsolute], + mock: { + // node libs + path, + stream, + util, + url, + http, + https, + punycode, + zlib, + // 3rd party libs + ajv, + 'ajv-formats': addFormats, + btoa, + atob, + lodash, + moment, + uuid, + nanoid, + axios, + chai, + 'node-fetch': fetch, + 'crypto-js': CryptoJS, + 'xml2js': xml2js, + cheerio, + tv4, + ...whitelistedModules, + fs: allowScriptFilesystemAccess ? fs : undefined, + 'node-vault': NodeVault + } } - } - }); - const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js')); - await asyncVM(); + }); + const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js')); + await asyncVM(); + } + } catch (error) { + scriptError = error; + console.error('Test script execution error:', error); } - return { + const result = { request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), @@ -181,6 +188,13 @@ class TestRuntime { results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest }; + + if (scriptError) { + scriptError.partialResults = result; + throw scriptError; + } + + return result; } } From 4725300c410fd3fa4282c2ef2fc9a73dc9530bda Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Mon, 16 Jun 2025 13:23:00 +0545 Subject: [PATCH 104/214] feat(cli): add support for environment file input in run command --- packages/bruno-cli/src/commands/run.js | 29 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index af4cf4ae3..f6b4463a4 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -106,6 +106,10 @@ const builder = async (yargs) => { describe: 'Environment variables', type: 'string' }) + .option('env-file', { + describe: 'Path to environment file (.bru) - can be absolute or relative path', + type: 'string' + }) .option('env-var', { describe: 'Overwrite a single environment variable, multiple usages possible', type: 'string' @@ -175,6 +179,7 @@ const builder = async (yargs) => { }) .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 request.bru --env-file env.bru', 'Run a request with the environment from env.bru file') .example('$0 run folder', 'Run all requests in a folder') .example('$0 run folder -r', 'Run all requests in a folder recursively') .example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output') @@ -224,6 +229,7 @@ const handler = async function (argv) { ignoreTruststore, disableCookies, env, + envFile, envVar, insecure, r: recursive, @@ -294,19 +300,28 @@ const handler = async function (argv) { const runtimeVariables = {}; let envVars = {}; - if (env) { - const envFile = path.join(collectionPath, 'environments', `${env}.bru`); - const envPathExists = await exists(envFile); + if (env && envFile) { + console.error(chalk.red(`Cannot use both --env and --env-file options together`)); + process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE); + } + + if (envFile || env) { + const envFilePath = envFile + ? path.resolve(collectionPath, envFile) + : path.join(collectionPath, 'environments', `${env}.bru`); + + const envFileExists = await exists(envFilePath); + if (!envFileExists) { + const errorPath = envFile || `environments/${env}.bru`; + console.error(chalk.red(`Environment file not found: `) + chalk.dim(errorPath)); - if (!envPathExists) { - console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`)); process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND); } - const envBruContent = fs.readFileSync(envFile, 'utf8'); + const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n'); const envJson = bruToEnvJson(envBruContent); envVars = getEnvVars(envJson); - envVars.__name__ = env; + envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env; } if (envVar) { From 9cbfeccbed76ab972d5876f51338f23c97efe835 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Mon, 16 Jun 2025 21:53:38 +0530 Subject: [PATCH 105/214] fix: timeline-scroll --- packages/bruno-app/src/components/ResponsePane/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index ab648a146..205a89c56 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -187,7 +187,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { onClose={() => setShowScriptErrorCard(false)} /> )} -
+
{!item?.response ? ( focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? ( Date: Mon, 16 Jun 2025 22:21:33 +0530 Subject: [PATCH 106/214] Merge pull request #4901 from Pragadesh-45/feat/support-multiple-run-cli-v1 Co-authored-by: William Quintal Feat: Enhance run command to accept multiple inputs for requests and folders in Bruno CLI (Improves: #2956) (Fixes: #2955) --- packages/bruno-cli/src/commands/run.js | 71 +-- packages/bruno-cli/src/utils/collection.js | 36 +- .../utils/collection/get-call-stack.spec.js | 460 ++++++++++++++++++ 3 files changed, 520 insertions(+), 47 deletions(-) create mode 100644 packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index aa4b4d199..3d4bfb6c7 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -12,9 +12,9 @@ const { rpad } = require('../utils/common'); const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); const { dotenvToJson } = require('@usebruno/lang'); const constants = require('../constants'); -const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname } = require('../utils/collection'); -const command = 'run [filename]'; -const desc = 'Run a request'; +const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection'); +const command = 'run [paths...]'; +const desc = 'Run one or more requests/folders'; const formatTestSummary = (label, maxLength, passed, failed, total, errorCount = 0, skippedCount = 0) => { const parts = [ @@ -204,6 +204,7 @@ const builder = async (yargs) => { .example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file') .example('$0 run folder', 'Run all requests in a folder') .example('$0 run folder -r', 'Run all requests in a folder recursively') + .example('$0 run request.bru folder', 'Run a request and all requests in a folder') .example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output') .example( '$0 run --reporter-skip-headers "Authorization"', @@ -246,7 +247,7 @@ const builder = async (yargs) => { const handler = async function (argv) { try { let { - filename, + paths, cacert, ignoreTruststore, disableCookies, @@ -308,17 +309,6 @@ const handler = async function (argv) { } } - if (filename && filename.length) { - const pathExists = await exists(filename); - if (!pathExists) { - console.error(chalk.red(`File or directory ${filename} does not exist`)); - process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND); - } - } else { - filename = './'; - recursive = true; - } - const runtimeVariables = {}; let envVars = {}; @@ -438,45 +428,34 @@ const handler = async function (argv) { }); } - const _isFile = isFile(filename); + let requestItems = []; let results = []; - let requestItems = []; - - if (_isFile) { - 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); + if (!paths || !paths.length) { + paths = ['./']; + recursive = true; } - const _isDirectory = isDirectory(filename); - if (_isDirectory) { - if (!recursive) { - console.log(chalk.yellow('Running Folder \n')); - } else { - console.log(chalk.yellow('Running Folder Recursively \n')); - } - const resolvedFilepath = path.resolve(filename); - if (resolvedFilepath === collectionPath) { - requestItems = getAllRequestsInFolder(collection?.items, recursive); - } else { - const folderItem = findItemInCollection(collection, resolvedFilepath); - if (folderItem) { - requestItems = getAllRequestsInFolder(folderItem.items, recursive); - } - } + const resolvedPaths = paths.map(p => path.resolve(process.cwd(), p)); - if (testsOnly) { - requestItems = requestItems.filter((iter) => { - const requestHasTests = iter.request?.tests; - const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false; - return requestHasTests || requestHasActiveAsserts; - }); + for (const resolvedPath of resolvedPaths) { + const pathExists = await exists(resolvedPath); + if (!pathExists) { + console.error(chalk.red(`Path not found: ${resolvedPath}`)); + process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND); } } + requestItems = getCallStack(resolvedPaths, collection, { recursive }); + + if (testsOnly) { + requestItems = requestItems.filter((iter) => { + const requestHasTests = iter.request?.tests; + const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false; + return requestHasTests || requestHasActiveAsserts; + }); + } + const runtime = getJsSandboxRuntime(sandbox); const runSingleRequestByPathname = async (relativeItemPathname) => { diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 649fb2a33..4b698ced9 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -349,6 +349,39 @@ const getAllRequestsAtFolderRoot = (folderItems = []) => { return getAllRequestsInFolder(folderItems, false); } +const getCallStack = (resolvedPaths = [], collection, {recursive}) => { + let requestItems = []; + + + if (!resolvedPaths || !resolvedPaths.length) { + return requestItems; + } + + for (const resolvedPath of resolvedPaths) { + if (!resolvedPath || !resolvedPath.length) { + continue; + } + + if (resolvedPath === collection.pathname) { + requestItems = requestItems.concat(getAllRequestsInFolder(collection.items, recursive)); + continue; + } + + const item = findItemInCollection(collection, resolvedPath); + if (!item) { + continue; + } + + if (item.type === 'folder') { + requestItems = requestItems.concat(getAllRequestsInFolder(item.items, recursive)); + } else { + requestItems.push(item); + } + } + + return requestItems; +}; + /** * Safe write file implementation to handle errors * @param {string} filePath - Path to write file @@ -489,5 +522,6 @@ module.exports = { createCollectionFromBrunoObject, mergeAuth, getAllRequestsInFolder, - getAllRequestsAtFolderRoot + getAllRequestsAtFolderRoot, + getCallStack } \ No newline at end of file diff --git a/packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js b/packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js new file mode 100644 index 000000000..8260b1485 --- /dev/null +++ b/packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js @@ -0,0 +1,460 @@ +const { describe, it, expect, beforeEach } = require('@jest/globals'); +const { getCallStack } = require('../../../src/utils/collection'); + +const collection = { + brunoConfig: { + version: '1', + name: 'multirun-cli', + type: 'collection', + ignore: ['node_modules', '.git'] + }, + root: { + request: { + headers: [], + auth: {}, + script: {}, + vars: {}, + tests: '' + } + }, + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20', + items: [ + { + name: 'root-folder', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder', + type: 'folder', + items: [ + { + name: 'root-child-folder', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder', + type: 'folder', + items: [ + { + name: 'root-child-child-folder', + pathname: + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder', + type: 'folder', + items: [ + { + name: 'root-child-child-child-req-0', + pathname: + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru', + type: 'http-request', + seq: 1, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-child-child-file-0")' + }, + tests: '' + } + }, + { + name: 'root-child-child-child-req-1', + pathname: + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru', + type: 'http-request', + seq: 2, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-child-child-file-1")' + }, + tests: '' + } + } + ], + root: { + request: { + headers: [], + auth: {}, + script: {}, + vars: {}, + tests: '' + }, + meta: { + name: 'root-child-child-folder', + seq: 3 + } + }, + seq: 3 + }, + { + name: 'root-child-child-req-0', + pathname: + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru', + type: 'http-request', + seq: 4, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-child-file-0")' + }, + tests: '' + } + }, + { + name: 'root-child-child-req-1', + pathname: + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru', + type: 'http-request', + seq: 5, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-child-file-1")' + }, + tests: '' + } + } + ], + root: { + request: { + headers: [], + auth: {}, + script: {}, + vars: {}, + tests: '' + }, + meta: { + name: 'root-child-folder', + seq: 6 + } + }, + seq: 6 + }, + { + name: 'root-child-req-0', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru', + type: 'http-request', + seq: 7, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-file-0")' + }, + tests: '' + } + }, + { + name: 'root-child-req-1', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru', + type: 'http-request', + seq: 8, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-child-file-1")' + }, + tests: '' + } + } + ], + root: { + request: { + headers: [], + auth: {}, + script: {}, + vars: {}, + tests: '' + }, + meta: { + name: 'root-folder', + seq: 9 + } + }, + seq: 9 + }, + { + name: 'root-req-0', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru', + type: 'http-request', + seq: 10, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-file-0")' + }, + tests: '' + } + }, + { + name: 'root-req-1', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru', + type: 'http-request', + seq: 11, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-file-1")' + }, + tests: '' + } + }, + { + name: 'root-req-2', + pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru', + type: 'http-request', + seq: 12, + request: { + method: 'GET', + url: 'https://g.cn', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: { + req: 'console.log("root-file-2")' + }, + tests: '' + } + } + ] +}; + +const sequenceChangedCollection = { + brunoConfig: { + version: '1', + name: 'sequenceChangedCollection', + type: 'collection', + ignore: ['node_modules', '.git'] + }, + root: {}, + pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection', + items: [ + { + name: 'three', + pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru', + type: 'http-request', + seq: 1, + request: { + method: 'GET', + url: 'https://usebruno.com', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: {}, + tests: '' + } + }, + { + name: 'one', + pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru', + type: 'http-request', + seq: 2, + request: { + method: 'GET', + url: 'https://usebruno.com', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: {}, + tests: '' + } + }, + { + name: 'two', + pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru', + type: 'http-request', + seq: 2, + request: { + method: 'GET', + url: 'https://usebruno.com', + auth: { + mode: 'inherit' + }, + params: [], + headers: [], + body: { + mode: 'none' + }, + vars: [], + assertions: [], + script: {}, + tests: '' + } + } + ] +}; + +describe('getCallStack', () => { + it('should return all requests in the collection', () => { + const callStack = getCallStack(['/Users/tempo/Downloads/t-temp/multirun-cli-20'], collection, { recursive: true }); + const expectedCallStack = [ + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru' + ]; + expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack); + }); + + it('should return all requests in the collection when sequence is changed', () => { + const callStack = getCallStack( + ['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'], + sequenceChangedCollection, + { + recursive: true + } + ); + const expectedCallStack = [ + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru', + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru', + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru' + ]; + expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack); + }); +}); + +describe('getCallStack with collection sequence changed', () => { + it('should return an empty array', () => { + const callStack = getCallStack( + ['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'], + sequenceChangedCollection, + { + recursive: true + } + ); + const expectedCallStack = [ + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru', + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru', + '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru' + ]; + expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack); + }); +}); + +describe('getCallStack with muliple folders and requests run', () => { + it('should return an empty array', () => { + const callStack = getCallStack( + [ + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru' + ], + collection, + { + recursive: true + } + ); + const expectedCallStack = [ + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru', + '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru' + ]; + expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack); + }); +}); From 745a71700c6f02c55a5d214644c1bf752796d584 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 16 Jun 2025 22:50:45 +0530 Subject: [PATCH 107/214] add await keyword to the translated bru.sendRequest function calls (#4906) * add await keyword for the bru.sendRequest postman translations --------- Co-authored-by: lohit --- .../src/utils/send-request-transformer.js | 76 +++++++--------- .../transformers/send-request.test.js | 90 +++++++++---------- .../src/network/axios-instance.ts | 30 ++++++- 3 files changed, 107 insertions(+), 89 deletions(-) diff --git a/packages/bruno-converters/src/utils/send-request-transformer.js b/packages/bruno-converters/src/utils/send-request-transformer.js index cb5ecd19b..3656c4a2c 100644 --- a/packages/bruno-converters/src/utils/send-request-transformer.js +++ b/packages/bruno-converters/src/utils/send-request-transformer.js @@ -175,13 +175,10 @@ const transformCallback = (j, callback) => { // Define translations for callback response properties const responsePropertyMap = { - 'json': 'getBody', - 'text': 'getBody', - 'code': 'getStatus()', + 'json': 'data', + 'text': 'data', + 'code': 'status', 'status': 'statusText', - 'responseTime': 'getResponseTime()', - 'statusText': 'statusText', - 'headers': 'getHeaders()', }; // Process the callback body to transform response property references @@ -196,42 +193,26 @@ const transformCallback = (j, callback) => { // Handle property access if (property.type === 'Identifier' && responsePropertyMap[property.name]) { const bruProperty = responsePropertyMap[property.name]; - - // If it's a method call (with parentheses) - if (bruProperty.endsWith('()')) { - // If it's already being called (e.g., response.json()) - if (memberPath.parent.node.type === 'CallExpression' && - memberPath.parent.node.callee === memberPath.node) { - // Replace with method call: res.getBody() - j(memberPath.parent).replaceWith( - j.callExpression( - j.memberExpression( - j.identifier(responseVarName), - j.identifier(bruProperty.slice(0, -2)) - ), - [] + if (bruProperty) { + // Check if memberPath is part of a CallExpression + const parentPath = memberPath.parent; + if (parentPath && parentPath.node.type === 'CallExpression') { + // Replace the entire CallExpression with a property access + j(parentPath).replaceWith( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(bruProperty) ) ); } else { - // Replace with method call: res.getBody() + // Regular property access replacement j(memberPath).replaceWith( - j.callExpression( - j.memberExpression( - j.identifier(responseVarName), - j.identifier(bruProperty.slice(0, -2)) - ), - [] + j.memberExpression( + j.identifier(responseVarName), + j.identifier(bruProperty) ) ); } - } else { - // Replace with property access: res.statusText - j(memberPath).replaceWith( - j.memberExpression( - j.identifier(responseVarName), - j.identifier(bruProperty) - ) - ); } } }); @@ -267,17 +248,26 @@ const sendRequestTransformer = (path, j) => { if (callback) { const transformedCallback = transformCallback(j, callback); - // Create expression: bru.sendRequest(requestConfig, callback); - return j.callExpression( - j.identifier('bru.sendRequest'), - transformedCallback ? [requestOptions, transformedCallback] : [requestOptions] + // Add async keyword to the callback function + if (transformedCallback && transformedCallback.type === 'FunctionExpression') { + transformedCallback.async = true; + } + + // Create expression: await bru.sendRequest(requestConfig, callback); + return j.awaitExpression( + j.callExpression( + j.identifier('bru.sendRequest'), + transformedCallback ? [requestOptions, transformedCallback] : [requestOptions] + ) ); } - // If there's no callback, just transform to bru.sendRequest - return j.callExpression( - j.identifier('bru.sendRequest'), - [requestOptions] + // If there's no callback, just transform to await bru.sendRequest + return j.awaitExpression( + j.callExpression( + j.identifier('bru.sendRequest'), + [requestOptions] + ) ); }; diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js index 90f54e38b..f0c12bf3e 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js @@ -17,7 +17,7 @@ describe('Send Request Translation', () => { "x": 1 }) } - }, function (error, response) { + }, async function (error, response) { if (error) { const errorCode = error.code; console.log(errorCode); @@ -31,7 +31,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -41,14 +41,14 @@ describe('Send Request Translation', () => { data: JSON.stringify({ "x": 1 }) - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -84,7 +84,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -94,14 +94,14 @@ describe('Send Request Translation', () => { data: { "x": 1 } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -126,15 +126,15 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { 'Content-Type': 'text/plain', }, data: 'Hello World' - }, function(error, response) { - console.log(response.getBody()); + }, async function(error, response) { + console.log(response.data); }); `); }); @@ -170,7 +170,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -180,14 +180,14 @@ describe('Send Request Translation', () => { data: { "key": "value" } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -214,7 +214,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -225,8 +225,8 @@ describe('Send Request Translation', () => { "lastName": "Doe", "email": "john.doe@example.com" } - }, function(error, response) { - console.log(response.getBody()); + }, async function(error, response) { + console.log(response.data); }); `); }); @@ -247,7 +247,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', @@ -282,7 +282,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -326,7 +326,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -335,14 +335,14 @@ describe('Send Request Translation', () => { data: { "key": "value" } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -379,7 +379,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -390,14 +390,14 @@ describe('Send Request Translation', () => { "lastName": "Doe", "email": "john.doe@example.com" } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -431,7 +431,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', @@ -444,14 +444,14 @@ describe('Send Request Translation', () => { headers: { "Content-Type": "multipart/form-data" } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -488,7 +488,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'POST', headers: { @@ -499,14 +499,14 @@ describe('Send Request Translation', () => { "lastName": "Doe", "email": "john.doe@example.com" } - }, function(error, response) { + }, async function(error, response) { if (error) { const errorCode = error.code; console.log(errorCode); } if (response) { - const response_body = response.getBody(); - const response_headers = response.getHeaders(); + const response_body = response.data; + const response_headers = response.headers; console.log(response_body, response_headers); } }); @@ -528,7 +528,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'GET', headers: { @@ -552,7 +552,7 @@ describe('Send Request Translation', () => { `; const translatedCode = translateCode(code); expect(translatedCode).toBe(` - bru.sendRequest({ + await bru.sendRequest({ url: 'https://echo.usebruno.com', method: 'GET', headers: { @@ -581,12 +581,12 @@ describe('Send Request Translation', () => { }); `; const translatedCode = translateCode(code); - expect(translatedCode).toContain('const status = response.getStatus()'); - expect(translatedCode).toContain('const statusText = response.statusText'); - expect(translatedCode).toContain('const headers = response.getHeaders()'); - expect(translatedCode).toContain('const body = response.getBody()'); - expect(translatedCode).toContain('const responseTime = response.getResponseTime()'); - expect(translatedCode).toContain('const text = response.getBody()'); + expect(translatedCode).toContain(`const status = response.status; + const statusText = response.statusText;`); + expect(translatedCode).toContain('const headers = response.headers'); + expect(translatedCode).toContain('const body = response.data'); + expect(translatedCode).toContain('const responseTime = response.responseTime'); + expect(translatedCode).toContain('const text = response.data'); }); }); }); \ No newline at end of file diff --git a/packages/bruno-requests/src/network/axios-instance.ts b/packages/bruno-requests/src/network/axios-instance.ts index bede865d7..529045fe4 100644 --- a/packages/bruno-requests/src/network/axios-instance.ts +++ b/packages/bruno-requests/src/network/axios-instance.ts @@ -1,4 +1,4 @@ -import { default as axios, AxiosRequestConfig, AxiosRequestHeaders } from 'axios'; +import { default as axios, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; /** * @@ -16,6 +16,14 @@ import { default as axios, AxiosRequestConfig, AxiosRequestHeaders } from 'axios * }); */ +type ModifiedInternalAxiosRequestConfig = InternalAxiosRequestConfig & { + startTime: number; +} + +type ModifiedAxiosResponse = AxiosResponse & { + responseTime: number; +} + const baseRequestConfig: Partial = { transformRequest: function transformRequest(data: any, headers: AxiosRequestHeaders) { const contentType = headers.getContentType() || ''; @@ -40,6 +48,26 @@ const makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => { ...baseRequestConfig, ...customRequestConfig }); + + axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { + const modifiedConfig: ModifiedInternalAxiosRequestConfig = { + ...config, + startTime: Date.now() + } + return modifiedConfig; + }); + + axiosInstance.interceptors.response.use((response: AxiosResponse) => { + const config = response.config as ModifiedInternalAxiosRequestConfig; + const startTime = config.startTime; + const endTime = Date.now(); + const modifiedResponse: ModifiedAxiosResponse = { + ...response, + responseTime: endTime - startTime + }; + return modifiedResponse; + }); + return axiosInstance; }; From a05f7cb6868ccb4f022924fe88d15e683ee1ba11 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 17 Jun 2025 00:26:39 +0530 Subject: [PATCH 108/214] Merge pull request #4918 from lohxt1/bru_send_request_fixes bru.sendRequest translation fixes --- .../src/utils/send-request-transformer.js | 27 +++--- .../transformers/send-request.test.js | 96 +++++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/packages/bruno-converters/src/utils/send-request-transformer.js b/packages/bruno-converters/src/utils/send-request-transformer.js index 3656c4a2c..1029b1943 100644 --- a/packages/bruno-converters/src/utils/send-request-transformer.js +++ b/packages/bruno-converters/src/utils/send-request-transformer.js @@ -157,7 +157,7 @@ const transformBody = (j, requestOptions) => { * @returns {Object} - Transformed callback function */ const transformCallback = (j, callback) => { - if (!callback || callback.type !== 'FunctionExpression') return null; + if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) return null; const params = callback.params; const callbackBody = callback.body; @@ -236,6 +236,9 @@ const sendRequestTransformer = (path, j) => { const requestOptions = args[0]; const callback = args[1]; + // Check if original call was awaited + const wasAwaited = path.parent.parent.value.type === 'AwaitExpression'; + // transform the request config options if (requestOptions.type === 'ObjectExpression') { // Transform headers @@ -249,26 +252,26 @@ const sendRequestTransformer = (path, j) => { const transformedCallback = transformCallback(j, callback); // Add async keyword to the callback function - if (transformedCallback && transformedCallback.type === 'FunctionExpression') { + if (transformedCallback && (transformedCallback.type === 'FunctionExpression' || transformedCallback.type === 'ArrowFunctionExpression')) { transformedCallback.async = true; } // Create expression: await bru.sendRequest(requestConfig, callback); - return j.awaitExpression( - j.callExpression( - j.identifier('bru.sendRequest'), - transformedCallback ? [requestOptions, transformedCallback] : [requestOptions] - ) + const sendRequestCall = j.callExpression( + j.identifier('bru.sendRequest'), + transformedCallback ? [requestOptions, transformedCallback] : [requestOptions] ); + + return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall); } // If there's no callback, just transform to await bru.sendRequest - return j.awaitExpression( - j.callExpression( - j.identifier('bru.sendRequest'), - [requestOptions] - ) + const sendRequestCall = j.callExpression( + j.identifier('bru.sendRequest'), + [requestOptions] ); + + return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall); }; export default sendRequestTransformer; \ No newline at end of file diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js index f0c12bf3e..d6e9e62a7 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js @@ -589,4 +589,100 @@ describe('Send Request Translation', () => { expect(translatedCode).toContain('const text = response.data'); }); }); + + describe('Async/Await', () => { + it('Should not add await if already present', () => { + const code = ` + try { + const response = await pm.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }); + + console.log(response.json()); + } catch (err) { + console.error(err); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + try { + const response = await bru.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }); + + console.log(response.json()); + } catch (err) { + console.error(err); + } + `); + }); + + it('Should handle arrow function callbacks', () => { + const code = ` + try { + pm.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }, (error, response) => { + console.log(response.json()); + }); + } catch (err) { + console.error(err); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + try { + await bru.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }, async function(error, response) { + console.log(response.data); + }); + } catch (err) { + console.error(err); + } + `); + }); + + it('Should handle async arrow function callbacks', () => { + const code = ` + try { + pm.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }, async (error, response) => { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000) + }); + console.log(response.json()); + }); + } catch (err) { + console.error(err); + } + `; + const translatedCode = translateCode(code); + expect(translatedCode).toBe(` + try { + await bru.sendRequest({ + url: "https://echo.usebruno.com", + method: "GET" + }, async function(error, response) { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000) + }); + console.log(response.data); + }); + } catch (err) { + console.error(err); + } + `); + }); + }); }); \ No newline at end of file From 0eda1b761d94caf28580524376b11e1cbed10281 Mon Sep 17 00:00:00 2001 From: Maintainer Bruno Date: Tue, 17 Jun 2025 13:40:06 +0530 Subject: [PATCH 109/214] fix(workflow): ensure E2E test collection dependencies are installed in GitHub Actions --- .github/workflows/tests.yml | 4 ++++ e2e-tests/bruno-testbench/run-testbench-requests.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44ff08cce..aaf5d1880 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,6 +113,10 @@ jobs: 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: Install dependencies for test collection environment + run: | + npm ci --prefix packages/bruno-tests/collection + - name: Build libraries run: | npm run build:graphql-docs diff --git a/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts b/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts index f6bca6510..04398232f 100644 --- a/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts +++ b/e2e-tests/bruno-testbench/run-testbench-requests.spec.ts @@ -21,7 +21,7 @@ test.describe.parallel('Run Testbench Requests', () => { .slice(1); await expect(parseInt(failed)).toBe(0); - await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); }); test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => { @@ -44,6 +44,6 @@ test.describe.parallel('Run Testbench Requests', () => { .slice(1); await expect(parseInt(failed)).toBe(0); - await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); }); }); \ No newline at end of file From acd42eaa1b5b822a93d6d8d27904fef98cd4021e Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 18 Jun 2025 17:54:15 +0530 Subject: [PATCH 110/214] add: pre and post in report template (#4931) --- .../src/runner/reports/html/template.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/bruno-common/src/runner/reports/html/template.ts b/packages/bruno-common/src/runner/reports/html/template.ts index d19e2077f..cd0839e67 100644 --- a/packages/bruno-common/src/runner/reports/html/template.ts +++ b/packages/bruno-common/src/runner/reports/html/template.ts @@ -369,6 +369,30 @@ export const htmlTemplateString = (resutsJsonString: string) =>`