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'
+ : ''
+ }
+
+
+
+
+ 0}
+ aria-controls="search-results"
+ aria-activedescendant={results.length > 0 ? `search-result-${selectedIndex}` : undefined}
+ role="combobox"
+ aria-autocomplete="list"
+ />
+ {query && (
+
+
+
+ )}
+
+
+
+
+ {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
+
+
+ esc
+ 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}
-
+
diff --git a/packages/bruno-app/src/components/StatusBar/index.js b/packages/bruno-app/src/components/StatusBar/index.js
index a98db747b..25b984ffe 100644
--- a/packages/bruno-app/src/components/StatusBar/index.js
+++ b/packages/bruno-app/src/components/StatusBar/index.js
@@ -1,9 +1,11 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { IconSettings, IconCookie, IconTool } from '@tabler/icons';
-import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
+import { IconSettings, IconCookie, IconTool, IconSearch } from '@tabler/icons';
+import Mousetrap from 'mousetrap';
+import { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings';
import ToolHint from 'components/ToolHint';
import Preferences from 'components/Preferences';
+import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
import Cookies from 'components/Cookies';
import Notifications from 'components/Notifications';
import Portal from 'components/Portal';
@@ -26,6 +28,13 @@ const StatusBar = () => {
dispatch(openConsole());
};
+ const openGlobalSearch = () => {
+ const bindings = getKeyBindingsForActionAllOS('globalSearch') || [];
+ bindings.forEach((binding) => {
+ Mousetrap.trigger(binding);
+ });
+ };
+
return (
{preferencesOpen && (
@@ -93,6 +102,19 @@ const StatusBar = () => {
+
+
+
+ Search
+
+
+
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: 0.75rem;
+ font-weight: 500;
+ font-family: inherit;
+ line-height: 1;
+ color: ${(props) => props.theme.text};
+ }
`;
export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js
index fd8cbd756..f36b9eb4a 100644
--- a/packages/bruno-app/src/components/Welcome/index.js
+++ b/packages/bruno-app/src/components/Welcome/index.js
@@ -138,6 +138,12 @@ const Welcome = () => {
{t('COMMON.GITHUB')}
+
+
+ {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
```