diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a829098e..4bf2d753c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,6 +83,11 @@ jobs: npm run build --workspace=packages/bruno-requests npm run build --workspace=packages/bruno-filestore + - name: Run Local Testbench + run: | + npm start --workspace=packages/bruno-tests & + sleep 5 + - name: Run tests run: | cd packages/bruno-tests/collection diff --git a/assets/images/vscode-demo.png b/assets/images/vscode-demo.png new file mode 100644 index 000000000..cdf2796a3 Binary files /dev/null and b/assets/images/vscode-demo.png differ diff --git a/docs/readme/readme_ar.md b/docs/readme/readme_ar.md index 9e7829fcc..5578ab1cd 100644 --- a/docs/readme/readme_ar.md +++ b/docs/readme/readme_ar.md @@ -74,10 +74,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 ffcc1a916..cf9c2110f 100644 --- a/docs/readme/readme_bn.md +++ b/docs/readme/readme_bn.md @@ -59,10 +59,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 560e34cd1..6ddb0b4fd 100644 --- a/docs/readme/readme_cn.md +++ b/docs/readme/readme_cn.md @@ -63,10 +63,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_de.md b/docs/readme/readme_de.md index d7c1a89be..dbba7be19 100644 --- a/docs/readme/readme_de.md +++ b/docs/readme/readme_de.md @@ -78,10 +78,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_es.md b/docs/readme/readme_es.md index 58e293bad..298a264fc 100644 --- a/docs/readme/readme_es.md +++ b/docs/readme/readme_es.md @@ -75,10 +75,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_fr.md b/docs/readme/readme_fr.md index c3553bebd..777ad42fc 100644 --- a/docs/readme/readme_fr.md +++ b/docs/readme/readme_fr.md @@ -63,10 +63,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_hi.md b/docs/readme/readme_hi.md index 395f38dec..1983059f5 100644 --- a/docs/readme/readme_hi.md +++ b/docs/readme/readme_hi.md @@ -75,12 +75,14 @@ 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 +sudo apt update && sudo apt install gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 कई प्लेटफार्मों पर चलाएं 🖥️

@@ -148,4 +150,3 @@ Scriptmania लाइसेंस 📄 MIT - diff --git a/docs/readme/readme_it.md b/docs/readme/readme_it.md index 82a84564f..c74e01cdc 100644 --- a/docs/readme/readme_it.md +++ b/docs/readme/readme_it.md @@ -59,10 +59,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_ja.md b/docs/readme/readme_ja.md index ea494e9fd..0bd366889 100644 --- a/docs/readme/readme_ja.md +++ b/docs/readme/readme_ja.md @@ -78,10 +78,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_ka.md b/docs/readme/readme_ka.md index af9d7de02..2b25e9193 100644 --- a/docs/readme/readme_ka.md +++ b/docs/readme/readme_ka.md @@ -77,12 +77,14 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 c901d6720..219617a0a 100644 --- a/docs/readme/readme_kr.md +++ b/docs/readme/readme_kr.md @@ -59,10 +59,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_nl.md b/docs/readme/readme_nl.md index 809de08ee..2e50bad5d 100644 --- a/docs/readme/readme_nl.md +++ b/docs/readme/readme_nl.md @@ -61,12 +61,14 @@ flatpak install com.usebruno.Bruno # Op Linux via Apt sudo mkdir -p /etc/apt/keyrings -sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266 - -echo "deb [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 ``` ### Draai op meerdere platformen 🖥️ diff --git a/docs/readme/readme_pl.md b/docs/readme/readme_pl.md index bef47b724..8c455a460 100644 --- a/docs/readme/readme_pl.md +++ b/docs/readme/readme_pl.md @@ -69,10 +69,13 @@ 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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_pt_br.md b/docs/readme/readme_pt_br.md index ba83906b3..bba4758c4 100644 --- a/docs/readme/readme_pt_br.md +++ b/docs/readme/readme_pt_br.md @@ -76,10 +76,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_ro.md b/docs/readme/readme_ro.md index fe82a5ce9..1c2ecf244 100644 --- a/docs/readme/readme_ro.md +++ b/docs/readme/readme_ro.md @@ -59,10 +59,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_tr.md b/docs/readme/readme_tr.md index cfd4df1bc..ed7153bde 100644 --- a/docs/readme/readme_tr.md +++ b/docs/readme/readme_tr.md @@ -63,10 +63,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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_zhtw.md b/docs/readme/readme_zhtw.md index 92e08edd5..183b7d25d 100644 --- a/docs/readme/readme_zhtw.md +++ b/docs/readme/readme_zhtw.md @@ -63,10 +63,13 @@ 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 [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 gpg curl +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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/e2e-tests/import-tests/bruno/001-import-bruno-testbench.spec.ts b/e2e-tests/import-tests/bruno/001-import-bruno-testbench.spec.ts new file mode 100644 index 000000000..cb436c1dd --- /dev/null +++ b/e2e-tests/import-tests/bruno/001-import-bruno-testbench.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + + +test.describe('Import Bruno Testbench Collection', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test.beforeAll(async ({ page }) => { + // Navigate back to homescreen after all tests + await page.locator('.bruno-logo').click(); + }); + + test('Import Bruno Testbench collection successfully', async ({ page }) => { + const brunoFile = path.join(testDataDir, 'bruno-testbench.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', brunoFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('bruno-testbench')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/bruno/002-import-bruno-corrupted-fails.spec.ts b/e2e-tests/import-tests/bruno/002-import-bruno-corrupted-fails.spec.ts new file mode 100644 index 000000000..9d6d10bf4 --- /dev/null +++ b/e2e-tests/import-tests/bruno/002-import-bruno-corrupted-fails.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Corrupted Bruno Collection - Should Fail', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Bruno collection with invalid JSON structure should fail', async ({ page }) => { + const brunoFile = path.join(testDataDir, 'bruno-malformed.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', brunoFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for JSON parsing error + const hasImportError = await page.getByText('Failed to parse the file – ensure it is valid JSON or YAML').first().isVisible(); + + // Either parsing error or import error should be shown + expect(hasImportError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/bruno/003-import-bruno-missing-required-schema.spec.ts b/e2e-tests/import-tests/bruno/003-import-bruno-missing-required-schema.spec.ts new file mode 100644 index 000000000..0f76559f6 --- /dev/null +++ b/e2e-tests/import-tests/bruno/003-import-bruno-missing-required-schema.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Bruno Collection - Missing Required Schema Fields', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Bruno collection missing required version field should fail', async ({ page }) => { + const brunoFile = path.join(testDataDir, 'bruno-missing-required-fields.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', brunoFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for schema validation error messages + const hasImportError = await page.getByText('Import collection failed').first().isVisible(); + + expect(hasImportError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/file-types/001-file-input-acceptance.spec.ts b/e2e-tests/import-tests/file-types/001-file-input-acceptance.spec.ts new file mode 100644 index 000000000..903f00d68 --- /dev/null +++ b/e2e-tests/import-tests/file-types/001-file-input-acceptance.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '../../../playwright'; + +test.describe('File Input Acceptance', () => { + test('File input accepts expected file types', async ({ page }) => { + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Check that file input exists (even if hidden) + const fileInput = page.locator('input[type="file"]'); + await expect(fileInput).toBeAttached(); + + // Verify it accepts the expected file types + const acceptValue = await fileInput.getAttribute('accept'); + expect(acceptValue).toContain('.json'); + expect(acceptValue).toContain('.yaml'); + expect(acceptValue).toContain('.yml'); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/file-types/002-invalid-file-handling.spec.ts b/e2e-tests/import-tests/file-types/002-invalid-file-handling.spec.ts new file mode 100644 index 000000000..8a655d65e --- /dev/null +++ b/e2e-tests/import-tests/file-types/002-invalid-file-handling.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid File Handling', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle invalid file without crashing', async ({ page }) => { + const invalidFile = path.join(testDataDir, 'invalid.txt'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', invalidFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + const hasError = await page.getByText("Failed to parse the file – ensure it is valid JSON or YAML").isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/insomnia/001-import-insomnia-v4.spec.ts b/e2e-tests/import-tests/insomnia/001-import-insomnia-v4.spec.ts new file mode 100644 index 000000000..1afeed306 --- /dev/null +++ b/e2e-tests/import-tests/insomnia/001-import-insomnia-v4.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Insomnia Collection v4', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Insomnia Collection v4 successfully', async ({ page }) => { + const insomniaFile = path.join(testDataDir, 'insomnia-v4.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Test API Collection v4')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/insomnia/002-import-insomnia-v5.spec.ts b/e2e-tests/import-tests/insomnia/002-import-insomnia-v5.spec.ts new file mode 100644 index 000000000..b246a2454 --- /dev/null +++ b/e2e-tests/import-tests/insomnia/002-import-insomnia-v5.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Insomnia Collection v5', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Insomnia Collection v5 successfully', async ({ page }) => { + const insomniaFile = path.join(testDataDir, 'insomnia-v5.yaml'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Test API Collection v5')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/insomnia/003-invalid-missing-collection.spec.ts b/e2e-tests/import-tests/insomnia/003-invalid-missing-collection.spec.ts new file mode 100644 index 000000000..0237a8240 --- /dev/null +++ b/e2e-tests/import-tests/insomnia/003-invalid-missing-collection.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Insomnia Collection - Missing Collection Array', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle Insomnia v5 collection missing collection array', async ({ page }) => { + const insomniaFile = path.join(testDataDir, 'insomnia-v5-invalid-missing-collection.yaml'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message + const hasError = await page.getByText('Import collection failed').first().isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/insomnia/004-malformed-structure.spec.ts b/e2e-tests/import-tests/insomnia/004-malformed-structure.spec.ts new file mode 100644 index 000000000..644bd35fe --- /dev/null +++ b/e2e-tests/import-tests/insomnia/004-malformed-structure.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Insomnia Collection - Malformed Structure', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle malformed Insomnia collection structure', async ({ page }) => { + const insomniaFile = path.join(testDataDir, 'insomnia-malformed.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message - this should fail during JSON parsing + const hasError = await page.getByText('Failed to parse the file').isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/openapi/001-import-openapi-yaml.spec.ts b/e2e-tests/import-tests/openapi/001-import-openapi-yaml.spec.ts new file mode 100644 index 000000000..0d705f1b1 --- /dev/null +++ b/e2e-tests/import-tests/openapi/001-import-openapi-yaml.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import OpenAPI v3 YAML Collection', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page }) => { + const openApiFile = path.join(testDataDir, 'openapi-comprehensive.yaml'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', openApiFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Comprehensive API Test Collection')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/openapi/002-import-openapi-json.spec.ts b/e2e-tests/import-tests/openapi/002-import-openapi-json.spec.ts new file mode 100644 index 000000000..847d4c49b --- /dev/null +++ b/e2e-tests/import-tests/openapi/002-import-openapi-json.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import OpenAPI v3 JSON Collection', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import simple OpenAPI v3 JSON successfully', async ({ page }) => { + const openApiFile = path.join(testDataDir, 'openapi-simple.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', openApiFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Simple Test API')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/openapi/003-missing-info.spec.ts b/e2e-tests/import-tests/openapi/003-missing-info.spec.ts new file mode 100644 index 000000000..aa8298dcd --- /dev/null +++ b/e2e-tests/import-tests/openapi/003-missing-info.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid OpenAPI - Missing Info Section', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle OpenAPI specification missing required info section', async ({ page }) => { + const openApiFile = path.join(testDataDir, 'openapi-missing-info.yaml'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', openApiFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // The OpenAPI parser might handle missing info gracefully with defaults + const hasError = await page.getByText('Import collection failed').first().isVisible(); + + // Either should show an error or create an "Untitled Collection" + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/openapi/004-malformed-yaml.spec.ts b/e2e-tests/import-tests/openapi/004-malformed-yaml.spec.ts new file mode 100644 index 000000000..2f2479e9a --- /dev/null +++ b/e2e-tests/import-tests/openapi/004-malformed-yaml.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid OpenAPI - Malformed YAML', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle malformed OpenAPI YAML structure', async ({ page }) => { + const openApiFile = path.join(testDataDir, 'openapi-malformed.yaml'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', openApiFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message - this should fail during YAML parsing + const hasParseError = await page.getByText('Failed to parse the file').isVisible(); + const hasImportError = await page.getByText('Import collection failed').isVisible(); + + // Either parsing error or import error should be shown + expect(hasParseError || hasImportError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/001-import-postman-v21.spec.ts b/e2e-tests/import-tests/postman/001-import-postman-v21.spec.ts new file mode 100644 index 000000000..08a7029df --- /dev/null +++ b/e2e-tests/import-tests/postman/001-import-postman-v21.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Postman Collection v2.1', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Postman Collection v2.1 successfully', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-v21.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Postman v2.1 Collection')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/002-import-postman-v20.spec.ts b/e2e-tests/import-tests/postman/002-import-postman-v20.spec.ts new file mode 100644 index 000000000..b6866fa3e --- /dev/null +++ b/e2e-tests/import-tests/postman/002-import-postman-v20.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Import Postman Collection v2.0', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Import Postman Collection v2.0 successfully', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-v20.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Verify that the Import Collection modal is displayed (for location selection) + const locationModal = page.getByRole('dialog'); + await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + // Wait for collection to appear in the location modal + await expect(locationModal.getByText('Postman v2.0 Collection')).toBeVisible(); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/003-invalid-missing-info.spec.ts b/e2e-tests/import-tests/postman/003-invalid-missing-info.spec.ts new file mode 100644 index 000000000..c2ab608c8 --- /dev/null +++ b/e2e-tests/import-tests/postman/003-invalid-missing-info.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Postman Collection - Missing Info', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle Postman collection missing required info field', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-invalid-missing-info.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message + const hasError = await page.getByText('Import collection failed').isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/004-invalid-schema.spec.ts b/e2e-tests/import-tests/postman/004-invalid-schema.spec.ts new file mode 100644 index 000000000..34ddfa07b --- /dev/null +++ b/e2e-tests/import-tests/postman/004-invalid-schema.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Postman Collection - Invalid Schema', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle Postman collection with invalid schema version', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-invalid-schema.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message + const hasError = await page.getByText('Conversion failed').isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/005-malformed-structure.spec.ts b/e2e-tests/import-tests/postman/005-malformed-structure.spec.ts new file mode 100644 index 000000000..2c39faf0d --- /dev/null +++ b/e2e-tests/import-tests/postman/005-malformed-structure.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Postman Collection - Malformed Structure', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle malformed Postman collection structure', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-malformed.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message + const hasError = await page.getByText('Import collection failed').first().isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/postman/006-invalid-json.spec.ts b/e2e-tests/import-tests/postman/006-invalid-json.spec.ts new file mode 100644 index 000000000..ef1e881ca --- /dev/null +++ b/e2e-tests/import-tests/postman/006-invalid-json.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; + +test.describe('Invalid Postman Collection - Invalid JSON', () => { + const testDataDir = path.join(__dirname, '../test-data'); + + test('Handle invalid JSON syntax', async ({ page }) => { + const postmanFile = path.join(testDataDir, 'postman-invalid-schema.json'); + + await page.getByRole('button', { name: 'Import Collection' }).click(); + + // Wait for import collection modal to be ready + const importModal = page.getByRole('dialog'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', postmanFile); + + // Wait for the loader to disappear + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + // Check for error message + const hasError = await page.getByText('Conversion failed').first().isVisible(); + expect(hasError).toBe(true); + + // Cleanup: close any open modals + await page.locator('[data-test-id="modal-close-button"]').click(); + }); +}); diff --git a/e2e-tests/import-tests/test-data/bruno-invalid-corrupted.json b/e2e-tests/import-tests/test-data/bruno-invalid-corrupted.json new file mode 100644 index 000000000..5f3348861 --- /dev/null +++ b/e2e-tests/import-tests/test-data/bruno-invalid-corrupted.json @@ -0,0 +1,66 @@ +{ + "version": "1", + "uid": "corrupted_bruno_collection", + "name": "Corrupted Bruno Collection", + "items": [ + { + "uid": "corrupted_request", + "type": "invalid-request-type", + "name": "Invalid Request Type", + "seq": 1, + "request": { + "url": "https://example.com/api", + "method": "INVALID_METHOD", + "headers": "this should be an array not a string", + "params": null, + "body": { + "mode": "invalid-mode", + "invalidField": "this field doesn't exist in schema" + }, + "auth": { + "mode": "unknown-auth-type", + "invalidAuth": { + "badField": "invalid value" + } + }, + "script": "this should be an object not a string", + "vars": "this should be an object not a string", + "assertions": "this should be an array not a string", + "tests": 12345, + "docs": true + } + }, + { + "uid": "missing_required_fields", + "type": "http-request", + "name": "Missing Required Fields", + "seq": 2 + } + ], + "environments": [ + { + "uid": "invalid_env", + "name": "Invalid Environment", + "variables": "this should be an array not a string" + } + ], + "activeEnvironmentUid": "non_existent_environment_id", + "root": { + "request": { + "headers": "invalid headers format", + "auth": { + "mode": "completely-unknown-auth" + }, + "script": 42, + "vars": false, + "tests": null + } + }, + "invalidTopLevelField": "this field doesn't belong here", + "brunoConfig": { + "version": "999", + "name": "Invalid Config", + "type": "invalid-type", + "invalidConfigField": true + } +} diff --git a/e2e-tests/import-tests/test-data/bruno-malformed.json b/e2e-tests/import-tests/test-data/bruno-malformed.json new file mode 100644 index 000000000..3ae3cb16b --- /dev/null +++ b/e2e-tests/import-tests/test-data/bruno-malformed.json @@ -0,0 +1,43 @@ +{ + "version": "1", + "uid": "malformed_bruno_collection", + "name": "Malformed Bruno Collection", + "items": [ + { + "uid": "malformed_request", + "type": "http-request", + "name": "Malformed Request", + "seq": 1, + "request": { + "url": "https://example.com/api", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none" + }, + "auth": { + "mode": "none" + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "" + } + } + ], + "environments": [], + "activeEnvironmentUid": null, + "root": { + "request": { + "headers": [], + "auth": { + "mode": "none" + }, + "script": {}, + "vars": {}, + "tests": "" + } + } + // Missing comma and closing bracket - this makes it malformed JSON \ No newline at end of file diff --git a/e2e-tests/import-tests/test-data/bruno-missing-required-fields.json b/e2e-tests/import-tests/test-data/bruno-missing-required-fields.json new file mode 100644 index 000000000..6ecf37a09 --- /dev/null +++ b/e2e-tests/import-tests/test-data/bruno-missing-required-fields.json @@ -0,0 +1,2939 @@ +{ + "name": "bruno-testbench", + "items": [ + { + "type": "http", + "name": "aaaaa", + "seq": 2, + "request": { + "url": "https://reqres.in/api/users/1", + "method": "PUT", + "headers": [ + { + "name": "Accept", + "value": "application/json", + "enabled": true + }, + { + "name": "Cookie", + "value": "session-id=abc123", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "console.log(req.getCookie());" + }, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "folder", + "name": "auth", + "root": { + "meta": { + "name": "auth" + } + }, + "items": [ + { + "type": "folder", + "name": "basic", + "root": { + "meta": { + "name": "basic" + } + }, + "items": [ + { + "type": "folder", + "name": "via auth", + "root": { + "meta": { + "name": "via auth" + } + }, + "items": [ + { + "type": "http", + "name": "Basic Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "YLpcflD1RLvdkSSvAYimh" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "oqRPDS5d7CLIqBQ5OCEko" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "basic", + "basic": { + "username": "bruno", + "password": "{{basic_auth_password}}" + } + } + } + }, + { + "type": "http", + "name": "Basic Auth 400", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "401", + "enabled": true, + "uid": "WsBvjaJuowT05ri9A8Qc5" + }, + { + "name": "res.body", + "value": "Unauthorized", + "enabled": true, + "uid": "VW1wyd6hu74Yyfzhn0RuQ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "via script", + "root": { + "meta": { + "name": "via script" + } + }, + "items": [ + { + "type": "http", + "name": "Basic Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const username = \"bruno\";\nconst password = \"della\";\n\nconst authString = `${username}:${password}`;\nconst encodedAuthString = require('btoa')(authString);\n\nreq.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "5p6vUUMuLxbA7KnYnXrkT" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "Tk43KT6Hyf3h8Jfeyk2XD" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Basic Auth 401", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const username = \"bruno\";\nconst password = \"invalid\";\n\nconst authString = `${username}:${password}`;\nconst encodedAuthString = require('btoa')(authString);\n\nreq.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "401", + "enabled": true, + "uid": "dLnctBQFSISlaooCYzp5C" + }, + { + "name": "res.body", + "value": "Unauthorized", + "enabled": true, + "uid": "iWPqym01ksxrDV1gfuhWv" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "bearer", + "root": { + "meta": { + "name": "bearer" + } + }, + "items": [ + { + "type": "folder", + "name": "via auth", + "root": { + "meta": { + "name": "via auth" + } + }, + "items": [ + { + "type": "http", + "name": "Bearer Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "F01gjRjDDQefuLn2Vcyed" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "Hmw3BpVyz9tDBEcdA88O0" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "bearer", + "bearer": { + "token": "{{bearer_auth_token}}" + } + } + } + } + ] + }, + { + "type": "folder", + "name": "via headers", + "root": { + "meta": { + "name": "via headers" + } + }, + "items": [ + { + "type": "http", + "name": "Bearer Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [ + { + "name": "Authorization", + "value": "Bearer your_secret_token", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": { + "req": [ + { + "name": "a-c", + "value": "foo", + "enabled": true, + "local": false + } + ] + }, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "VDQ7l9zN9WfS3lEGsJ0aw" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "0n8UMinkhzOuRJCCABVz9" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "cookie", + "root": { + "meta": { + "name": "cookie" + } + }, + "items": [ + { + "type": "http", + "name": "Check", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/cookie/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Login", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/cookie/login", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "inherit auth", + "root": { + "meta": { + "name": "inherit auth" + } + }, + "items": [ + { + "type": "http", + "name": "inherit Bearer Auth 200", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "G6vVLMAqfvpa4aBtQ3WSG" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "QItzfaevVVrycFox5jTlS" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "inherit" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "echo", + "root": { + "meta": { + "name": "echo" + } + }, + "items": [ + { + "type": "http", + "name": "echo bigint", + "seq": 6, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [ + { + "name": "foo", + "value": "bar", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": 990531470713421825,\n \"decimal\": 1.0,\n \"decimal2\": 1.00,\n \"decimal3\": 1.00200,\n \"decimal4\": 0.00\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "lCoGpuLOpVmXPfGzJqbTB" + } + ], + "tests": "// todo: add tests once lossless json echo server is ready", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo bom json", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/bom-json-test", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo form-url-encoded", + "seq": 9, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "formUrlEncoded", + "formUrlEncoded": [ + { + "name": "form-data-key", + "value": "{{form-data-key}}", + "enabled": true + }, + { + "name": "form-data-stringified-object", + "value": "{{form-data-stringified-object}}", + "enabled": true + } + ], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "let obj = JSON.stringify({foo:123});\nbru.setVar('form-data-key', 'form-data-value');\nbru.setVar('form-data-stringified-object', obj);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D", + "enabled": true, + "uid": "V0MSBvq2iFun9gIWfgqtQ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo json", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [ + { + "name": "foo", + "value": "bar", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"foo\", \"foo-world-2\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "FFVx1w4MstKeQfQR66Xy8" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo multipart via scripting", + "seq": 10, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "multipartForm", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const FormData = require(\"form-data\");\nconst form = new FormData();\nform.append('form-data-key', 'form-data-value');\nreq.setBody(form);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "contains form-data-value", + "enabled": true, + "uid": "USCnLx51IlWz6HrLxlR1r" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo multipart", + "seq": 8, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "multipartForm", + "formUrlEncoded": [], + "multipartForm": [ + { + "type": "text", + "name": "form-data-key", + "value": "{{form-data-key}}", + "enabled": true + }, + { + "type": "text", + "name": "form-data-stringified-object", + "value": "{{form-data-stringified-object}}", + "enabled": true + }, + { + "type": "file", + "name": "file", + "value": [ + "bruno.png" + ], + "enabled": true + } + ], + "file": [] + }, + "script": { + "req": "let obj = JSON.stringify({foo:123});\nbru.setVar('form-data-key', 'form-data-value');\nbru.setVar('form-data-stringified-object', obj);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "contains form-data-value", + "enabled": true, + "uid": "L5wrs8CJKD7skDazamdTZ" + }, + { + "name": "res.body", + "value": "contains {\"foo\":123}", + "enabled": true, + "uid": "2rPiaUFbPuWPq0ew6dqVd" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo plaintext", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/text", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "text", + "text": "hello", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "cW2RamEi0zqLmn84SjUoh" + } + ], + "tests": "test(\"should return plain text\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql(\"hello\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml parsed-self closing tags-", + "seq": 6, + "request": { + "url": "{{host}}/api/echo/xml-parsed", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "\n bruno\n \n", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "lIbw7OdlPxbNUKdShGNvi" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": {\n \"world\": [\n \"bruno\",\n \"\"\n ]\n }\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml parsed", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/xml-parsed", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "\n bruno\n", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "5yr5fbjrAre0Cp0C3PY4c" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": {\n \"world\": [\"bruno\"]\n }\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml raw", + "seq": 5, + "request": { + "url": "{{host}}/api/echo/xml-raw", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "bruno", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "graphql", + "root": { + "meta": { + "name": "graphql" + } + }, + "items": [ + { + "type": "graphql", + "name": "spacex", + "seq": 1, + "request": { + "url": "https://spacex-production.up.railway.app/", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "{\n company {\n ceo\n }\n}\n" + }, + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "P99wa88sX4L4Zat94bHzz" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "lib", + "root": { + "meta": { + "name": "lib" + } + } + }, + { + "type": "http", + "name": "ping", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "folder", + "name": "preview", + "root": { + "meta": { + "name": "preview" + } + }, + "items": [ + { + "type": "folder", + "name": "html", + "root": { + "meta": { + "name": "html" + } + }, + "items": [ + { + "type": "http", + "name": "bruno", + "seq": 1, + "request": { + "url": "https://www.github.com", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "console.log(req.getCookie());\n\nconsole.log(req.getHeaders());" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "2bpXrsR3q5MBpkXbO1vfS" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const headers = res.getHeaders();\n expect(headers['content-type']).to.eql(\"text/html; charset=utf-8\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "image", + "root": { + "meta": { + "name": "image" + } + }, + "items": [ + { + "type": "http", + "name": "bruno", + "seq": 1, + "request": { + "url": "https://www.usebruno.com/images/landing-2.png", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should return parsed xml\", function() {\n const headers = res.getHeaders();\n expect(headers['content-type']).to.eql(\"image/png\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "redirects", + "root": { + "meta": { + "name": "redirects" + } + }, + "items": [ + { + "type": "http", + "name": "Disable Redirect", + "seq": 1, + "request": { + "url": "{{host}}/redirect-to-ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setMaxRedirects(0);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "302", + "enabled": true, + "uid": "Bs0jGEFFNAyyRBx6DILEN" + } + ], + "tests": "test(\"should disable redirect to ping\", function() {\n const data = res.getBody();\n expect(data).to.equal('Found. Redirecting to /ping');\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Test Redirect", + "seq": 2, + "request": { + "url": "{{host}}/redirect-to-ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "nNRcxeANwM6VBEXR1qoM0" + }, + { + "name": "res.body", + "value": "pong", + "enabled": true, + "uid": "3Y5SHtNsQHK0glgikD1IU" + } + ], + "tests": "test(\"should redirect to ping\", function() {\n const data = res.getBody();\n expect(data).to.equal('pong');\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "scripting", + "root": { + "meta": { + "name": "scripting" + } + }, + "items": [ + { + "type": "folder", + "name": "api", + "root": { + "meta": { + "name": "api" + } + }, + "items": [ + { + "type": "folder", + "name": "bru", + "root": { + "meta": { + "name": "bru" + } + }, + "items": [ + { + "type": "http", + "name": "getEnvName", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const envName = bru.getEnvName();\nbru.setVar(\"testEnvName\", envName);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should get env name in scripts\", function() {\n const testEnvName = bru.getVar(\"testEnvName\");\n expect(testEnvName).to.equal(\"Prod\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getEnvVar", + "seq": 2, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should get env var in scripts\", function() {\n const host = bru.getEnvVar(\"host\")\n expect(host).to.equal(\"https://testbench-sanity.usebruno.com\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getProcessEnv", + "seq": 6, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"bru.getProcessEnv()\", function() {\n const v = bru.getProcessEnv(\"PROC_ENV_VAR\");\n expect(v).to.equal(\"woof\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getVar", + "seq": 5, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should get var in scripts\", function() {\n const testSetVar = bru.getVar(\"testSetVar\");\n expect(testSetVar).to.equal(\"bruno-test-87267\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setEnvVar", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"testSetEnvVar\", \"bruno-29653\")" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should set env var in scripts\", function() {\n const testSetEnvVar = bru.getEnvVar(\"testSetEnvVar\")\n expect(testSetEnvVar).to.equal(\"bruno-29653\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setVar", + "seq": 4, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setVar(\"testSetVar\", \"bruno-test-87267\")" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should get var in scripts\", function() {\n const testSetVar = bru.getVar(\"testSetVar\");\n expect(testSetVar).to.equal(\"bruno-test-87267\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "req", + "root": { + "meta": { + "name": "req" + } + }, + "items": [ + { + "type": "http", + "name": "getBody", + "seq": 9, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "7VvXmRWwUdGYbvrQaeDMD" + } + ], + "tests": "test(\"req.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeader", + "seq": 5, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "HssG2g6gUaWaBFFanCozP" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "50NSDIeXgRr0TcXEdquci" + } + ], + "tests": "test(\"req.getHeader(name)\", function() {\n const h = req.getHeader('bruno');\n expect(h).to.equal(\"is-awesome\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeaders", + "seq": 7, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + }, + { + "name": "della", + "value": "is-beautiful", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "9NDZWAvBS23WJAZsKl9SS" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "f9ULUob9jYiABYuEzfmgC" + } + ], + "tests": "test(\"req.getHeaders()\", function() {\n const h = req.getHeaders();\n expect(h.bruno).to.equal(\"is-awesome\");\n expect(h.della).to.equal(\"is-beautiful\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getMethod", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "EpNcKUgCYzdg8KrOLqaCX" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "qFqcLtNYbZ9nqhiwvS3Qk" + } + ], + "tests": "test(\"req.getMethod()()\", function() {\n const method = req.getMethod();\n expect(method).to.equal(\"GET\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getUrl", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "VaiDs2JU1NM8prTc59GdX" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "4vC9zp5XBajbYKDYM4oFN" + } + ], + "tests": "test(\"req.getUrl()\", function() {\n const url = req.getUrl();\n expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setBody", + "seq": 10, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setBody({\n \"bruno\": \"is awesome\"\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "EGlBavIEZ2j0s2aczQxAP" + } + ], + "tests": "test(\"req.setBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"bruno\": \"is awesome\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setHeader", + "seq": 6, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setHeader('bruno', 'is-the-future');" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "J9AUIh6CbTnIxlCyKqqq7" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "sb6dpEtw8SYyeXVqEz3OA" + } + ], + "tests": "test(\"req.setHeader(name)\", function() {\n const h = req.getHeader('bruno');\n expect(h).to.equal(\"is-the-future\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setHeaders", + "seq": 8, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + }, + { + "name": "della", + "value": "is-beautiful", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setHeaders({\n \"content-type\": \"application/text\",\n \"transaction-id\": \"foobar\"\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "1djJxGMAwmAHF2ruhdQQO" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "sqvBwQilTBWnFDoIBAC4T" + } + ], + "tests": "test(\"req.setHeaders()\", function() {\n const h = req.getHeaders();\n expect(h['content-type']).to.equal(\"application/text\");\n expect(h['transaction-id']).to.equal(\"foobar\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setMethod", + "seq": 4, + "request": { + "url": "{{host}}/ping", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setMethod(\"GET\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "3eRRXHvWErUAC2IiBto7B" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "UAqBwv3S2607RXzgC6S1i" + } + ], + "tests": "test(\"req.setMethod()()\", function() {\n const method = req.getMethod();\n expect(method).to.equal(\"GET\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setUrl", + "seq": 2, + "request": { + "url": "{{host}}/ping/invalid", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setUrl(\"https://testbench-sanity.usebruno.com/ping\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "sGLaBON85rqipc1VP8R3W" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "SPVLRHys6RQh1GbRWnPpT" + } + ], + "tests": "test(\"req.setUrl()\", function() {\n const url = req.getUrl();\n expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "res", + "root": { + "meta": { + "name": "res" + } + }, + "items": [ + { + "type": "http", + "name": "getBody", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "rQe4gitaooPrQkEqD6AvL" + } + ], + "tests": "test(\"res.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeader", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "LOem8LkfWBML3yI1Kh3ZU" + } + ], + "tests": "test(\"res.getHeader(name)\", function() {\n const server = res.getHeader('x-powered-by');\n expect(server).to.eql('Express');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeaders", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "qF8ASpikHJzQLRS0ZqNkA" + } + ], + "tests": "test(\"res.getHeaders(name)\", function() {\n const h = res.getHeaders();\n expect(h['x-powered-by']).to.eql('Express');\n expect(h['content-length']).to.eql('17');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getResponseTime", + "seq": 5, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "pqN8P939S4dfZ1Bmuemi7" + } + ], + "tests": "test(\"res.getResponseTime()\", function() {\n const responseTime = res.getResponseTime();\n expect(typeof responseTime).to.eql(\"number\");\n expect(responseTime > 0).to.be.true;\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getStatus", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "0DqeIPuHtcmaULlYG9eWu" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "WZDxwKadXkDkK3Ea1VMKW" + } + ], + "tests": "test(\"res.getStatus()\", function() {\n const status = res.getStatus()\n expect(status).to.equal(200);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "inbuilt modules", + "root": { + "meta": { + "name": "inbuilt modules" + } + }, + "items": [ + { + "type": "folder", + "name": "axios", + "root": { + "meta": { + "name": "axios" + } + }, + "items": [ + { + "type": "http", + "name": "axios-pre-req-script", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const axios = require(\"axios\");\n\nconst url = \"https://testbench-sanity.usebruno.com/api/echo/json\";\nconst response = await axios.post(url, {\n \"hello\": \"bruno\"\n});\n\nreq.setBody(response.data);\nreq.setMethod(\"POST\");\nreq.setUrl(url);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"req.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "crypto-js", + "root": { + "meta": { + "name": "crypto-js" + } + }, + "items": [ + { + "type": "http", + "name": "crypto-js-pre-request-script", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "var CryptoJS = require(\"crypto-js\");\n\n// Encrypt\nvar ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();\n\n// Decrypt\nvar bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');\nvar originalText = bytes.toString(CryptoJS.enc.Utf8);\n\nbru.setVar('crypto-test-message', originalText);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"crypto message\", function() {\n const data = bru.getVar('crypto-test-message');\n bru.setVar('crypto-test-message', null);\n expect(data).to.eql('my message');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "nanoid", + "root": { + "meta": { + "name": "nanoid" + } + }, + "items": [ + { + "type": "http", + "name": "nanoid", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { nanoid } = require(\"nanoid\");\n \nbru.setVar(\"nanoid-test-id\", nanoid());" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"nanoid var\", function() {\n const id = bru.getVar('nanoid-test-id');\n let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)\n bru.setVar('nanoid-test-id', null);\n expect(isValidNanoid).to.eql(true);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "uuid", + "root": { + "meta": { + "name": "uuid" + } + }, + "items": [ + { + "type": "http", + "name": "uuid", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { v4 } = require(\"uuid\");\n \nbru.setVar(\"uuid-test-id\", v4());" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"uuid var\", function() {\n const id = bru.getVar('uuid-test-id');\n let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);\n bru.setVar('uuid-test-id', null);\n expect(isValidUuid).to.eql(true);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "js", + "root": { + "meta": { + "name": "js" + } + }, + "items": [ + { + "type": "http", + "name": "data types - request vars", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"boolean\": false,\n \"number_1\": 1,\n \"number_2\": 0,\n \"number_3\": -1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "req.body.boolean", + "value": "isBoolean false", + "enabled": true, + "uid": "SgXkeY8p7ahXIq2kA9FzA" + }, + { + "name": "req.body.number_1", + "value": "isNumber 1", + "enabled": true, + "uid": "lhS17xvEP5jzHGP2Uqfg9" + }, + { + "name": "req.body.undefined", + "value": "isUndefined undefined", + "enabled": true, + "uid": "bFTk8cAUAzNnrvUhbNeyC" + }, + { + "name": "req.body.string", + "value": "isString bruno", + "enabled": true, + "uid": "ohANzzhuM8E8egvoVy20M" + }, + { + "name": "req.body.null", + "value": "isNull null", + "enabled": true, + "uid": "r6W6I7ATDVswqkAf7Kl1k" + }, + { + "name": "req.body.array", + "value": "isArray", + "enabled": true, + "uid": "fFUuv0vldfqaAGPjhfmdl" + }, + { + "name": "req.body.boolean", + "value": "eq false", + "enabled": true, + "uid": "eXPS2R19qWsPGm6usEogu" + }, + { + "name": "req.body.number_1", + "value": "eq 1", + "enabled": true, + "uid": "WCKmMIqsFPwocy6LZmCcc" + }, + { + "name": "req.body.undefined", + "value": "eq undefined", + "enabled": true, + "uid": "7fJRYC8ELm68Uc5CaB7B8" + }, + { + "name": "req.body.string", + "value": "eq bruno", + "enabled": true, + "uid": "fXTl58gxhAUrUM8SZFxLW" + }, + { + "name": "req.body.null", + "value": "eq null", + "enabled": true, + "uid": "yUhXaWTPJaUYU4zerBABN" + }, + { + "name": "req.body.number_2", + "value": "eq 0", + "enabled": true, + "uid": "WWCCm6i8GzyNBH6xiQGAP" + }, + { + "name": "req.body.number_3", + "value": "eq -1", + "enabled": true, + "uid": "G73JIdrxSUDpc33EfAZGW" + }, + { + "name": "req.body.number_2", + "value": "isNumber", + "enabled": true, + "uid": "Dp5pdDeMEulfPB3ZDdjl4" + }, + { + "name": "req.body.number_3", + "value": "isNumber", + "enabled": true, + "uid": "fWghLNcBbslVF2OJMSS6x" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "data types", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"boolean\": false,\n \"number\": 1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const reqBody = req.getBody();\n\nbru.setVar(\"dataTypeVarTest\", {\n ...reqBody,\n \"undefined\": undefined\n});" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"data types check via bru var\", function() {\n let v = bru.getVar(\"dataTypeVarTest\");\n v = {\n ...v,\n \"undefined\": undefined\n };\n expect(v).to.eql({\n \"boolean\": false,\n \"number\": 1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null,\n \"undefined\": undefined\n })\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setTimeout", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"test-js-set-timeout\", \"\");\nawait new Promise((resolve, reject) => {\n setTimeout(() => {\n bru.setVar(\"test-js-set-timeout\", \"bruno\");\n resolve();\n }, 1000);\n});\n\nconst v = bru.getVar(\"test-js-set-timeout\");\nbru.setVar(\"test-js-set-timeout\", v + \"-is-awesome\");\n" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"setTimeout()\", function() {\n const v = bru.getVar(\"test-js-set-timeout\")\n expect(v).to.eql(\"bruno-is-awesome\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "local modules", + "root": { + "meta": { + "name": "local modules" + } + }, + "items": [ + { + "type": "http", + "name": "invalid and valid module imports", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "try {\n bru.setVar('invalid_module_error_thrown', false);\n // should throw an error\n const invalid = require(\"./lib/invalid\");\n}\ncatch(error) {\n bru.setVar('invalid_module_error_thrown', true);\n}\n\n\ntry {\n bru.setVar('valid_module_no_error', true);\n // should not throw an error\n const math = require(\"./lib/math\");\n}\ncatch(error) {\n bru.setVar('valid_module_no_error', false);\n}" + }, + "vars": {}, + "assertions": [ + { + "name": "invalid_module_error_thrown", + "value": "eq true", + "enabled": true, + "uid": "wdZ0MsGXmW7tRiX4VtQaT" + }, + { + "name": "valid_module_no_error", + "value": "eq true", + "enabled": true, + "uid": "A7hwDplpc0qDp5Bk46AMJ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "sum -without js extn-", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"a\": 1,\n \"b\": 2\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const math = require(\"./lib/math\");\nconsole.log(math, 'math');\n\nconst body = req.getBody();\nbody.sum = math.sum(body.a, body.b);\nbody.areaOfCircle = math.areaOfCircle(2);\n\nreq.setBody(body);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "R6TRM5HoxKGuC5dHFvYdH" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3,\n \"areaOfCircle\": 12.56\n });\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "sum", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"a\": 1,\n \"b\": 2\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const math = require(\"./lib/math.js\"); \nconst body = req.getBody();\nbody.sum = math.sum(body.a, body.b);\n\nreq.setBody(body);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "UXDIZRejDajw3j0oYHhij" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});\n\ntest(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});\n\ntest(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "npm modules", + "root": { + "meta": { + "name": "npm modules" + } + }, + "items": [ + { + "type": "http", + "name": "fakerjs", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { faker } = require('@faker-js/faker');\nconst uuid = faker.string.uuid();\n\nconst data = req.getBody();\ndata.uuid = uuid;\n\nreq.setBody(data);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "ZVuM9BByoo5XFCtUYTwfP" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n const isUUID = (inputString) => {\n return uuidRegex.test(inputString);\n };\n \n expect(data.hello).to.equal(\"bruno\");\n expect(isUUID(data.uuid)).to.be.true;\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "string interpolation", + "root": { + "request": { + "vars": { + "req": [ + { + "name": "folder_pre_var", + "value": "folder_pre_var_value", + "enabled": true, + "local": false, + "uid": "OHd64NVOj1HQV2PLqRzy8" + }, + { + "name": "folder_pre_var_2", + "value": "{{env.var1}}", + "enabled": true, + "local": false, + "uid": "J12VEAvPGi3R0wBKXy2jK" + } + ] + } + }, + "meta": { + "name": "string interpolation" + } + }, + "items": [ + { + "type": "http", + "name": "env vars", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"envVar1\": \"{{env.var1}}\",\n \"envVar2\": \"{{env-var2}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "mxXvAcLVxfpRpGqh76ugy" + } + ], + "tests": "test(\"should return json\", function() {\n expect(res.getBody()).to.eql({\n \"envVar1\": \"envVar1\",\n \"envVar2\": \"envVar2\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "missing values", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json?foo={{undefinedVar}}", + "method": "POST", + "headers": [], + "params": [ + { + "name": "foo", + "value": "{{undefinedVar}}", + "type": "query", + "enabled": true + } + ], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"{{undefinedVar2}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "nzPl7aN2MK9uc3SKEepG0" + } + ], + "tests": "test(\"should return json\", function() {\n const url = req.getUrl();\n const query = url.split(\"?\")[1];\n expect(query).to.equal(\"foo={{undefinedVar}}\");\n\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": \"{{undefinedVar2}}\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "process env vars", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"bark\": \"{{bark}}\",\n \"bark2\": \"{{process.env.PROC_ENV_VAR}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "T1m34pWveQfE4Aao7Xqlt" + } + ], + "tests": "test(\"should return json\", function() {\n expect(res.getBody()).to.eql({\n \"bark\": \"woof\",\n \"bark2\": \"woof\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "runtime vars", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/text", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "text", + "json": "{\n \"envVar1\": \"{{env.var1}}\",\n \"envVar2\": \"{{env-var2}}\"\n}", + "text": "Hi, I am {{rUser.full_name}},\nI am {{rUser.age}} years old.\nMy favorite food is {{rUser.fav-food[0]}} and {{rUser.fav-food[1]}}.\nI like attention: {{rUser.want.attention}}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"rUser\", {\n full_name: 'Bruno',\n age: 4,\n 'fav-food': ['egg', 'meat'],\n 'want.attention': true\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "xe4IAIu4EXYOYiXmKU374" + } + ], + "tests": "test(\"should return json\", function() {\n const expectedResponse = `Hi, I am Bruno,\nI am 4 years old.\nMy favorite food is egg and meat.\nI like attention: true`;\n expect(res.getBody()).to.equal(expectedResponse);\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ], + "activeEnvironmentUid": "s4jJkWbb9017JXdVqOxLR", + "environments": [ + { + "variables": [ + { + "name": "host", + "value": "http://localhost:8080", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bearer_auth_token", + "value": "your_secret_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "basic_auth_password", + "value": "della", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "client_id", + "value": "client_id_1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "client_secret", + "value": "client_secret_1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "auth_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "callback_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/callback", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "access_token_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "passwordCredentials_username", + "value": "foo", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "passwordCredentials_password", + "value": "bar", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_authorize_url", + "value": "https://github.com/login/oauth/authorize", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_access_token_url", + "value": "https://github.com/login/oauth/access_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_auth_url", + "value": "https://accounts.google.com/o/oauth2/auth", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_access_token_url", + "value": "https://accounts.google.com/o/oauth2/token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_scope", + "value": "https://www.googleapis.com/auth/userinfo.email", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_client_secret", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_client_id", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "google_client_id", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "google_client_secret", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_authorization_code", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "passwordCredentials_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "client_credentials_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "authorization_code_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + } + ], + "name": "Local" + }, + { + "variables": [ + { + "name": "host", + "value": "https://testbench-sanity.usebruno.com", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bearer_auth_token", + "value": "your_secret_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "basic_auth_password", + "value": "della", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "env.var1", + "value": "envVar1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "env-var2", + "value": "envVar2", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bark", + "value": "{{process.env.PROC_ENV_VAR}}", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "foo", + "value": "bar", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "testSetEnvVar", + "value": "bruno-29653", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "echo-host", + "value": "https://echo.usebruno.com", + "enabled": true, + "secret": false, + "type": "text" + } + ], + "name": "Prod" + } + ], + "root": { + "request": { + "auth": { + "mode": "bearer", + "bearer": { + "token": "{{bearer_auth_token}}" + } + }, + "headers": [ + { + "name": "check", + "value": "again", + "enabled": true, + "uid": "wbTRFykhPHZwnzVUTd1gr" + }, + { + "name": "token", + "value": "{{collection_pre_var_token}}", + "enabled": true, + "uid": "YGZ16VXf9NusINngKeXqn" + } + ], + "vars": { + "req": [ + { + "name": "collection_pre_var", + "value": "collection_pre_var_value", + "enabled": true, + "local": false, + "uid": "HI7DgTPA1gBLB6lIl1t3O" + }, + { + "name": "collection_pre_var_token", + "value": "{{request_pre_var_token}}", + "enabled": true, + "local": false, + "uid": "FoDj77i1KoZ6Koq9oavPy" + } + ] + } + }, + "docs": "# bruno-testbench 🐶\n\nThis is a test collection that I am using to test various functionalities around bruno" + }, + "brunoConfig": { + "version": "1", + "name": "bruno-testbench", + "type": "collection", + "proxy": { + "enabled": false, + "protocol": "http", + "hostname": "{{proxyHostname}}", + "port": 4000, + "auth": { + "enabled": false, + "username": "anoop" + }, + "bypassProxy": "" + }, + "scripts": { + "moduleWhitelist": [ + "crypto", + "buffer", + "form-data" + ], + "filesystemAccess": { + "allow": true + } + }, + "clientCertificates": { + "enabled": true, + "certs": [] + }, + "presets": { + "requestType": "http", + "requestUrl": "http://localhost:6000" + }, + "ignore": [ + "node_modules", + ".git" + ], + "size": 0.026633262634277344, + "filesCount": 96 + } +} \ No newline at end of file diff --git a/e2e-tests/import-tests/test-data/bruno-testbench.json b/e2e-tests/import-tests/test-data/bruno-testbench.json new file mode 100644 index 000000000..02a9c29fb --- /dev/null +++ b/e2e-tests/import-tests/test-data/bruno-testbench.json @@ -0,0 +1,2940 @@ +{ + "name": "bruno-testbench", + "version": "1", + "items": [ + { + "type": "http", + "name": "aaaaa", + "seq": 2, + "request": { + "url": "https://reqres.in/api/users/1", + "method": "PUT", + "headers": [ + { + "name": "Accept", + "value": "application/json", + "enabled": true + }, + { + "name": "Cookie", + "value": "session-id=abc123", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "console.log(req.getCookie());" + }, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "folder", + "name": "auth", + "root": { + "meta": { + "name": "auth" + } + }, + "items": [ + { + "type": "folder", + "name": "basic", + "root": { + "meta": { + "name": "basic" + } + }, + "items": [ + { + "type": "folder", + "name": "via auth", + "root": { + "meta": { + "name": "via auth" + } + }, + "items": [ + { + "type": "http", + "name": "Basic Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "YLpcflD1RLvdkSSvAYimh" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "oqRPDS5d7CLIqBQ5OCEko" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "basic", + "basic": { + "username": "bruno", + "password": "{{basic_auth_password}}" + } + } + } + }, + { + "type": "http", + "name": "Basic Auth 400", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "401", + "enabled": true, + "uid": "WsBvjaJuowT05ri9A8Qc5" + }, + { + "name": "res.body", + "value": "Unauthorized", + "enabled": true, + "uid": "VW1wyd6hu74Yyfzhn0RuQ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "via script", + "root": { + "meta": { + "name": "via script" + } + }, + "items": [ + { + "type": "http", + "name": "Basic Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const username = \"bruno\";\nconst password = \"della\";\n\nconst authString = `${username}:${password}`;\nconst encodedAuthString = require('btoa')(authString);\n\nreq.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "5p6vUUMuLxbA7KnYnXrkT" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "Tk43KT6Hyf3h8Jfeyk2XD" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Basic Auth 401", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/basic/protected", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const username = \"bruno\";\nconst password = \"invalid\";\n\nconst authString = `${username}:${password}`;\nconst encodedAuthString = require('btoa')(authString);\n\nreq.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "401", + "enabled": true, + "uid": "dLnctBQFSISlaooCYzp5C" + }, + { + "name": "res.body", + "value": "Unauthorized", + "enabled": true, + "uid": "iWPqym01ksxrDV1gfuhWv" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "bearer", + "root": { + "meta": { + "name": "bearer" + } + }, + "items": [ + { + "type": "folder", + "name": "via auth", + "root": { + "meta": { + "name": "via auth" + } + }, + "items": [ + { + "type": "http", + "name": "Bearer Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "F01gjRjDDQefuLn2Vcyed" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "Hmw3BpVyz9tDBEcdA88O0" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "bearer", + "bearer": { + "token": "{{bearer_auth_token}}" + } + } + } + } + ] + }, + { + "type": "folder", + "name": "via headers", + "root": { + "meta": { + "name": "via headers" + } + }, + "items": [ + { + "type": "http", + "name": "Bearer Auth 200", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [ + { + "name": "Authorization", + "value": "Bearer your_secret_token", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": { + "req": [ + { + "name": "a-c", + "value": "foo", + "enabled": true, + "local": false + } + ] + }, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "VDQ7l9zN9WfS3lEGsJ0aw" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "0n8UMinkhzOuRJCCABVz9" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "cookie", + "root": { + "meta": { + "name": "cookie" + } + }, + "items": [ + { + "type": "http", + "name": "Check", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/cookie/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Login", + "seq": 1, + "request": { + "url": "{{host}}/api/auth/cookie/login", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "inherit auth", + "root": { + "meta": { + "name": "inherit auth" + } + }, + "items": [ + { + "type": "http", + "name": "inherit Bearer Auth 200", + "seq": 2, + "request": { + "url": "{{host}}/api/auth/bearer/protected", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"foo\", \"bar\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "G6vVLMAqfvpa4aBtQ3WSG" + }, + { + "name": "res.body.message", + "value": "Authentication successful", + "enabled": true, + "uid": "QItzfaevVVrycFox5jTlS" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "inherit" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "echo", + "root": { + "meta": { + "name": "echo" + } + }, + "items": [ + { + "type": "http", + "name": "echo bigint", + "seq": 6, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [ + { + "name": "foo", + "value": "bar", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": 990531470713421825,\n \"decimal\": 1.0,\n \"decimal2\": 1.00,\n \"decimal3\": 1.00200,\n \"decimal4\": 0.00\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "lCoGpuLOpVmXPfGzJqbTB" + } + ], + "tests": "// todo: add tests once lossless json echo server is ready", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo bom json", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/bom-json-test", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo form-url-encoded", + "seq": 9, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "formUrlEncoded", + "formUrlEncoded": [ + { + "name": "form-data-key", + "value": "{{form-data-key}}", + "enabled": true + }, + { + "name": "form-data-stringified-object", + "value": "{{form-data-stringified-object}}", + "enabled": true + } + ], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "let obj = JSON.stringify({foo:123});\nbru.setVar('form-data-key', 'form-data-value');\nbru.setVar('form-data-stringified-object', obj);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D", + "enabled": true, + "uid": "V0MSBvq2iFun9gIWfgqtQ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo json", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [ + { + "name": "foo", + "value": "bar", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"foo\", \"foo-world-2\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "FFVx1w4MstKeQfQR66Xy8" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo multipart via scripting", + "seq": 10, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "multipartForm", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const FormData = require(\"form-data\");\nconst form = new FormData();\nform.append('form-data-key', 'form-data-value');\nreq.setBody(form);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "contains form-data-value", + "enabled": true, + "uid": "USCnLx51IlWz6HrLxlR1r" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo multipart", + "seq": 8, + "request": { + "url": "{{echo-host}}", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "multipartForm", + "formUrlEncoded": [], + "multipartForm": [ + { + "type": "text", + "name": "form-data-key", + "value": "{{form-data-key}}", + "enabled": true + }, + { + "type": "text", + "name": "form-data-stringified-object", + "value": "{{form-data-stringified-object}}", + "enabled": true + }, + { + "type": "file", + "name": "file", + "value": [ + "bruno.png" + ], + "enabled": true + } + ], + "file": [] + }, + "script": { + "req": "let obj = JSON.stringify({foo:123});\nbru.setVar('form-data-key', 'form-data-value');\nbru.setVar('form-data-stringified-object', obj);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.body", + "value": "contains form-data-value", + "enabled": true, + "uid": "L5wrs8CJKD7skDazamdTZ" + }, + { + "name": "res.body", + "value": "contains {\"foo\":123}", + "enabled": true, + "uid": "2rPiaUFbPuWPq0ew6dqVd" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo plaintext", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/text", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "text", + "text": "hello", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "cW2RamEi0zqLmn84SjUoh" + } + ], + "tests": "test(\"should return plain text\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql(\"hello\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml parsed-self closing tags-", + "seq": 6, + "request": { + "url": "{{host}}/api/echo/xml-parsed", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "\n bruno\n \n", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "lIbw7OdlPxbNUKdShGNvi" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": {\n \"world\": [\n \"bruno\",\n \"\"\n ]\n }\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml parsed", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/xml-parsed", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "\n bruno\n", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "5yr5fbjrAre0Cp0C3PY4c" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": {\n \"world\": [\"bruno\"]\n }\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "echo xml raw", + "seq": 5, + "request": { + "url": "{{host}}/api/echo/xml-raw", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "xml", + "xml": "bruno", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "graphql", + "root": { + "meta": { + "name": "graphql" + } + }, + "items": [ + { + "type": "graphql", + "name": "spacex", + "seq": 1, + "request": { + "url": "https://spacex-production.up.railway.app/", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "{\n company {\n ceo\n }\n}\n" + }, + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "P99wa88sX4L4Zat94bHzz" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "lib", + "root": { + "meta": { + "name": "lib" + } + } + }, + { + "type": "http", + "name": "ping", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "folder", + "name": "preview", + "root": { + "meta": { + "name": "preview" + } + }, + "items": [ + { + "type": "folder", + "name": "html", + "root": { + "meta": { + "name": "html" + } + }, + "items": [ + { + "type": "http", + "name": "bruno", + "seq": 1, + "request": { + "url": "https://www.github.com", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "console.log(req.getCookie());\n\nconsole.log(req.getHeaders());" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "2bpXrsR3q5MBpkXbO1vfS" + } + ], + "tests": "test(\"should return parsed xml\", function() {\n const headers = res.getHeaders();\n expect(headers['content-type']).to.eql(\"text/html; charset=utf-8\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "image", + "root": { + "meta": { + "name": "image" + } + }, + "items": [ + { + "type": "http", + "name": "bruno", + "seq": 1, + "request": { + "url": "https://www.usebruno.com/images/landing-2.png", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should return parsed xml\", function() {\n const headers = res.getHeaders();\n expect(headers['content-type']).to.eql(\"image/png\");\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "redirects", + "root": { + "meta": { + "name": "redirects" + } + }, + "items": [ + { + "type": "http", + "name": "Disable Redirect", + "seq": 1, + "request": { + "url": "{{host}}/redirect-to-ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setMaxRedirects(0);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "302", + "enabled": true, + "uid": "Bs0jGEFFNAyyRBx6DILEN" + } + ], + "tests": "test(\"should disable redirect to ping\", function() {\n const data = res.getBody();\n expect(data).to.equal('Found. Redirecting to /ping');\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "Test Redirect", + "seq": 2, + "request": { + "url": "{{host}}/redirect-to-ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "200", + "enabled": true, + "uid": "nNRcxeANwM6VBEXR1qoM0" + }, + { + "name": "res.body", + "value": "pong", + "enabled": true, + "uid": "3Y5SHtNsQHK0glgikD1IU" + } + ], + "tests": "test(\"should redirect to ping\", function() {\n const data = res.getBody();\n expect(data).to.equal('pong');\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "scripting", + "root": { + "meta": { + "name": "scripting" + } + }, + "items": [ + { + "type": "folder", + "name": "api", + "root": { + "meta": { + "name": "api" + } + }, + "items": [ + { + "type": "folder", + "name": "bru", + "root": { + "meta": { + "name": "bru" + } + }, + "items": [ + { + "type": "http", + "name": "getEnvName", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const envName = bru.getEnvName();\nbru.setVar(\"testEnvName\", envName);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should get env name in scripts\", function() {\n const testEnvName = bru.getVar(\"testEnvName\");\n expect(testEnvName).to.equal(\"Prod\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getEnvVar", + "seq": 2, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should get env var in scripts\", function() {\n const host = bru.getEnvVar(\"host\")\n expect(host).to.equal(\"https://testbench-sanity.usebruno.com\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getProcessEnv", + "seq": 6, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"bru.getProcessEnv()\", function() {\n const v = bru.getProcessEnv(\"PROC_ENV_VAR\");\n expect(v).to.equal(\"woof\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getVar", + "seq": 5, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [], + "tests": "test(\"should get var in scripts\", function() {\n const testSetVar = bru.getVar(\"testSetVar\");\n expect(testSetVar).to.equal(\"bruno-test-87267\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setEnvVar", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setEnvVar(\"testSetEnvVar\", \"bruno-29653\")" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should set env var in scripts\", function() {\n const testSetEnvVar = bru.getEnvVar(\"testSetEnvVar\")\n expect(testSetEnvVar).to.equal(\"bruno-29653\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setVar", + "seq": 4, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "res": "bru.setVar(\"testSetVar\", \"bruno-test-87267\")" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"should get var in scripts\", function() {\n const testSetVar = bru.getVar(\"testSetVar\");\n expect(testSetVar).to.equal(\"bruno-test-87267\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "req", + "root": { + "meta": { + "name": "req" + } + }, + "items": [ + { + "type": "http", + "name": "getBody", + "seq": 9, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "7VvXmRWwUdGYbvrQaeDMD" + } + ], + "tests": "test(\"req.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeader", + "seq": 5, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "HssG2g6gUaWaBFFanCozP" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "50NSDIeXgRr0TcXEdquci" + } + ], + "tests": "test(\"req.getHeader(name)\", function() {\n const h = req.getHeader('bruno');\n expect(h).to.equal(\"is-awesome\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeaders", + "seq": 7, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + }, + { + "name": "della", + "value": "is-beautiful", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "9NDZWAvBS23WJAZsKl9SS" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "f9ULUob9jYiABYuEzfmgC" + } + ], + "tests": "test(\"req.getHeaders()\", function() {\n const h = req.getHeaders();\n expect(h.bruno).to.equal(\"is-awesome\");\n expect(h.della).to.equal(\"is-beautiful\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getMethod", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "EpNcKUgCYzdg8KrOLqaCX" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "qFqcLtNYbZ9nqhiwvS3Qk" + } + ], + "tests": "test(\"req.getMethod()()\", function() {\n const method = req.getMethod();\n expect(method).to.equal(\"GET\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getUrl", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "VaiDs2JU1NM8prTc59GdX" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "4vC9zp5XBajbYKDYM4oFN" + } + ], + "tests": "test(\"req.getUrl()\", function() {\n const url = req.getUrl();\n expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setBody", + "seq": 10, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setBody({\n \"bruno\": \"is awesome\"\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "EGlBavIEZ2j0s2aczQxAP" + } + ], + "tests": "test(\"req.setBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"bruno\": \"is awesome\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setHeader", + "seq": 6, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setHeader('bruno', 'is-the-future');" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "J9AUIh6CbTnIxlCyKqqq7" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "sb6dpEtw8SYyeXVqEz3OA" + } + ], + "tests": "test(\"req.setHeader(name)\", function() {\n const h = req.getHeader('bruno');\n expect(h).to.equal(\"is-the-future\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setHeaders", + "seq": 8, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [ + { + "name": "bruno", + "value": "is-awesome", + "enabled": true + }, + { + "name": "della", + "value": "is-beautiful", + "enabled": true + } + ], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setHeaders({\n \"content-type\": \"application/text\",\n \"transaction-id\": \"foobar\"\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "1djJxGMAwmAHF2ruhdQQO" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "sqvBwQilTBWnFDoIBAC4T" + } + ], + "tests": "test(\"req.setHeaders()\", function() {\n const h = req.getHeaders();\n expect(h['content-type']).to.equal(\"application/text\");\n expect(h['transaction-id']).to.equal(\"foobar\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setMethod", + "seq": 4, + "request": { + "url": "{{host}}/ping", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setMethod(\"GET\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "3eRRXHvWErUAC2IiBto7B" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "UAqBwv3S2607RXzgC6S1i" + } + ], + "tests": "test(\"req.setMethod()()\", function() {\n const method = req.getMethod();\n expect(method).to.equal(\"GET\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setUrl", + "seq": 2, + "request": { + "url": "{{host}}/ping/invalid", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "req.setUrl(\"https://testbench-sanity.usebruno.com/ping\");" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "sGLaBON85rqipc1VP8R3W" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "SPVLRHys6RQh1GbRWnPpT" + } + ], + "tests": "test(\"req.setUrl()\", function() {\n const url = req.getUrl();\n expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "res", + "root": { + "meta": { + "name": "res" + } + }, + "items": [ + { + "type": "http", + "name": "getBody", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "rQe4gitaooPrQkEqD6AvL" + } + ], + "tests": "test(\"res.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeader", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "LOem8LkfWBML3yI1Kh3ZU" + } + ], + "tests": "test(\"res.getHeader(name)\", function() {\n const server = res.getHeader('x-powered-by');\n expect(server).to.eql('Express');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getHeaders", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "qF8ASpikHJzQLRS0ZqNkA" + } + ], + "tests": "test(\"res.getHeaders(name)\", function() {\n const h = res.getHeaders();\n expect(h['x-powered-by']).to.eql('Express');\n expect(h['content-length']).to.eql('17');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getResponseTime", + "seq": 5, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "pqN8P939S4dfZ1Bmuemi7" + } + ], + "tests": "test(\"res.getResponseTime()\", function() {\n const responseTime = res.getResponseTime();\n expect(typeof responseTime).to.eql(\"number\");\n expect(responseTime > 0).to.be.true;\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "getStatus", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "0DqeIPuHtcmaULlYG9eWu" + }, + { + "name": "res.body", + "value": "eq pong", + "enabled": true, + "uid": "WZDxwKadXkDkK3Ea1VMKW" + } + ], + "tests": "test(\"res.getStatus()\", function() {\n const status = res.getStatus()\n expect(status).to.equal(200);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "inbuilt modules", + "root": { + "meta": { + "name": "inbuilt modules" + } + }, + "items": [ + { + "type": "folder", + "name": "axios", + "root": { + "meta": { + "name": "axios" + } + }, + "items": [ + { + "type": "http", + "name": "axios-pre-req-script", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const axios = require(\"axios\");\n\nconst url = \"https://testbench-sanity.usebruno.com/api/echo/json\";\nconst response = await axios.post(url, {\n \"hello\": \"bruno\"\n});\n\nreq.setBody(response.data);\nreq.setMethod(\"POST\");\nreq.setUrl(url);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"req.getBody()\", function() {\n const data = res.getBody();\n expect(data).to.eql({\n \"hello\": \"bruno\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "crypto-js", + "root": { + "meta": { + "name": "crypto-js" + } + }, + "items": [ + { + "type": "http", + "name": "crypto-js-pre-request-script", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "var CryptoJS = require(\"crypto-js\");\n\n// Encrypt\nvar ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();\n\n// Decrypt\nvar bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');\nvar originalText = bytes.toString(CryptoJS.enc.Utf8);\n\nbru.setVar('crypto-test-message', originalText);" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"crypto message\", function() {\n const data = bru.getVar('crypto-test-message');\n bru.setVar('crypto-test-message', null);\n expect(data).to.eql('my message');\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "nanoid", + "root": { + "meta": { + "name": "nanoid" + } + }, + "items": [ + { + "type": "http", + "name": "nanoid", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { nanoid } = require(\"nanoid\");\n \nbru.setVar(\"nanoid-test-id\", nanoid());" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"nanoid var\", function() {\n const id = bru.getVar('nanoid-test-id');\n let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)\n bru.setVar('nanoid-test-id', null);\n expect(isValidNanoid).to.eql(true);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "uuid", + "root": { + "meta": { + "name": "uuid" + } + }, + "items": [ + { + "type": "http", + "name": "uuid", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { v4 } = require(\"uuid\");\n \nbru.setVar(\"uuid-test-id\", v4());" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"uuid var\", function() {\n const id = bru.getVar('uuid-test-id');\n let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);\n bru.setVar('uuid-test-id', null);\n expect(isValidUuid).to.eql(true);\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "js", + "root": { + "meta": { + "name": "js" + } + }, + "items": [ + { + "type": "http", + "name": "data types - request vars", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"boolean\": false,\n \"number_1\": 1,\n \"number_2\": 0,\n \"number_3\": -1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "req.body.boolean", + "value": "isBoolean false", + "enabled": true, + "uid": "SgXkeY8p7ahXIq2kA9FzA" + }, + { + "name": "req.body.number_1", + "value": "isNumber 1", + "enabled": true, + "uid": "lhS17xvEP5jzHGP2Uqfg9" + }, + { + "name": "req.body.undefined", + "value": "isUndefined undefined", + "enabled": true, + "uid": "bFTk8cAUAzNnrvUhbNeyC" + }, + { + "name": "req.body.string", + "value": "isString bruno", + "enabled": true, + "uid": "ohANzzhuM8E8egvoVy20M" + }, + { + "name": "req.body.null", + "value": "isNull null", + "enabled": true, + "uid": "r6W6I7ATDVswqkAf7Kl1k" + }, + { + "name": "req.body.array", + "value": "isArray", + "enabled": true, + "uid": "fFUuv0vldfqaAGPjhfmdl" + }, + { + "name": "req.body.boolean", + "value": "eq false", + "enabled": true, + "uid": "eXPS2R19qWsPGm6usEogu" + }, + { + "name": "req.body.number_1", + "value": "eq 1", + "enabled": true, + "uid": "WCKmMIqsFPwocy6LZmCcc" + }, + { + "name": "req.body.undefined", + "value": "eq undefined", + "enabled": true, + "uid": "7fJRYC8ELm68Uc5CaB7B8" + }, + { + "name": "req.body.string", + "value": "eq bruno", + "enabled": true, + "uid": "fXTl58gxhAUrUM8SZFxLW" + }, + { + "name": "req.body.null", + "value": "eq null", + "enabled": true, + "uid": "yUhXaWTPJaUYU4zerBABN" + }, + { + "name": "req.body.number_2", + "value": "eq 0", + "enabled": true, + "uid": "WWCCm6i8GzyNBH6xiQGAP" + }, + { + "name": "req.body.number_3", + "value": "eq -1", + "enabled": true, + "uid": "G73JIdrxSUDpc33EfAZGW" + }, + { + "name": "req.body.number_2", + "value": "isNumber", + "enabled": true, + "uid": "Dp5pdDeMEulfPB3ZDdjl4" + }, + { + "name": "req.body.number_3", + "value": "isNumber", + "enabled": true, + "uid": "fWghLNcBbslVF2OJMSS6x" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "data types", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"boolean\": false,\n \"number\": 1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const reqBody = req.getBody();\n\nbru.setVar(\"dataTypeVarTest\", {\n ...reqBody,\n \"undefined\": undefined\n});" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"data types check via bru var\", function() {\n let v = bru.getVar(\"dataTypeVarTest\");\n v = {\n ...v,\n \"undefined\": undefined\n };\n expect(v).to.eql({\n \"boolean\": false,\n \"number\": 1,\n \"string\": \"bruno\",\n \"array\": [1, 2, 3, 4, 5],\n \"object\": {\n \"hello\": \"bruno\"\n },\n \"null\": null,\n \"undefined\": undefined\n })\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "setTimeout", + "seq": 1, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"test-js-set-timeout\", \"\");\nawait new Promise((resolve, reject) => {\n setTimeout(() => {\n bru.setVar(\"test-js-set-timeout\", \"bruno\");\n resolve();\n }, 1000);\n});\n\nconst v = bru.getVar(\"test-js-set-timeout\");\nbru.setVar(\"test-js-set-timeout\", v + \"-is-awesome\");\n" + }, + "vars": {}, + "assertions": [], + "tests": "test(\"setTimeout()\", function() {\n const v = bru.getVar(\"test-js-set-timeout\")\n expect(v).to.eql(\"bruno-is-awesome\");\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "local modules", + "root": { + "meta": { + "name": "local modules" + } + }, + "items": [ + { + "type": "http", + "name": "invalid and valid module imports", + "seq": 3, + "request": { + "url": "{{host}}/ping", + "method": "GET", + "headers": [], + "params": [], + "body": { + "mode": "none", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "try {\n bru.setVar('invalid_module_error_thrown', false);\n // should throw an error\n const invalid = require(\"./lib/invalid\");\n}\ncatch(error) {\n bru.setVar('invalid_module_error_thrown', true);\n}\n\n\ntry {\n bru.setVar('valid_module_no_error', true);\n // should not throw an error\n const math = require(\"./lib/math\");\n}\ncatch(error) {\n bru.setVar('valid_module_no_error', false);\n}" + }, + "vars": {}, + "assertions": [ + { + "name": "invalid_module_error_thrown", + "value": "eq true", + "enabled": true, + "uid": "wdZ0MsGXmW7tRiX4VtQaT" + }, + { + "name": "valid_module_no_error", + "value": "eq true", + "enabled": true, + "uid": "A7hwDplpc0qDp5Bk46AMJ" + } + ], + "tests": "", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "sum -without js extn-", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"a\": 1,\n \"b\": 2\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const math = require(\"./lib/math\");\nconsole.log(math, 'math');\n\nconst body = req.getBody();\nbody.sum = math.sum(body.a, body.b);\nbody.areaOfCircle = math.areaOfCircle(2);\n\nreq.setBody(body);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "R6TRM5HoxKGuC5dHFvYdH" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3,\n \"areaOfCircle\": 12.56\n });\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "sum", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"a\": 1,\n \"b\": 2\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const math = require(\"./lib/math.js\"); \nconst body = req.getBody();\nbody.sum = math.sum(body.a, body.b);\n\nreq.setBody(body);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "UXDIZRejDajw3j0oYHhij" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});\n\ntest(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});\n\ntest(\"should return json\", function() {\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"a\": 1,\n \"b\": 2,\n \"sum\": 3\n });\n});", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + }, + { + "type": "folder", + "name": "npm modules", + "root": { + "meta": { + "name": "npm modules" + } + }, + "items": [ + { + "type": "http", + "name": "fakerjs", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"bruno\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "const { faker } = require('@faker-js/faker');\nconst uuid = faker.string.uuid();\n\nconst data = req.getBody();\ndata.uuid = uuid;\n\nreq.setBody(data);" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "ZVuM9BByoo5XFCtUYTwfP" + } + ], + "tests": "test(\"should return json\", function() {\n const data = res.getBody();\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n const isUUID = (inputString) => {\n return uuidRegex.test(inputString);\n };\n \n expect(data.hello).to.equal(\"bruno\");\n expect(isUUID(data.uuid)).to.be.true;\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ] + }, + { + "type": "folder", + "name": "string interpolation", + "root": { + "request": { + "vars": { + "req": [ + { + "name": "folder_pre_var", + "value": "folder_pre_var_value", + "enabled": true, + "local": false, + "uid": "OHd64NVOj1HQV2PLqRzy8" + }, + { + "name": "folder_pre_var_2", + "value": "{{env.var1}}", + "enabled": true, + "local": false, + "uid": "J12VEAvPGi3R0wBKXy2jK" + } + ] + } + }, + "meta": { + "name": "string interpolation" + } + }, + "items": [ + { + "type": "http", + "name": "env vars", + "seq": 2, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"envVar1\": \"{{env.var1}}\",\n \"envVar2\": \"{{env-var2}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "mxXvAcLVxfpRpGqh76ugy" + } + ], + "tests": "test(\"should return json\", function() {\n expect(res.getBody()).to.eql({\n \"envVar1\": \"envVar1\",\n \"envVar2\": \"envVar2\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "missing values", + "seq": 1, + "request": { + "url": "{{host}}/api/echo/json?foo={{undefinedVar}}", + "method": "POST", + "headers": [], + "params": [ + { + "name": "foo", + "value": "{{undefinedVar}}", + "type": "query", + "enabled": true + } + ], + "body": { + "mode": "json", + "json": "{\n \"hello\": \"{{undefinedVar2}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "nzPl7aN2MK9uc3SKEepG0" + } + ], + "tests": "test(\"should return json\", function() {\n const url = req.getUrl();\n const query = url.split(\"?\")[1];\n expect(query).to.equal(\"foo={{undefinedVar}}\");\n\n const data = res.getBody();\n expect(res.getBody()).to.eql({\n \"hello\": \"{{undefinedVar2}}\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "process env vars", + "seq": 4, + "request": { + "url": "{{host}}/api/echo/json", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "json", + "json": "{\n \"bark\": \"{{bark}}\",\n \"bark2\": \"{{process.env.PROC_ENV_VAR}}\"\n}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": {}, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "T1m34pWveQfE4Aao7Xqlt" + } + ], + "tests": "test(\"should return json\", function() {\n expect(res.getBody()).to.eql({\n \"bark\": \"woof\",\n \"bark2\": \"woof\"\n });\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + }, + { + "type": "http", + "name": "runtime vars", + "seq": 3, + "request": { + "url": "{{host}}/api/echo/text", + "method": "POST", + "headers": [], + "params": [], + "body": { + "mode": "text", + "json": "{\n \"envVar1\": \"{{env.var1}}\",\n \"envVar2\": \"{{env-var2}}\"\n}", + "text": "Hi, I am {{rUser.full_name}},\nI am {{rUser.age}} years old.\nMy favorite food is {{rUser.fav-food[0]}} and {{rUser.fav-food[1]}}.\nI like attention: {{rUser.want.attention}}", + "formUrlEncoded": [], + "multipartForm": [], + "file": [] + }, + "script": { + "req": "bru.setVar(\"rUser\", {\n full_name: 'Bruno',\n age: 4,\n 'fav-food': ['egg', 'meat'],\n 'want.attention': true\n});" + }, + "vars": {}, + "assertions": [ + { + "name": "res.status", + "value": "eq 200", + "enabled": true, + "uid": "xe4IAIu4EXYOYiXmKU374" + } + ], + "tests": "test(\"should return json\", function() {\n const expectedResponse = `Hi, I am Bruno,\nI am 4 years old.\nMy favorite food is egg and meat.\nI like attention: true`;\n expect(res.getBody()).to.equal(expectedResponse);\n});\n", + "docs": "", + "auth": { + "mode": "none" + } + } + } + ] + } + ], + "activeEnvironmentUid": "s4jJkWbb9017JXdVqOxLR", + "environments": [ + { + "variables": [ + { + "name": "host", + "value": "http://localhost:8080", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bearer_auth_token", + "value": "your_secret_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "basic_auth_password", + "value": "della", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "client_id", + "value": "client_id_1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "client_secret", + "value": "client_secret_1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "auth_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "callback_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/callback", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "access_token_url", + "value": "http://localhost:8080/api/auth/oauth2/authorization_code/token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "passwordCredentials_username", + "value": "foo", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "passwordCredentials_password", + "value": "bar", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_authorize_url", + "value": "https://github.com/login/oauth/authorize", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_access_token_url", + "value": "https://github.com/login/oauth/access_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_auth_url", + "value": "https://accounts.google.com/o/oauth2/auth", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_access_token_url", + "value": "https://accounts.google.com/o/oauth2/token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "google_scope", + "value": "https://www.googleapis.com/auth/userinfo.email", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "github_client_secret", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_client_id", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "google_client_id", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "google_client_secret", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_authorization_code", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "passwordCredentials_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "client_credentials_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "authorization_code_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + }, + { + "name": "github_access_token", + "value": "", + "enabled": true, + "secret": true, + "type": "text" + } + ], + "name": "Local" + }, + { + "variables": [ + { + "name": "host", + "value": "https://testbench-sanity.usebruno.com", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bearer_auth_token", + "value": "your_secret_token", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "basic_auth_password", + "value": "della", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "env.var1", + "value": "envVar1", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "env-var2", + "value": "envVar2", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "bark", + "value": "{{process.env.PROC_ENV_VAR}}", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "foo", + "value": "bar", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "testSetEnvVar", + "value": "bruno-29653", + "enabled": true, + "secret": false, + "type": "text" + }, + { + "name": "echo-host", + "value": "https://echo.usebruno.com", + "enabled": true, + "secret": false, + "type": "text" + } + ], + "name": "Prod" + } + ], + "root": { + "request": { + "auth": { + "mode": "bearer", + "bearer": { + "token": "{{bearer_auth_token}}" + } + }, + "headers": [ + { + "name": "check", + "value": "again", + "enabled": true, + "uid": "wbTRFykhPHZwnzVUTd1gr" + }, + { + "name": "token", + "value": "{{collection_pre_var_token}}", + "enabled": true, + "uid": "YGZ16VXf9NusINngKeXqn" + } + ], + "vars": { + "req": [ + { + "name": "collection_pre_var", + "value": "collection_pre_var_value", + "enabled": true, + "local": false, + "uid": "HI7DgTPA1gBLB6lIl1t3O" + }, + { + "name": "collection_pre_var_token", + "value": "{{request_pre_var_token}}", + "enabled": true, + "local": false, + "uid": "FoDj77i1KoZ6Koq9oavPy" + } + ] + } + }, + "docs": "# bruno-testbench 🐶\n\nThis is a test collection that I am using to test various functionalities around bruno" + }, + "brunoConfig": { + "version": "1", + "name": "bruno-testbench", + "type": "collection", + "proxy": { + "enabled": false, + "protocol": "http", + "hostname": "{{proxyHostname}}", + "port": 4000, + "auth": { + "enabled": false, + "username": "anoop" + }, + "bypassProxy": "" + }, + "scripts": { + "moduleWhitelist": [ + "crypto", + "buffer", + "form-data" + ], + "filesystemAccess": { + "allow": true + } + }, + "clientCertificates": { + "enabled": true, + "certs": [] + }, + "presets": { + "requestType": "http", + "requestUrl": "http://localhost:6000" + }, + "ignore": [ + "node_modules", + ".git" + ], + "size": 0.026633262634277344, + "filesCount": 96 + } +} \ No newline at end of file diff --git a/e2e-tests/import-tests/test-data/empty-file.json b/e2e-tests/import-tests/test-data/empty-file.json new file mode 100644 index 000000000..e69de29bb diff --git a/e2e-tests/import-tests/test-data/insomnia-malformed.json b/e2e-tests/import-tests/test-data/insomnia-malformed.json new file mode 100644 index 000000000..8a6bcc0d8 --- /dev/null +++ b/e2e-tests/import-tests/test-data/insomnia-malformed.json @@ -0,0 +1,19 @@ +{ + "_type": "export", + "__export_format": 4, + "resources": [ + { + "_id": "req_123", + "parentId": "wrk_456", + "url": "https://api.example.com/users", + "name": "Get Users", + "method": "GET", + "_type": "request" + }, + { + "_id": "wrk_456", + "name": "Test Collection", + "_type": "workspace" + // Missing comma and closing bracket - malformed JSON + } + ] diff --git a/e2e-tests/import-tests/test-data/insomnia-v4.json b/e2e-tests/import-tests/test-data/insomnia-v4.json new file mode 100644 index 000000000..486e295f1 --- /dev/null +++ b/e2e-tests/import-tests/test-data/insomnia-v4.json @@ -0,0 +1,113 @@ +{ + "_type": "export", + "__export_format": 4, + "__export_date": "2025-01-01T12:00:00.000Z", + "__export_source": "insomnia.desktop.app:v10.3.1", + "resources": [ + { + "_id": "req_fdedb34f7d5541d0aa7a917ce37ec067", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1689952276171, + "created": 1689951240510, + "url": "{{baseUrl}}/api/users", + "name": "Get Users", + "description": "Fetch all users from the API", + "method": "GET", + "body": {}, + "parameters": [], + "headers": [ + { + "name": "Accept", + "value": "application/json" + } + ], + "authentication": {}, + "metaSortKey": -1689951414329, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "req_c920d219404144e8bc6b6bd36f442974", + "parentId": "fld_ab2a1533f2be48c194883bf07d693292", + "modified": 1689952281595, + "created": 1689951281897, + "url": "{{baseUrl}}/api/auth/login", + "name": "Login User", + "description": "User authentication endpoint", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n \"username\": \"admin\",\n \"password\": \"password123\"\n}" + }, + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + } + ], + "authentication": {}, + "metaSortKey": -1689951404530.5625, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "fld_ab2a1533f2be48c194883bf07d693292", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1743683080329, + "created": 1743683080329, + "name": "Authentication", + "description": "Authentication related endpoints", + "environment": {}, + "environmentPropertyOrder": null, + "metaSortKey": -1743683080329, + "_type": "request_group" + }, + { + "_id": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "parentId": null, + "modified": 1743678539806, + "created": 1743678539806, + "name": "Test API Collection v4", + "description": "Test collection for Insomnia v4 format", + "scope": "collection", + "_type": "workspace" + }, + { + "_id": "env_93781eb62f074459bb67692112b76da0", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1743681240772, + "created": 1689951235312, + "name": "Base Environment", + "data": { + "baseUrl": "https://api.example.com" + }, + "dataPropertyOrder": null, + "color": null, + "isPrivate": false, + "metaSortKey": 1689951235312, + "_type": "environment" + }, + { + "_id": "jar_09963a0322c24b698ecd2f866ae9a6ab", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1689951235313, + "created": 1689951235313, + "name": "Default Jar", + "cookies": [], + "_type": "cookie_jar" + } + ] +} \ No newline at end of file diff --git a/e2e-tests/import-tests/test-data/insomnia-v5-invalid-missing-collection.yaml b/e2e-tests/import-tests/test-data/insomnia-v5-invalid-missing-collection.yaml new file mode 100644 index 000000000..5214df1b2 --- /dev/null +++ b/e2e-tests/import-tests/test-data/insomnia-v5-invalid-missing-collection.yaml @@ -0,0 +1,23 @@ +type: collection.insomnia.rest/5.0 +name: Invalid v5 Collection +meta: + id: wrk_7faf891d273e4b7ea82bdbaa641ee17a + created: 1743683067888 + modified: 1743683067888 +# Missing collection array - this should cause import to fail +cookieJar: + name: Default Jar + meta: + id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067908 + modified: 1743683833282 + cookies: [] +environments: + name: Base Environment + meta: + id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067895 + modified: 1743683476058 + isPrivate: false + data: + base_url: https://api.example.com diff --git a/e2e-tests/import-tests/test-data/insomnia-v5.yaml b/e2e-tests/import-tests/test-data/insomnia-v5.yaml new file mode 100644 index 000000000..09a2da4aa --- /dev/null +++ b/e2e-tests/import-tests/test-data/insomnia-v5.yaml @@ -0,0 +1,129 @@ +type: collection.insomnia.rest/5.0 +name: Test API Collection v5 +meta: + id: wrk_7faf891d273e4b7ea82bdbaa641ee17a + created: 1743683067888 + modified: 1743683067888 +collection: + - name: API Tests + meta: + id: fld_ab2a1533f2be48c194883bf07d693292 + created: 1743683080329 + modified: 1743683080329 + sortKey: -1743683080329 + children: + - name: Authentication + meta: + id: fld_e7bcaad160254179a9c86e39a58c6893 + created: 1743683088154 + modified: 1743683090190 + sortKey: -1743683090140 + children: + - url: "{{ _.base_url }}/api/auth/login" + name: Login User + meta: + id: req_d48ab8553cff4eb486b816e064cf99a4 + created: 1743683199141 + modified: 1743683342872 + isPrivate: false + sortKey: -1743683199141 + method: POST + body: + mimeType: application/json + text: |- + { + "username": "testuser", + "password": "testpass123" + } + headers: + - name: Content-Type + value: application/json + - name: User-Agent + value: insomnia/10.3.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - url: "{{ _.base_url }}/api/users" + name: Get Users + meta: + id: req_0393b8ff4ee1454daddacdda33fd33ea + created: 1743683426423 + modified: 1743683632735 + isPrivate: false + description: Retrieve all users from the system + sortKey: -1743683429031 + method: GET + headers: + - name: Authorization + value: Bearer {{ _.auth_token }} + - name: User-Agent + value: insomnia/10.3.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - name: Data Management + meta: + id: fld_4736de73a1634b16960fa9e90d78f868 + created: 1743683403969 + modified: 1743683403969 + sortKey: -1743683403969 + children: + - url: "{{ _.base_url }}/api/posts" + name: Create Post + meta: + id: req_10db7ec1332d4444aa551e70a1bfae33 + created: 1743683795359 + modified: 1743683832200 + isPrivate: false + sortKey: -1743683429131 + method: POST + body: + mimeType: application/json + text: |- + { + "title": "Test Post", + "content": "This is a test post content", + "author": "Test Author" + } + headers: + - name: Content-Type + value: application/json + - name: Authorization + value: Bearer {{ _.auth_token }} + - name: User-Agent + value: insomnia/10.3.1 + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true +cookieJar: + name: Default Jar + meta: + id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067908 + modified: 1743683833282 + cookies: [] +environments: + name: Base Environment + meta: + id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067895 + modified: 1743683476058 + isPrivate: false + data: + base_url: https://api.example.com + auth_token: your_auth_token_here diff --git a/e2e-tests/import-tests/test-data/invalid-json.json b/e2e-tests/import-tests/test-data/invalid-json.json new file mode 100644 index 000000000..f3ca33078 --- /dev/null +++ b/e2e-tests/import-tests/test-data/invalid-json.json @@ -0,0 +1 @@ +{ "invalid": json syntax } diff --git a/e2e-tests/import-tests/test-data/invalid.txt b/e2e-tests/import-tests/test-data/invalid.txt new file mode 100644 index 000000000..2defe8ade --- /dev/null +++ b/e2e-tests/import-tests/test-data/invalid.txt @@ -0,0 +1 @@ +This is not a valid collection file diff --git a/e2e-tests/import-tests/test-data/openapi-comprehensive.yaml b/e2e-tests/import-tests/test-data/openapi-comprehensive.yaml new file mode 100644 index 000000000..d55d149a9 --- /dev/null +++ b/e2e-tests/import-tests/test-data/openapi-comprehensive.yaml @@ -0,0 +1,374 @@ +openapi: 3.0.3 +info: + title: Comprehensive API Test Collection + description: A comprehensive API for testing OpenAPI v3 imports with various features + version: 2.1.0 + contact: + name: API Support + email: support@example.com + license: + name: MIT + url: https://opensource.org/licenses/MIT +servers: + - url: https://api.example.com/v1 + description: Production server + - url: https://staging-api.example.com/v1 + description: Staging server + - url: http://localhost:3000/v1 + description: Development server +security: + - bearerAuth: [] + - apiKey: [] +paths: + /users: + get: + summary: Get all users + description: Retrieve a paginated list of all users + tags: + - Users + parameters: + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: limit + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: filter + in: query + description: Filter users by name or email + schema: + type: string + responses: + '200': + description: List of users retrieved successfully + content: + application/json: + schema: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + $ref: '#/components/schemas/Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + post: + summary: Create a new user + description: Create a new user account + tags: + - Users + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: User already exists + /users/{userId}: + get: + summary: Get user by ID + description: Retrieve a specific user by their ID + tags: + - Users + parameters: + - name: userId + in: path + required: true + description: The user ID + schema: + type: string + format: uuid + responses: + '200': + description: User retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + $ref: '#/components/responses/NotFound' + put: + summary: Update user + description: Update an existing user + tags: + - Users + parameters: + - name: userId + in: path + required: true + description: The user ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + $ref: '#/components/responses/NotFound' + delete: + summary: Delete user + description: Delete a user account + tags: + - Users + parameters: + - name: userId + in: path + required: true + description: The user ID + schema: + type: string + format: uuid + responses: + '204': + description: User deleted successfully + '404': + $ref: '#/components/responses/NotFound' + /auth/login: + post: + summary: User login + description: Authenticate user and get access token + tags: + - Authentication + security: [] # No security required for login + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - password + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + responses: + '200': + description: Login successful + content: + application/json: + schema: + type: object + properties: + token: + type: string + expiresIn: + type: integer + user: + $ref: '#/components/schemas/User' + '401': + description: Invalid credentials + /posts: + get: + summary: Get all posts + description: Retrieve all blog posts + tags: + - Posts + parameters: + - name: author + in: query + description: Filter by author ID + schema: + type: string + - name: category + in: query + description: Filter by category + schema: + type: string + responses: + '200': + description: List of posts + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Post' + post: + summary: Create a new post + description: Create a new blog post + tags: + - Posts + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePostRequest' + responses: + '201': + description: Post created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Post' +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + apiKey: + type: apiKey + in: header + name: X-API-Key + schemas: + User: + type: object + properties: + id: + type: string + format: uuid + email: + type: string + format: email + name: + type: string + avatar: + type: string + format: uri + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + CreateUserRequest: + type: object + required: + - email + - name + - password + properties: + email: + type: string + format: email + name: + type: string + minLength: 2 + password: + type: string + minLength: 8 + avatar: + type: string + format: uri + UpdateUserRequest: + type: object + properties: + name: + type: string + minLength: 2 + avatar: + type: string + format: uri + Post: + type: object + properties: + id: + type: string + format: uuid + title: + type: string + content: + type: string + author: + $ref: '#/components/schemas/User' + category: + type: string + publishedAt: + type: string + format: date-time + createdAt: + type: string + format: date-time + CreatePostRequest: + type: object + required: + - title + - content + - category + properties: + title: + type: string + minLength: 5 + content: + type: string + minLength: 10 + category: + type: string + Pagination: + type: object + properties: + page: + type: integer + limit: + type: integer + total: + type: integer + totalPages: + type: integer + Error: + type: object + properties: + error: + type: string + message: + type: string + code: + type: integer + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/e2e-tests/import-tests/test-data/openapi-invalid-version.yaml b/e2e-tests/import-tests/test-data/openapi-invalid-version.yaml new file mode 100644 index 000000000..a8a496d98 --- /dev/null +++ b/e2e-tests/import-tests/test-data/openapi-invalid-version.yaml @@ -0,0 +1,16 @@ +openapi: 2.0 # Invalid version - only v3 is supported +info: + title: Invalid OpenAPI Version + description: This uses OpenAPI v2 which is not supported + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: + /users: + get: + summary: Get users + responses: + '200': + description: List of users diff --git a/e2e-tests/import-tests/test-data/openapi-malformed.yaml b/e2e-tests/import-tests/test-data/openapi-malformed.yaml new file mode 100644 index 000000000..432316f27 --- /dev/null +++ b/e2e-tests/import-tests/test-data/openapi-malformed.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: Malformed OpenAPI + version: 1.0.0 +paths: + /test: + get: + summary: Test endpoint + responses: + '200': + description: Success + # Missing closing quotes and malformed YAML + '400': + description: Bad request + malformed: yaml here + extra: indentation diff --git a/e2e-tests/import-tests/test-data/openapi-missing-info.yaml b/e2e-tests/import-tests/test-data/openapi-missing-info.yaml new file mode 100644 index 000000000..971860b4e --- /dev/null +++ b/e2e-tests/import-tests/test-data/openapi-missing-info.yaml @@ -0,0 +1,9 @@ +openapi: 3.0.0 +# Missing required info section +paths: + /test: + get: + summary: Test endpoint + responses: + '200': + description: Success diff --git a/e2e-tests/import-tests/test-data/openapi-simple.json b/e2e-tests/import-tests/test-data/openapi-simple.json new file mode 100644 index 000000000..9aaa873e8 --- /dev/null +++ b/e2e-tests/import-tests/test-data/openapi-simple.json @@ -0,0 +1,116 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Simple Test API", + "description": "A simple API for basic OpenAPI testing", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://httpbin.org", + "description": "HTTPBin test server" + } + ], + "paths": { + "/get": { + "get": { + "summary": "HTTP GET test", + "description": "Test HTTP GET request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "args": { + "type": "object" + }, + "headers": { + "type": "object" + }, + "origin": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/post": { + "post": { + "summary": "HTTP POST test", + "description": "Test HTTP POST request", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "json": { + "type": "object" + }, + "origin": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/status/{code}": { + "get": { + "summary": "Return status code", + "description": "Return a specific HTTP status code", + "parameters": [ + { + "name": "code", + "in": "path", + "required": true, + "description": "HTTP status code to return", + "schema": { + "type": "integer", + "minimum": 100, + "maximum": 599 + } + } + ], + "responses": { + "default": { + "description": "Returns the specified status code" + } + } + } + } + } +} diff --git a/e2e-tests/import-tests/test-data/postman-invalid-missing-info.json b/e2e-tests/import-tests/test-data/postman-invalid-missing-info.json new file mode 100644 index 000000000..dd9a726af --- /dev/null +++ b/e2e-tests/import-tests/test-data/postman-invalid-missing-info.json @@ -0,0 +1,11 @@ +{ + "item": [ + { + "name": "Request without info", + "request": { + "method": "GET", + "url": "https://example.com" + } + } + ] +} diff --git a/e2e-tests/import-tests/test-data/postman-invalid-schema.json b/e2e-tests/import-tests/test-data/postman-invalid-schema.json new file mode 100644 index 000000000..573976819 --- /dev/null +++ b/e2e-tests/import-tests/test-data/postman-invalid-schema.json @@ -0,0 +1,7 @@ +{ + "info": { + "name": "Invalid Schema Collection", + "schema": "https://schema.getpostman.com/json/collection/v999.0.0/collection.json" + }, + "item": [] +} diff --git a/e2e-tests/import-tests/test-data/postman-malformed.json b/e2e-tests/import-tests/test-data/postman-malformed.json new file mode 100644 index 000000000..844fbf453 --- /dev/null +++ b/e2e-tests/import-tests/test-data/postman-malformed.json @@ -0,0 +1,6 @@ +{ + "info": { + "name": "Malformed Collection" + }, + "item": "this should be an array, not a string" +} diff --git a/e2e-tests/import-tests/test-data/postman-v20.json b/e2e-tests/import-tests/test-data/postman-v20.json new file mode 100644 index 000000000..02f38b945 --- /dev/null +++ b/e2e-tests/import-tests/test-data/postman-v20.json @@ -0,0 +1,17 @@ +{ + "info": { + "name": "Postman v2.0 Collection", + "description": "Test collection using Postman Collection Format v2.0", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "Get Posts", + "request": { + "method": "GET", + "header": [], + "url": "https://jsonplaceholder.typicode.com/posts" + } + } + ] +} diff --git a/e2e-tests/import-tests/test-data/postman-v21.json b/e2e-tests/import-tests/test-data/postman-v21.json new file mode 100644 index 000000000..e922335a7 --- /dev/null +++ b/e2e-tests/import-tests/test-data/postman-v21.json @@ -0,0 +1,64 @@ +{ + "info": { + "name": "Postman v2.1 Collection", + "description": "Test collection using Postman Collection Format v2.1", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_postman_id": "12345678-1234-1234-1234-123456789012" + }, + "item": [ + { + "name": "Get Users", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"], + "query": [ + { + "key": "page", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Create User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"John Doe\",\n \"email\": \"john@example.com\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + }, + "response": [] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "https://api.example.com" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 58dfa6086..55f483f47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1605,7 +1605,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1623,7 +1623,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1640,7 +1640,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, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1658,7 +1658,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, + "devOptional": true, "license": "MIT" }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -1729,7 +1729,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1804,7 +1804,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -1847,7 +1847,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1864,7 +1864,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1880,7 +1880,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1896,7 +1896,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1914,7 +1914,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1949,7 +1949,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2048,7 +2048,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2064,7 +2064,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2246,7 +2246,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -2263,7 +2263,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2279,7 +2279,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2297,7 +2297,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -2315,7 +2315,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2331,7 +2331,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2363,7 +2363,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -2380,7 +2380,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2401,7 +2401,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -2411,7 +2411,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2428,7 +2428,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2444,7 +2444,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2461,7 +2461,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2477,7 +2477,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2494,7 +2494,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2510,7 +2510,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2526,7 +2526,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2558,7 +2558,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2575,7 +2575,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2593,7 +2593,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2609,7 +2609,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2625,7 +2625,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2641,7 +2641,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2657,7 +2657,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2690,7 +2690,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2709,7 +2709,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2726,7 +2726,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2743,7 +2743,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2774,7 +2774,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2790,7 +2790,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2808,7 +2808,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2825,7 +2825,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2857,7 +2857,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2889,7 +2889,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2907,7 +2907,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2992,7 +2992,7 @@ "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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3009,7 +3009,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3026,7 +3026,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3042,7 +3042,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3058,7 +3058,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3075,7 +3075,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3091,7 +3091,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3107,7 +3107,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3142,7 +3142,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3158,7 +3158,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3175,7 +3175,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3192,7 +3192,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3209,7 +3209,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.0", @@ -3310,7 +3310,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -7900,6 +7900,61 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?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==", + "license": "MIT", + "peer": true, + "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/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==", + "license": "MIT", + "peer": true + }, "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", @@ -7928,6 +7983,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tippyjs/react": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", @@ -7965,7 +8033,6 @@ "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": { @@ -8220,7 +8287,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -8252,7 +8318,6 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -8263,7 +8328,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -9314,30 +9378,12 @@ } }, "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, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "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" + "dequal": "^2.0.3" } }, "node_modules/array-flatten": { @@ -9702,7 +9748,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", @@ -9717,7 +9763,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", @@ -9731,7 +9777,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, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" @@ -11481,7 +11527,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, + "devOptional": true, "license": "MIT", "dependencies": { "browserslist": "^4.24.2" @@ -12258,46 +12304,6 @@ "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", @@ -12571,7 +12577,6 @@ "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": { @@ -13094,34 +13099,6 @@ "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", @@ -13591,7 +13568,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -14808,16 +14785,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/generic-names": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", @@ -14914,20 +14881,6 @@ "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", @@ -15290,19 +15243,6 @@ "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", @@ -16100,21 +16040,6 @@ "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", @@ -16175,46 +16100,12 @@ "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", @@ -16227,23 +16118,6 @@ "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", @@ -16274,7 +16148,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, + "devOptional": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -16286,23 +16160,6 @@ "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", @@ -16407,19 +16264,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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", @@ -16453,23 +16297,6 @@ "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", @@ -16524,25 +16351,6 @@ "@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", @@ -16552,35 +16360,6 @@ "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", @@ -16594,41 +16373,6 @@ "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", @@ -16663,36 +16407,6 @@ "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", @@ -18685,7 +18399,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, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.flow": { @@ -18823,7 +18537,6 @@ "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" @@ -20252,7 +19965,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, + "devOptional": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -22592,14 +22305,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, + "devOptional": 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, + "devOptional": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -22618,38 +22331,17 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, + "devOptional": 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, + "devOptional": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -22667,14 +22359,14 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, + "devOptional": 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, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -22687,7 +22379,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, + "devOptional": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -22843,7 +22535,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, + "devOptional": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -23331,24 +23023,6 @@ "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", @@ -24031,22 +23705,6 @@ "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", @@ -24551,20 +24209,6 @@ "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-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -25103,7 +24747,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -25970,7 +25614,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -26031,7 +25675,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -26041,7 +25685,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, + "devOptional": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -26055,7 +25699,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -26065,7 +25709,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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -26621,45 +26265,6 @@ "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", @@ -26994,6 +26599,7 @@ "@prantlf/jsonlint": "^16.0.0", "@reduxjs/toolkit": "^1.8.0", "@tabler/icons": "^1.46.0", + "@testing-library/user-event": "^14.6.1", "@tippyjs/react": "^4.2.6", "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", @@ -28448,16 +28054,6 @@ } } }, - "packages/bruno-app/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "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", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index aac3dbab7..6b5f8f798 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -16,6 +16,7 @@ "@prantlf/jsonlint": "^16.0.0", "@reduxjs/toolkit": "^1.8.0", "@tabler/icons": "^1.46.0", + "@testing-library/user-event": "^14.6.1", "@tippyjs/react": "^4.2.6", "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", diff --git a/packages/bruno-app/src/components/GlobalSearchModal/StyledWrapper.js b/packages/bruno-app/src/components/GlobalSearchModal/StyledWrapper.js new file mode 100644 index 000000000..0900e1502 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalSearchModal/StyledWrapper.js @@ -0,0 +1,361 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + /* Screen reader only content */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + .command-k-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: flex-start; + justify-content: center; + overflow-y: auto; + z-index: 20; + background-color: transparent; + &:before { + content: ''; + height: 100%; + width: 100%; + left: 0; + opacity: ${(props) => props.theme.modal.backdrop.opacity}; + top: 0; + background: black; + position: fixed; + } + animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1); + } + .command-k-modal { + background: ${(props) => props.theme.modal.body.bg}; + border: 1px solid ${(props) => props.theme.modal.input.border}; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + width: 90%; + max-width: 600px; + max-height: 70vh; + display: flex; + flex-direction: column; + overflow: hidden; + margin: 80px auto; + animation: fade-and-slide-in-from-top 0.3s forwards cubic-bezier(0.19, 1, 0.22, 1); + will-change: opacity, transform; + } + .command-k-header { + padding: 12px; + border-bottom: 1px solid ${(props) => props.theme.modal.input.border}; + background: ${(props) => props.theme.modal.title.bg}; + } + .search-input-container { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: 8px 12px; + border: 1px solid ${(props) => props.theme.modal.input.border}; + border-radius: 6px; + background: ${(props) => props.theme.modal.input.bg}; + transition: all 0.2s ease; + &:focus-within { + border-color: ${(props) => props.theme.colors.text.muted}; + box-shadow: 0 0 0 1px ${(props) => props.theme.colors.text.muted}40; + } + .search-icon { + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + margin-right: 8px; + flex-shrink: 0; + } + .clear-button { + background: transparent; + border: none; + padding: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + margin-left: 8px; + border-radius: 4px; + flex-shrink: 0; + &:hover { + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}; + } + } + } + .search-input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: ${(props) => props.theme.text}; + font-size: 13px; + width: 100%; + padding: 0; + &::placeholder { + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.7; + } + } + .command-k-results { + flex: 1; + overflow-y: auto; + max-height: 400px; + background: ${(props) => props.theme.modal.body.bg}; + scrollbar-width: thin; + padding: 4px; + scroll-behavior: smooth; + /* Webkit scrollbar styling */ + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}; + border-radius: 4px; + &:hover { + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}; + } + } + } + .result-item { + display: flex; + align-items: center; + padding: 8px 12px; + gap: 8px; + cursor: pointer; + border-left: 2px solid transparent; + &:hover { + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}; + } + &.selected { + background: ${(props) => `${props.theme.colors.text.yellow}15`}; + border-left: 2px solid ${(props) => props.theme.colors.text.yellow}; + } + } + .result-icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + } + .result-content { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + .result-info { + flex: 1; + min-width: 0; + margin-right: 8px; + } + .result-badges { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + .result-name { + font-size: 13px; + margin-bottom: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: ${(props) => props.theme.text}; + letter-spacing: 0.2px; + } + .result-path { + font-size: 12px; + color: ${(props) => props.theme.colors.text.muted}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0.1px; + } + .method-badge { + font-size: 11px; + font-weight: 500; + padding: 3px 8px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + min-width: 55px; + text-align: center; + &.get { + color: #2ecc71; + background: rgba(46, 204, 113, 0.1); + } + &.post { + color: #3498db; + background: rgba(52, 152, 219, 0.1); + } + &.put { + color: #e67e22; + background: rgba(230, 126, 34, 0.1); + } + &.delete { + color: #e74c3c; + background: rgba(231, 76, 60, 0.1); + } + &.patch { + color: #9b59b6; + background: rgba(155, 89, 182, 0.1); + } + &.head { + color: #2980b9; + background: rgba(41, 128, 185, 0.1); + } + &.options { + color: #f1c40f; + background: rgba(241, 196, 15, 0.1); + } + &.unary { + color: #27ae60; + background: rgba(39, 174, 96, 0.12); + font-weight: 600; + } + &.client-streaming { + color: #2980b9; + background: rgba(41, 128, 185, 0.12); + font-weight: 600; + } + &.server-streaming { + color: #f39c12; + background: rgba(243, 156, 18, 0.12); + font-weight: 600; + } + &.bidirectional-streaming, + &.bidi-streaming { + color: #8e44ad; + background: rgba(142, 68, 173, 0.12); + font-weight: 600; + } + } + .result-type { + font-size: 11px; + color: ${(props) => props.theme.colors.text.muted}; + padding: 2px 6px; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: 0.3px; + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)'}; + opacity: 0.8; + flex-shrink: 0; + } + .result-item[data-type="documentation"] { + .result-icon { + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + } + .result-path { + font-size: 12px; + color: ${(props) => props.theme.colors.text.muted}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0.1px; + opacity: 0.8; + } + &:hover:not(.selected) { + background: ${(props) => props.theme.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}; + } + } + .no-results, + .empty-state { + padding: 24px 16px; + text-align: center; + color: ${(props) => props.theme.colors.text.muted}; + font-size: 13px; + } + .command-k-footer { + padding: 8px 12px; + border-top: 1px solid ${(props) => props.theme.modal.input.border}; + background: ${(props) => props.theme.colors.surface}; + } + .keyboard-hints { + display: flex; + justify-content: center; + gap: 24px; + color: ${(props) => props.theme.colors.text.muted}; + font-size: 12px; + letter-spacing: 0.2px; + span { + display: flex; + align-items: center; + gap: 6px; + .hint-icon { + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + } + .hint-icon + .hint-icon { + margin-left: -8px; + } + .keycap { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 6px; + border: 1px solid ${(props) => props.theme.modal.input.border}; + border-radius: 4px; + background: ${(props) => + props.theme.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'}; + font-size: 11px; + font-weight: 500; + font-family: inherit; + line-height: 1; + color: ${(props) => props.theme.text}; + } + } + } + .highlight { + background: ${(props) => `${props.theme.colors.text.yellow}30`}; + border-radius: 2px; + padding: 0 2px; + margin: 0 -1px; + font-weight: 500; + } + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes fade-and-slide-in-from-top { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/GlobalSearchModal/constants/index.js b/packages/bruno-app/src/components/GlobalSearchModal/constants/index.js new file mode 100644 index 000000000..f7e174c52 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalSearchModal/constants/index.js @@ -0,0 +1,32 @@ +export const SEARCH_TYPES = { + DOCUMENTATION: 'documentation', + COLLECTION: 'collection', + FOLDER: 'folder', + REQUEST: 'request' +}; + +export const MATCH_TYPES = { + COLLECTION: 'collection', + FOLDER: 'folder', + REQUEST: 'request', + URL: 'url', + PATH: 'path', + DOCUMENTATION: 'documentation' +}; + +export const SEARCH_CONFIG = { + MAX_DEPTH: 20, + FOCUS_DELAY: 100, + SCROLL_BEHAVIOR: 'smooth', + SCROLL_BLOCK: 'nearest', + DEBOUNCE_DELAY: 300 +}; + +export const DOCUMENTATION_RESULT = { + type: SEARCH_TYPES.DOCUMENTATION, + item: { id: 'docs', name: 'Bruno Documentation' }, + name: 'Bruno Documentation', + path: '/', + description: 'Browse the official Bruno documentation', + matchType: MATCH_TYPES.DOCUMENTATION +}; diff --git a/packages/bruno-app/src/components/GlobalSearchModal/index.js b/packages/bruno-app/src/components/GlobalSearchModal/index.js new file mode 100644 index 000000000..5bfb09ff8 --- /dev/null +++ b/packages/bruno-app/src/components/GlobalSearchModal/index.js @@ -0,0 +1,515 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + IconSearch, + IconX, + IconFolder, + IconBox, + IconFileText, + IconBook +} from '@tabler/icons'; +import { flattenItems, isItemARequest, isItemAFolder, findParentItemInCollection } from 'utils/collections'; +import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; +import { hideHomePage } from 'providers/ReduxStore/slices/app'; +import { toggleCollectionItem, toggleCollection } from 'providers/ReduxStore/slices/collections'; +import { mountCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { getDefaultRequestPaneTab } from 'utils/collections'; +import { normalizeQuery, isValidQuery, highlightText, sortResults, getTypeLabel, getItemPath } from './utils/searchUtils'; +import { SEARCH_TYPES, MATCH_TYPES, SEARCH_CONFIG, DOCUMENTATION_RESULT } from './constants'; +import StyledWrapper from './StyledWrapper'; + +const GlobalSearchModal = ({ isOpen, onClose }) => { + const [query, setQuery] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const [results, setResults] = useState([]); + const inputRef = useRef(null); + const resultsRef = useRef(null); + const debounceTimeoutRef = useRef(null); + const dispatch = useDispatch(); + + const collections = useSelector((state) => state.collections.collections); + const tabs = useSelector((state) => state.tabs.tabs); + + const createCollectionResults = () => { + const collectionResults = collections.map(collection => ({ + type: SEARCH_TYPES.COLLECTION, + item: collection, + name: collection.name, + path: collection.name, + matchType: MATCH_TYPES.COLLECTION, + collectionUid: collection.uid + })); + + collectionResults.sort((a, b) => a.name.localeCompare(b.name)); + return [DOCUMENTATION_RESULT, ...collectionResults]; + }; + + const searchInCollections = (searchTerms, enablePathMatch) => { + const results = []; + + // Check for documentation match + const queryLower = searchTerms.join(' '); + if (['documentation', 'docs', 'bruno docs'].some(term => term.includes(queryLower))) { + results.push(DOCUMENTATION_RESULT); + } + + collections.forEach(collection => { + // Search collection name + if (searchTerms.every(term => collection.name.toLowerCase().includes(term))) { + results.push({ + type: SEARCH_TYPES.COLLECTION, + item: collection, + name: collection.name, + path: collection.name, + matchType: MATCH_TYPES.COLLECTION, + collectionUid: collection.uid + }); + } + + // Search collection items + const flattenedItems = flattenItems(collection.items); + flattenedItems.forEach(item => { + const itemPath = getItemPath(item, collection, findParentItemInCollection); + const itemPathLower = itemPath.toLowerCase(); + + if (isItemARequest(item)) { + const nameMatch = searchTerms.every(term => item.name.toLowerCase().includes(term)); + const urlMatch = searchTerms.every(term => (item.request?.url || '').toLowerCase().includes(term)); + const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term)); + + if (nameMatch || urlMatch || pathMatch) { + // Check if this is a gRPC request and get the method type + const isGrpcRequest = item.request?.type === 'grpc'; + + let method = item.request?.method || ''; + + if (isGrpcRequest) { + // For gRPC requests, use the methodType + const methodType = item.request?.methodType || 'UNARY'; + method = methodType.toLowerCase().replace(/[_]/g, '-'); + } + + results.push({ + type: SEARCH_TYPES.REQUEST, + item, + name: item.name, + path: itemPath, + matchType: nameMatch ? MATCH_TYPES.REQUEST : urlMatch ? MATCH_TYPES.URL : MATCH_TYPES.PATH, + method, + collectionUid: collection.uid + }); + } + } else if (isItemAFolder(item)) { + const nameMatch = searchTerms.every(term => item.name.toLowerCase().includes(term)); + const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term)); + + if (nameMatch || pathMatch) { + results.push({ + type: SEARCH_TYPES.FOLDER, + item, + name: item.name, + path: itemPath, + matchType: nameMatch ? MATCH_TYPES.FOLDER : MATCH_TYPES.PATH, + collectionUid: collection.uid + }); + } + } + }); + }); + + return results; + }; + + const performSearch = (searchQuery) => { + const normalizedQuery = normalizeQuery(searchQuery); + + if (!normalizedQuery) { + setResults(createCollectionResults()); + return; + } + + if (!isValidQuery(normalizedQuery)) { + setResults([]); + return; + } + + const searchTerms = normalizedQuery.toLowerCase().split(/[\s\/]+/).filter(Boolean); + if (!searchTerms.length) { + setResults([]); + return; + } + + const enablePathMatch = normalizedQuery.includes('/'); + const searchResults = searchInCollections(searchTerms, enablePathMatch); + const sortedResults = sortResults(searchResults); + + setResults(sortedResults); + setSelectedIndex(0); + }; + + const debouncedSearch = useCallback((searchQuery) => { + // Clear existing timeout + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + // Set new timeout + debounceTimeoutRef.current = setTimeout(() => { + performSearch(searchQuery); + }, SEARCH_CONFIG.DEBOUNCE_DELAY); + }, [collections]); // Depend on collections to recreate when they change + + const expandItemPath = (result) => { + const collection = collections.find(c => c.uid === result.collectionUid); + if (!collection) return; + + ensureCollectionIsMounted(collection); + + if (collection.collapsed) { + dispatch(toggleCollection(collection.uid)); + } + + let currentItem = result.type === SEARCH_TYPES.FOLDER + ? result.item + : findParentItemInCollection(collection, result.item.uid); + + while (currentItem?.type === 'folder') { + if (currentItem.collapsed) { + dispatch(toggleCollectionItem({ collectionUid: collection.uid, itemUid: currentItem.uid })); + } + currentItem = findParentItemInCollection(collection, currentItem.uid); + } + }; + + const ensureCollectionIsMounted = (collection) => { + if (!collection || collection.mountStatus === 'mounted') return; + dispatch(mountCollection({ + collectionUid: collection.uid, + collectionPathname: collection.pathname, + brunoConfig: collection.brunoConfig + })); + }; + + const handleKeyNavigation = (e) => { + const handlers = { + ArrowDown: () => { + e.preventDefault(); + setSelectedIndex(prev => prev < results.length - 1 ? prev + 1 : 0); + }, + ArrowUp: () => { + e.preventDefault(); + setSelectedIndex(prev => prev > 0 ? prev - 1 : results.length - 1); + }, + Enter: () => { + e.preventDefault(); + if (results[selectedIndex]) { + handleResultSelection(results[selectedIndex]); + } + }, + Escape: () => { + e.preventDefault(); + onClose(); + }, + PageDown: () => { + e.preventDefault(); + setSelectedIndex(prev => Math.min(prev + 5, results.length - 1)); + }, + PageUp: () => { + e.preventDefault(); + setSelectedIndex(prev => Math.max(prev - 5, 0)); + }, + Home: () => { + e.preventDefault(); + setSelectedIndex(0); + }, + End: () => { + e.preventDefault(); + setSelectedIndex(results.length - 1); + } + }; + + const handler = handlers[e.key]; + if (handler) handler(); + }; + + const handleResultSelection = (result) => { + const targetCollection = collections.find(c => c.uid === result.collectionUid); + ensureCollectionIsMounted(targetCollection); + + if (result.type === SEARCH_TYPES.DOCUMENTATION) { + window.open('https://docs.usebruno.com/', '_blank'); + onClose(); + return; + } + + expandItemPath(result); + + if (result.type === SEARCH_TYPES.REQUEST) { + dispatch(hideHomePage()); + + const existingTab = tabs.find(tab => tab.uid === result.item.uid); + + if (existingTab) { + dispatch(focusTab({ uid: result.item.uid })); + } else { + dispatch(addTab({ + uid: result.item.uid, + collectionUid: result.collectionUid, + requestPaneTab: getDefaultRequestPaneTab(result.item), + type: 'request', + })); + } + } else if (result.type === SEARCH_TYPES.FOLDER) { + dispatch(addTab({ + uid: result.item.uid, + collectionUid: result.collectionUid, + type: 'folder-settings', + })); + } else if (result.type === SEARCH_TYPES.COLLECTION) { + dispatch(addTab({ + uid: result.item.uid, + collectionUid: result.collectionUid, + type: 'collection-settings', + })); + } + + onClose(); + }; + + const handleQueryChange = (e) => { + const newQuery = e.target.value; + setQuery(newQuery); + + if (newQuery.trim()) { + debouncedSearch(newQuery); + } else { + // For empty queries, search immediately to show collections + performSearch(newQuery); + } + }; + + const clearSearch = () => { + // Clear any pending debounced search + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + setQuery(''); + setResults([]); + }; + + // Initialize modal when opened + useEffect(() => { + if (isOpen) { + const timeoutId = setTimeout(() => inputRef.current?.focus(), SEARCH_CONFIG.FOCUS_DELAY); + setQuery(''); + performSearch(''); + setSelectedIndex(0); + + return () => clearTimeout(timeoutId); + } else { + // Clear any pending debounced search when modal closes + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + } + }, [isOpen]); + + // Auto-scroll selected item into view + useEffect(() => { + if (resultsRef.current && results.length > 0) { + const selectedElement = resultsRef.current.children[selectedIndex]; + selectedElement?.scrollIntoView({ + behavior: SEARCH_CONFIG.SCROLL_BEHAVIOR, + block: SEARCH_CONFIG.SCROLL_BLOCK + }); + } + }, [selectedIndex, results]); + + // Cleanup debounce timeout on unmount or modal close + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, []); + + const getResultIcon = (type) => { + const iconMap = { + [SEARCH_TYPES.DOCUMENTATION]: IconBook, + [SEARCH_TYPES.COLLECTION]: IconBox, + [SEARCH_TYPES.FOLDER]: IconFolder, + [SEARCH_TYPES.REQUEST]: IconFileText + }; + const IconComponent = iconMap[type] || IconFileText; + return ; + }; + + if (!isOpen) return null; + + return ( + +
+
e.stopPropagation()}> +

Global Search

+

+ Search through collections, requests, folders, and documentation. Use arrow keys to navigate results and Enter to select. +

+
+ {results.length > 0 && query + ? `${results.length} result${results.length === 1 ? '' : 's'} found` + : query && results.length === 0 + ? 'No results found' + : '' + } +
+
+
+
+
+ +
+ {results.length === 0 && query ? ( +
+

+ No results found for "{query}". +
+ + The item might not exist yet, or its collection isn’t mounted. Press Enter here (or open it from the sidebar) to mount the collection automatically. + +

+
+ ) : results.length === 0 ? ( +
+

+ No collections are currently mounted or visible. +
+ + Mount a collection via the sidebar or this search modal, then try again. + +

+
+ ) : ( + results.map((result, index) => { + const isSelected = index === selectedIndex; + const typeLabel = getTypeLabel(result.type); + + return ( +
handleResultSelection(result)} + data-selected={isSelected} + data-type={result.type} + role="option" + aria-selected={isSelected} + aria-label={`${result.name}, ${typeLabel || result.type}${result.method ? `, ${result.method}` : ''}`} + tabIndex={-1} + > +
+ {getResultIcon(result.type)} +
+
+
+
+ {highlightText(result.name, query)} +
+
+ {result.type === SEARCH_TYPES.DOCUMENTATION + ? result.description + : result.type === SEARCH_TYPES.REQUEST + ? highlightText(result.item.request?.url || '', query) + : highlightText(result.path, query)} +
+
+
+ {result.type === SEARCH_TYPES.REQUEST && result.method && ( + + {result.method.toUpperCase().replace(/-/g, ' ')} + + )} + {typeLabel && ( +
+ {typeLabel} +
+ )} +
+
+
+ ); + }) + )} +
+ +
+
+ + + + to navigate + + + + to select + + + + to close + +
+
+
+
+
+ ); +}; + +export default GlobalSearchModal; \ No newline at end of file diff --git a/packages/bruno-app/src/components/GlobalSearchModal/utils/searchUtils.js b/packages/bruno-app/src/components/GlobalSearchModal/utils/searchUtils.js new file mode 100644 index 000000000..b55cdb1ab --- /dev/null +++ b/packages/bruno-app/src/components/GlobalSearchModal/utils/searchUtils.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { SEARCH_TYPES, MATCH_TYPES, SEARCH_CONFIG } from '../constants'; + +export const normalizeQuery = (searchQuery) => { + return searchQuery.trim().replace(/\/+/g, '/'); +}; + +export const isValidQuery = (normalizedQuery) => { + return normalizedQuery && + normalizedQuery !== '/' && + !(normalizedQuery.length === 1 && !normalizedQuery.match(/[a-zA-Z0-9]/)); +}; + +export const highlightText = (text, searchQuery) => { + if (!searchQuery) return text; + + try { + const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedQuery})`, 'gi'); + return text.split(regex).map((part, i) => + regex.test(part) ? ( + {part} + ) : part + ); + } catch { + return text; + } +}; + +export const sortResults = (results) => { + return results.sort((a, b) => { + // Documentation always first + if (a.type === SEARCH_TYPES.DOCUMENTATION) return -1; + if (b.type === SEARCH_TYPES.DOCUMENTATION) return 1; + + // Sort by match type priority + const matchTypeOrder = { + [MATCH_TYPES.COLLECTION]: 0, + [MATCH_TYPES.FOLDER]: 1, + [MATCH_TYPES.REQUEST]: 2, + [MATCH_TYPES.URL]: 3, + [MATCH_TYPES.PATH]: 4 + }; + const aMatchType = matchTypeOrder[a.matchType] ?? 5; + const bMatchType = matchTypeOrder[b.matchType] ?? 5; + + if (aMatchType !== bMatchType) return aMatchType - bMatchType; + + // Sort by type priority + const typeOrder = { + [SEARCH_TYPES.COLLECTION]: 0, + [SEARCH_TYPES.FOLDER]: 1, + [SEARCH_TYPES.REQUEST]: 2 + }; + const aType = typeOrder[a.type] ?? 3; + const bType = typeOrder[b.type] ?? 3; + + if (aType !== bType) return aType - bType; + + // Finally sort alphabetically + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); +}; + +export const getTypeLabel = (type) => { + const baseLabels = { + [SEARCH_TYPES.DOCUMENTATION]: 'Documentation', + [SEARCH_TYPES.COLLECTION]: 'Collection', + [SEARCH_TYPES.FOLDER]: 'Folder' + }; + + return baseLabels[type] || ''; +}; + +export const getItemPath = (item, collection, findParentItemInCollection) => { + const pathParts = []; + let currentItem = item; + let depth = 0; + const maxDepth = SEARCH_CONFIG.MAX_DEPTH; + + while (currentItem && depth < maxDepth) { + pathParts.unshift(currentItem.name); + const parent = findParentItemInCollection(collection, currentItem.uid); + if (parent) { + currentItem = parent; + depth++; + } else { + break; + } + } + + pathParts.unshift(collection.name); + return pathParts.join('/'); +}; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 99bf1f89d..c52359711 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -9,7 +9,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
{customHeader ? customHeader : <>{title ?
{title}
: null}} {handleCancel && !hideClose ? ( -
handleCancel() : null}> +
handleCancel() : null} data-test-id="modal-close-button"> ×
) : null} diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.spec.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.spec.js new file mode 100644 index 000000000..568284dfd --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.spec.js @@ -0,0 +1,248 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import HttpMethodSelector from './index'; +import themes from 'themes/index'; +import userEvent from '@testing-library/user-event'; + +const renderWithTheme = (component) => { + return render( + + {component} + + ); +}; + +describe('HttpMethodSelector', () => { + const mockOnMethodSelect = jest.fn(); + + beforeEach(() => { + mockOnMethodSelect.mockClear(); + }); + + describe('Initial Render', () => { + it('should render with default GET method when no method prop is provided', () => { + renderWithTheme(); + + const methodSpan = screen.getByText('GET'); + expect(methodSpan).toBeInTheDocument(); + expect(methodSpan).toHaveClass('method-span'); + expect(methodSpan).toHaveAttribute('title', 'GET'); + }); + + it('should render with a standard method when method prop is provided', () => { + renderWithTheme(); + + const methodSpan = screen.getByText('POST'); + expect(methodSpan).toBeInTheDocument(); + expect(methodSpan).toHaveAttribute('title', 'POST'); + }); + + it('should render with a custom method when method prop is provided', () => { + renderWithTheme(); + + const methodSpan = screen.getByText('CUSTOM'); + expect(methodSpan).toBeInTheDocument(); + expect(methodSpan).toHaveAttribute('title', 'CUSTOM'); + }); + }); + + describe('Dropdown Interaction', () => { + it('should display all standard HTTP methods in dropdown when clicked', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const standardMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT']; + const dropdownItems = screen.getAllByText((content, element) => { + return element?.classList.contains('dropdown-item'); + }); + const renderedMethods = dropdownItems.map(item => item.textContent); + + standardMethods.forEach(method => { + expect(renderedMethods).toContain(method); + }); + }); + }); + + it('should display "Add Custom" option in dropdown', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustomSpan = screen.getByText('+ Add Custom'); + expect(addCustomSpan).toBeInTheDocument(); + expect(addCustomSpan).toHaveClass('text-link'); + }); + }); + + it('should call onMethodSelect when a standard method is clicked', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const postMethod = screen.getByText('POST'); + fireEvent.click(postMethod); + }); + + expect(mockOnMethodSelect).toHaveBeenCalledWith('POST'); + }); + }); + + describe('Custom Method Mode', () => { + it('should enter custom mode when "Add Custom" is clicked', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + expect(mockOnMethodSelect).toHaveBeenCalledWith(''); + + // Should show input field + await waitFor(() => { + const input = screen.getByRole('textbox'); + expect(input).toBeInTheDocument(); + expect(input).toHaveFocus(); + }); + }); + + it('should call onMethodSelect with uppercase value when typing in custom input', async () => { + const user = userEvent.setup(); + + // Create a wrapper component that manages the method state + const TestWrapper = () => { + const [method, setMethod] = React.useState('GET'); + + const handleMethodSelect = (newMethod) => { + mockOnMethodSelect(newMethod); + setMethod(newMethod); + }; + + return ( + + ); + }; + + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + const input = await screen.findByRole('textbox'); + await user.type(input, 'custom'); + + expect(mockOnMethodSelect).toHaveBeenCalledWith(''); + expect(mockOnMethodSelect).toHaveBeenCalledWith('C'); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CU'); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUS'); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUST'); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTO'); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM'); + expect(mockOnMethodSelect).toHaveBeenCalledTimes(7); + }); + + it('should exit custom mode and set method on Enter key', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + const input = await screen.findByRole('textbox'); + fireEvent.change(input, { target: { value: 'CUSTOM' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM'); + + // Should exit custom mode + await waitFor(() => { + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); + }); + }); + + it('should set default method on Enter key with empty input', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + const input = await screen.findByRole('textbox'); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(mockOnMethodSelect).toHaveBeenCalledWith('GET'); + }); + + it('should exit custom mode on Escape key and keep the custom method', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + const input = await screen.findByRole('textbox'); + fireEvent.change(input, { target: { value: 'CUSTOM' } }); + fireEvent.keyDown(input, { key: 'Escape' }); + + // Should exit custom mode and onMethodSelect should be called with custom method + await waitFor(() => { + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM'); + }); + }); + + it('should exit custom mode on blur and keep the custom method', async () => { + renderWithTheme(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + const addCustom = screen.getByText('+ Add Custom'); + fireEvent.click(addCustom); + }); + + const input = await screen.findByRole('textbox'); + fireEvent.change(input, { target: { value: 'CUSTOM' } }); + fireEvent.blur(input); + + // Should exit custom mode and onMethodSelect should be called with custom method + await waitFor(() => { + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); + expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM'); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js index 5349f2410..a119245e8 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js @@ -63,7 +63,8 @@ const StyledWrapper = styled.div` } div.graphql-docs-explorer-container { - background: white; + background: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.bg}; + color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.color}; outline: none; box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px; position: absolute; @@ -72,6 +73,14 @@ const StyledWrapper = styled.div` width: 350px; height: 100%; + .doc-explorer-contents, + .doc-explorer, + .search-box > input, + .search-box-clear { + background-color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.bg}; + color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.color}; + } + div.doc-explorer-title { text-align: left; } diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js index 65badf3aa..f3ec920a2 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js @@ -78,7 +78,7 @@ const TitleBar = () => { ) : null}
- +
+ +
+ {t('WELCOME.GLOBAL_SEARCH_TIP_PART1')} {' '}K{' '} + {t('WELCOME.GLOBAL_SEARCH_TIP_PART2')} Ctrl{' '}K{' '} + {t('WELCOME.GLOBAL_SEARCH_TIP_PART3')} +
); diff --git a/packages/bruno-app/src/i18n/translation/en.json b/packages/bruno-app/src/i18n/translation/en.json index 7dda41e42..4f3aff16e 100644 --- a/packages/bruno-app/src/i18n/translation/en.json +++ b/packages/bruno-app/src/i18n/translation/en.json @@ -15,6 +15,9 @@ "IMPORT_COLLECTION": "Import Collection", "COLLECTION_IMPORT_SUCCESS": "Collection imported successfully", "COLLECTION_IMPORT_ERROR": "An error occurred while importing the collection. Check the logs for more information.", - "COLLECTION_OPEN_ERROR": "An error occurred while opening the collection" + "COLLECTION_OPEN_ERROR": "An error occurred while opening the collection", + "GLOBAL_SEARCH_TIP_PART1": "Press", + "GLOBAL_SEARCH_TIP_PART2": "(mac) or", + "GLOBAL_SEARCH_TIP_PART3": "(windows) anytime to quickly search collections, folders, and requests" } } diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index d8a239907..7aef15122 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -6,6 +6,7 @@ import { useSelector, useDispatch } from 'react-redux'; import EnvironmentSettings from 'components/Environments/EnvironmentSettings'; import NetworkError from 'components/ResponsePane/NetworkError'; import NewRequest from 'components/Sidebar/NewRequest'; +import GlobalSearchModal from 'components/GlobalSearchModal'; import { sendRequest, saveRequest, @@ -27,6 +28,7 @@ export const HotkeysProvider = (props) => { const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen); const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); + const [showGlobalSearchModal, setShowGlobalSearchModal] = useState(false); const getCurrentCollection = () => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); @@ -149,6 +151,19 @@ export const HotkeysProvider = (props) => { }; }, [activeTabUid, tabs, collections, setShowNewRequestModal]); + // global search (ctrl/cmd + k) + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('globalSearch')], (e) => { + setShowGlobalSearchModal(true); + + return false; // stop bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('globalSearch')]); + }; + }, []); + // close tab hotkey useEffect(() => { Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => { @@ -247,6 +262,9 @@ export const HotkeysProvider = (props) => { {showNewRequestModal && ( setShowNewRequestModal(false)} /> )} + {showGlobalSearchModal && ( + setShowGlobalSearchModal(false)} /> + )}
{props.children}
); diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js index 0fd9bf1bc..997eb2cd0 100644 --- a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js +++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js @@ -3,6 +3,7 @@ const KeyMapping = { sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' }, editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' }, newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' }, + globalSearch: { mac: 'command+k', windows: 'ctrl+k', name: 'Global Search' }, closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' }, openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' }, closeBruno: { diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 02f895a10..1b4e6ade8 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -160,6 +160,10 @@ const darkTheme = { color: '#ccc' } } + }, + graphqlDocsExplorer: { + bg: '#1e1e1e', + color: '#d4d4d4' } }, diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index e6d230e23..d4fe21fd4 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -157,6 +157,10 @@ const lightTheme = { color: 'rgb(75 85 99)' } } + }, + graphqlDocsExplorer: { + bg: '#fff', + color: 'rgb(52, 52, 52)' } }, diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index d44aa547a..558611810 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -312,7 +312,22 @@ const isURL = (arg) => { if (typeof arg !== 'string') { return false; } - return !!URL.parse(arg || '').host; + + // First try to parse as a regular URL (with protocol) + if (URL.parse(arg || '').host) { + return true; + } + + // Check if it looks like a domain without protocol + // This regex matches domain patterns like: + // - example.com + // - sub.example.com + // - example.com/path + // - example.com/path?query=value + // Must contain at least one dot to be considered a domain + const DOMAIN_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\/[^\s]*)?(\?[^\s]*)?$/; + + return DOMAIN_PATTERN.test(arg); }; /** @@ -320,8 +335,9 @@ const isURL = (arg) => { * Handles shell-quote operator objects and query parameter patterns */ const isURLFragment = (arg) => { + // If it's a glob pattern that looks like a URL, treat it as a complete URL if (arg && typeof arg === 'object' && arg.op === 'glob') { - return !!URL.parse(arg.pattern || '').host; + return isURL(arg.pattern); } if (arg && typeof arg === 'object' && arg.op === '&') { return true; @@ -341,7 +357,13 @@ const setURL = (request, url) => { const urlString = getUrlString(url); if (!urlString) return; - const newUrl = request.url ? request.url + urlString : urlString; + // Add default protocol if none is present + let processedUrl = urlString; + if (!request.url && !urlString.match(/^[a-zA-Z]+:\/\//)) { + processedUrl = 'https://' + urlString; + } + + const newUrl = request.url ? request.url + processedUrl : processedUrl; const { url: formattedUrl, queries, urlWithoutQuery } = parseUrl(newUrl); diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js index 3ab767f62..7e8cff58a 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.spec.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -438,6 +438,73 @@ describe('parseCurlCommand', () => { }); }); + describe('handling URLs without protocols', () => { + it('should parse URL without protocol and default to https', () => { + const result = parseCurlCommand(` + curl echo.usebruno.com + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://echo.usebruno.com', + urlWithoutQuery: 'https://echo.usebruno.com' + }); + }); + + it('should parse URL without protocol with path and query parameters', () => { + const result = parseCurlCommand(` + curl api.example.com/users?page=1&limit=10 + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://api.example.com/users?page=1&limit=10', + urlWithoutQuery: 'https://api.example.com/users', + queries: [ + { name: 'page', value: '1' }, + { name: 'limit', value: '10' } + ] + }); + }); + + it('should parse a complex curl command with multiple features and no protocol', () => { + const result = parseCurlCommand(` + curl -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: custom header" \ + -d '{"name": "John\\'s data", "email": "john@example.com", "message": "Don\\'t stop believing!", "path": "/home/user/file.txt", "json": {"nested": "value", "array": [1, 2, 3]}}' \ + -u "api_user:api_pass" \ + --compressed \ + api.example.com/v1/users?param1=value1¶m2=custom+param + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom-Header': 'custom header', + 'Accept-Encoding': 'deflate, gzip' + }, + data: '{"name": "John\'s data", "email": "john@example.com", "message": "Don\'t stop believing!", "path": "/home/user/file.txt", "json": {"nested": "value", "array": [1, 2, 3]}}', + auth: { + mode: 'basic', + basic: { + username: 'api_user', + password: 'api_pass' + } + }, + queries: [ + { name: 'param1', value: 'value1' }, + { name: 'param2', value: 'custom+param' } + ], + url: 'https://api.example.com/v1/users?param1=value1¶m2=custom+param', + urlWithoutQuery: 'https://api.example.com/v1/users' + }); + }); + }); + describe('Edge Cases', () => { it('should handle compressed flag', () => { const result = parseCurlCommand(` diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 6dd102707..41e3dc714 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -351,6 +351,8 @@ const runSingleRequest = async function ( if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') { if (!(request?.data instanceof FormData)) { + request._originalMultipartData = request.data; + request.collectionPath = collectionPath; let form = createFormData(request.data, collectionPath); request.data = form; extend(request.headers, form.getHeaders()); diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index 1d0e7ccd9..4bba1af73 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -1,6 +1,7 @@ const axios = require('axios'); const { CLI_VERSION } = require('../constants'); const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); +const { createFormData } = require('./form-data'); const redirectResponseCodes = [301, 302, 303, 307, 308]; const METHOD_CHANGING_REDIRECTS = [301, 302, 303]; @@ -38,6 +39,28 @@ const createRedirectConfig = (error, redirectUrl) => { delete requestConfig.headers['Content-Length']; delete requestConfig.headers['content-type']; delete requestConfig.headers['Content-Type']; + } else { + // For 307, 308 and other status codes: preserve method and body + if (requestConfig.data && typeof requestConfig.data === 'object' && + requestConfig.data.constructor && requestConfig.data.constructor.name === 'FormData') { + + const formData = requestConfig.data; + if (formData._released || (formData._streams && formData._streams.length === 0)) { + if (error.config._originalMultipartData && error.config.collectionPath) { + const recreatedForm = createFormData(error.config._originalMultipartData, error.config.collectionPath); + requestConfig.data = recreatedForm; + const formHeaders = recreatedForm.getHeaders(); + Object.assign(requestConfig.headers, formHeaders); + + // preserve the original data for potential future redirects + requestConfig._originalMultipartData = error.config._originalMultipartData; + requestConfig.collectionPath = error.config.collectionPath; + } + } else { + requestConfig._originalMultipartData = error.config._originalMultipartData; + requestConfig.collectionPath = error.config.collectionPath; + } + } } return requestConfig; diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index 3b3cc2b14..1dd180cee 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -7,6 +7,7 @@ const { setupProxyAgents } = require('../../utils/proxy-util'); const { addCookieToJar, getCookieStringForUrl } = require('../../utils/cookies'); const { preferencesUtil } = require('../../store/preferences'); const { safeStringifyJSON } = require('../../utils/common'); +const { createFormData } = require('../../utils/form-data'); const LOCAL_IPV6 = '::1'; const LOCAL_IPV4 = '127.0.0.1'; @@ -328,6 +329,41 @@ function makeAxiosInstance({ type: 'info', message: `Changed method from ${originalMethod.toUpperCase()} to GET for ${statusCode} redirect and removed request body`, }); + } else { + // For 307, 308 and other status codes: preserve method and body + if (requestConfig.data && typeof requestConfig.data === 'object' && + requestConfig.data.constructor && requestConfig.data.constructor.name === 'FormData') { + + const formData = requestConfig.data; + if (formData._released || (formData._streams && formData._streams.length === 0)) { + if (error.config._originalMultipartData && error.config.collectionPath) { + timeline.push({ + timestamp: new Date(), + type: 'info', + message: `Recreating consumed FormData for ${statusCode} redirect`, + }); + + const recreatedForm = createFormData(error.config._originalMultipartData, error.config.collectionPath); + requestConfig.data = recreatedForm; + + const formHeaders = recreatedForm.getHeaders(); + Object.assign(requestConfig.headers, formHeaders); + + // preserve the original data for potential future redirects + requestConfig._originalMultipartData = error.config._originalMultipartData; + requestConfig.collectionPath = error.config.collectionPath; + } else { + timeline.push({ + timestamp: new Date(), + type: 'info', + message: `FormData consumed but no original data available for ${statusCode} redirect`, + }); + } + } else { + requestConfig._originalMultipartData = error.config._originalMultipartData; + requestConfig.collectionPath = error.config.collectionPath; + } + } } if (preferencesUtil.shouldSendCookies()) { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 3c7f68727..11b30abc8 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -452,6 +452,8 @@ const registerNetworkIpc = (mainWindow) => { if (request.headers['content-type'] === 'multipart/form-data') { if (!(request.data instanceof FormData)) { + request._originalMultipartData = request.data; + request.collectionPath = collectionPath; let form = createFormData(request.data, collectionPath); request.data = form; extend(request.headers, form.getHeaders()); diff --git a/packages/bruno-tests/collection/environments/Local.bru b/packages/bruno-tests/collection/environments/Local.bru index a7ac3f541..9980c994e 100644 --- a/packages/bruno-tests/collection/environments/Local.bru +++ b/packages/bruno-tests/collection/environments/Local.bru @@ -1,5 +1,6 @@ vars { host: http://localhost:8080 + localhost: http://localhost:8081 httpfaker: https://www.httpfaker.org bearer_auth_token: your_secret_token basic_auth_password: della diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru index f33c1bb05..92ba5fc76 100644 --- a/packages/bruno-tests/collection/environments/Prod.bru +++ b/packages/bruno-tests/collection/environments/Prod.bru @@ -1,5 +1,6 @@ vars { host: https://testbench-sanity.usebruno.com + localhost: http://localhost:8081 httpfaker: https://www.httpfaker.org bearer_auth_token: your_secret_token basic_auth_password: della diff --git a/packages/bruno-tests/collection/redirects/Test Multipart Redirect Consumed FormData.bru b/packages/bruno-tests/collection/redirects/Test Multipart Redirect Consumed FormData.bru new file mode 100644 index 000000000..1b00a438a --- /dev/null +++ b/packages/bruno-tests/collection/redirects/Test Multipart Redirect Consumed FormData.bru @@ -0,0 +1,45 @@ +meta { + name: Test Multipart Redirect Consumed FormData + type: http + seq: 7 +} + +post { + url: {{localhost}}/api/redirect/multipart-redirect-source + body: multipartForm + auth: none +} + +body:multipart-form { + consumed-field: consumed-value +} + +assert { + res.status: 200 +} + +tests { + test("should handle consumed FormData recreation during 308 redirect", function() { + const data = res.getBody(); + expect(data).to.be.an('object'); + expect(data.status).to.equal('success'); + expect(data.method).to.equal('POST'); + }); + + test("should preserve POST method when FormData is consumed and recreated", function() { + const data = res.getBody(); + expect(data.method).to.equal('POST'); + }); + + test("should receive form data after FormData recreation", function() { + const data = res.getBody(); + expect(data.body).to.have.property('consumed-field'); + expect(data.body['consumed-field']).to.equal('consumed-value'); + }); + + test("should maintain proper content-type after FormData recreation", function() { + const data = res.getBody(); + expect(data.headers).to.have.property('content-type'); + expect(data.headers['content-type']).to.include('multipart/form-data'); + }); +} diff --git a/packages/bruno-tests/collection/redirects/Test Multipart Redirect Multiple Fields.bru b/packages/bruno-tests/collection/redirects/Test Multipart Redirect Multiple Fields.bru new file mode 100644 index 000000000..29da01222 --- /dev/null +++ b/packages/bruno-tests/collection/redirects/Test Multipart Redirect Multiple Fields.bru @@ -0,0 +1,48 @@ +meta { + name: Test Multipart Redirect Multiple Fields + type: http + seq: 5 +} + +post { + url: {{localhost}}/api/redirect/multipart-redirect-source + body: multipartForm + auth: none +} + +body:multipart-form { + field1: value1 + field2: value2 + field3: value3 +} + +assert { + res.status: 200 +} + +tests { + test("should successfully redirect complex multipart form data with 308", function() { + const data = res.getBody(); + expect(data).to.be.an('object'); + expect(data.status).to.equal('success'); + expect(data.method).to.equal('POST'); + }); + + test("should preserve POST method during redirect", function() { + const data = res.getBody(); + expect(data.method).to.equal('POST'); + }); + + test("should receive all text fields at target endpoint", function() { + const data = res.getBody(); + expect(data.body).to.have.property('field1'); + expect(data.body).to.have.property('field2'); + expect(data.body).to.have.property('field3'); + }); + + test("should maintain content-type header during redirect", function() { + const data = res.getBody(); + expect(data.headers).to.have.property('content-type'); + expect(data.headers['content-type']).to.include('multipart/form-data'); + }); +} diff --git a/packages/bruno-tests/collection/redirects/Test Multipart Redirect.bru b/packages/bruno-tests/collection/redirects/Test Multipart Redirect.bru new file mode 100644 index 000000000..099ed4ba6 --- /dev/null +++ b/packages/bruno-tests/collection/redirects/Test Multipart Redirect.bru @@ -0,0 +1,41 @@ +meta { + name: Test Multipart Redirect + type: http + seq: 3 +} + +post { + url: {{localhost}}/api/redirect/multipart-redirect-source + body: multipartForm + auth: none +} + +body:multipart-form { + test-field: test-value +} + +assert { + res.status: 200 +} + +tests { + test("should successfully redirect multipart form data with 308", function() { + const data = res.getBody(); + expect(data).to.be.an('object'); + expect(data.status).to.equal('success'); + expect(data.method).to.equal('POST'); + expect(data.body).to.be.an('object'); + expect(data.body['test-field']).to.equal('test-value'); + }); + + test("should preserve POST method during redirect", function() { + const data = res.getBody(); + expect(data.method).to.equal('POST'); + }); + + test("should receive form data at target endpoint", function() { + const data = res.getBody(); + expect(data.body).to.have.property('test-field'); + expect(data.body['test-field']).to.equal('test-value'); + }); +} diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index 735bd929b..2e39f7a9c 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -6,6 +6,7 @@ const authRouter = require('./auth'); const echoRouter = require('./echo'); const xmlParser = require('./utils/xmlParser'); const multipartRouter = require('./multipart'); +const redirectRouter = require('./redirect'); const app = new express(); const port = process.env.PORT || 8081; @@ -28,6 +29,7 @@ formDataParser.init(app, express); app.use('/api/auth', authRouter); app.use('/api/echo', echoRouter); app.use('/api/multipart', multipartRouter); +app.use('/api/redirect', redirectRouter); app.get('/ping', function (req, res) { return res.send('pong'); diff --git a/packages/bruno-tests/src/redirect/index.js b/packages/bruno-tests/src/redirect/index.js new file mode 100644 index 000000000..4573fdf74 --- /dev/null +++ b/packages/bruno-tests/src/redirect/index.js @@ -0,0 +1,64 @@ +const express = require('express'); +const formDataParser = require('../multipart/form-data-parser'); +const router = express.Router(); + +const parseMultipartFormData = (req) => { + if (req.headers['content-type'] && req.headers['content-type'].includes('multipart/form-data')) { + try { + const parts = formDataParser.parse(req); + const parsedBody = {}; + const files = []; + + parts.forEach(part => { + if (part.filename) { + files.push({ + fieldname: part.name, + originalname: part.filename, + mimetype: part.contentType, + size: part.value ? part.value.length : 0 + }); + } else { + parsedBody[part.name] = part.value; + } + }); + + return { body: parsedBody, files }; + } catch (error) { + console.error('Error parsing multipart form data:', error); + return { body: {}, files: [] }; + } + } + return { body: req.body, files: [] }; +}; + +router.post('/multipart-redirect-source', function (req, res) { + console.log('Multipart redirect source endpoint hit'); + console.log('Method:', req.method); + console.log('Headers:', req.headers); + + const { body, files } = parseMultipartFormData(req); + console.log('Parsed Body:', body); + console.log('Files:', files); + + res.status(308).location('/api/redirect/multipart-redirect-target').send('Permanently moved'); +}); + +router.post('/multipart-redirect-target', function (req, res) { + console.log('Multipart redirect target endpoint hit'); + console.log('Method:', req.method); + console.log('Headers:', req.headers); + + const { body, files } = parseMultipartFormData(req); + console.log('Parsed Body:', body); + console.log('Files:', files); + + res.json({ + status: 'success', + method: req.method, + body: body, + files: files, + headers: req.headers + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/readme.md b/readme.md index 3c800536a..7954a914a 100644 --- a/readme.md +++ b/readme.md @@ -95,9 +95,12 @@ flatpak install com.usebruno.Bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl -sudo gpg --list-keys -curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" | gpg --dearmor | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null -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 +curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null +sudo chmod 644 /etc/apt/keyrings/bruno.gpg +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 ```