mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
138 Commits
feature/cu
...
v2.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aebc8241cc | ||
|
|
0eda1b761d | ||
|
|
a05f7cb686 | ||
|
|
745a71700c | ||
|
|
ac9c190b41 | ||
|
|
1a1a230a1e | ||
|
|
b2e02b7762 | ||
|
|
9cbfeccbed | ||
|
|
4725300c41 | ||
|
|
f2aedf780d | ||
|
|
f03047a2f9 | ||
|
|
a7ba23d97e | ||
|
|
2521e980ea | ||
|
|
1c118fa04a | ||
|
|
b6fb5e02d4 | ||
|
|
5313704d84 | ||
|
|
b147f14fef | ||
|
|
66fe1528df | ||
|
|
a598cda624 | ||
|
|
e1c12ea699 | ||
|
|
9801e91720 | ||
|
|
364fb45e97 | ||
|
|
5c9981aca2 | ||
|
|
fc697bf81b | ||
|
|
9bc07afc77 | ||
|
|
e4ae857df3 | ||
|
|
3d26833b8a | ||
|
|
1089a52171 | ||
|
|
9dde2df475 | ||
|
|
1cc94e8ffe | ||
|
|
223f79a3e2 | ||
|
|
5dc6f6757d | ||
|
|
e20fe790a6 | ||
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
cb611c6510 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
6f9daadcfb | ||
|
|
2de9b87c6f | ||
|
|
8d5d952026 | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
afb2d3dffd | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
9f1aed3209 | ||
|
|
ce1110bdd4 | ||
|
|
788569a5f4 | ||
|
|
91397eaf57 | ||
|
|
c293ceefcf | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
256f63dd38 | ||
|
|
0948964677 | ||
|
|
1b52bb27f7 | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
3e714ab9f8 | ||
|
|
f2e9a6a502 | ||
|
|
b924e15afa | ||
|
|
b0c74909ba | ||
|
|
548a6b4319 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
9c9afaf78f | ||
|
|
6cde453032 | ||
|
|
8f06889996 | ||
|
|
52662f0766 | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
5567e1b7f2 | ||
|
|
3cd18d1e16 | ||
|
|
9d3e42b5d4 | ||
|
|
0f318c26c2 | ||
|
|
79f4e69a05 | ||
|
|
6598d23ff0 | ||
|
|
c83436655c | ||
|
|
62595c519c | ||
|
|
d2eb2d2941 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
8e91640084 | ||
|
|
0ca2891166 | ||
|
|
5000bb8db3 | ||
|
|
9927424826 | ||
|
|
c14d3f4274 | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
0f6da35c0b | ||
|
|
ad3f5de99a | ||
|
|
2de7ba0d0c | ||
|
|
0d7c94e7e9 | ||
|
|
9e29821012 | ||
|
|
e0fb379511 | ||
|
|
ba9362ccb2 | ||
|
|
261a36c435 | ||
|
|
cb92e46f8d | ||
|
|
b5861dae39 | ||
|
|
f6ab59ceda | ||
|
|
f1004e2e36 | ||
|
|
26eaec4c72 | ||
|
|
d0419edb92 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
44
.github/workflows/playwright.yml
vendored
44
.github/workflows/playwright.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Playwright E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
42
.github/workflows/tests.yml
vendored
42
.github/workflows/tests.yml
vendored
@@ -91,5 +91,47 @@ jobs:
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Install dependencies for test collection environment
|
||||
run: |
|
||||
npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -99,14 +99,13 @@ npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
If `ELECTRON_APP_NAME` env-variable is present and its development mode, then the `appName` and `userData` path is modified accordingly.
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
```sh
|
||||
ELECTRON_APP_NAME=bruno-dev npm run dev:electron
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
> This doesn't change the name of the window or the names in lot of other places, only the name used by Electron internally.
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
||||
151
docs/readme/readme_hi.md
Normal file
151
docs/readme/readme_hi.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| [Italiano](./readme_it.md)
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
| **हिन्दी**
|
||||
|
||||
ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है।
|
||||
|
||||
ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं।
|
||||
|
||||
आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं।
|
||||
|
||||
ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें।
|
||||
|
||||
📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें।
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### गोल्डन संस्करण ✨
|
||||
|
||||
हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं।
|
||||
हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं।
|
||||
|
||||
[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी! <br/>
|
||||
[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें।
|
||||
|
||||
### स्थापना
|
||||
|
||||
ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है।
|
||||
|
||||
आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं।
|
||||
|
||||
```sh
|
||||
# Mac पर Homebrew के माध्यम से
|
||||
brew install bruno
|
||||
|
||||
# Windows पर Chocolatey के माध्यम से
|
||||
choco install bruno
|
||||
|
||||
# Windows पर Scoop के माध्यम से
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Linux पर Snap के माध्यम से
|
||||
snap install bruno
|
||||
|
||||
# Linux पर Flatpak के माध्यम से
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# Linux पर Apt के माध्यम से
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
|
||||
कई प्लेटफार्मों पर चलाएं 🖥️
|
||||
<br /><br />
|
||||
|
||||
Git के माध्यम से सहयोग करें 👩💻🧑💻
|
||||
या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें
|
||||
|
||||
<br /><br />
|
||||
|
||||
महत्वपूर्ण लिंक 📌
|
||||
हमारी दीर्घकालिक दृष्टि
|
||||
|
||||
रोडमैप
|
||||
|
||||
प्रलेखन
|
||||
|
||||
Stack Overflow
|
||||
|
||||
वेबसाइट
|
||||
|
||||
मूल्य निर्धारण
|
||||
|
||||
डाउनलोड
|
||||
|
||||
GitHub प्रायोजक
|
||||
|
||||
प्रस्तुतियाँ 🎥
|
||||
प्रशंसापत्र
|
||||
|
||||
ज्ञान केंद्र
|
||||
|
||||
Scriptmania
|
||||
|
||||
समर्थन ❤️
|
||||
यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें।
|
||||
|
||||
प्रशंसापत्र साझा करें 📣
|
||||
यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें
|
||||
|
||||
नए पैकेज प्रबंधकों में प्रकाशित करना
|
||||
अधिक जानकारी के लिए कृपया यहाँ देखें।
|
||||
|
||||
हमसे संपर्क करें 🌐
|
||||
𝕏 (ट्विटर) <br />
|
||||
वेबसाइट <br />
|
||||
डिस्कॉर्ड <br />
|
||||
लिंक्डइन
|
||||
|
||||
ट्रेडमार्क
|
||||
नाम
|
||||
|
||||
ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है।
|
||||
|
||||
लोगो
|
||||
|
||||
लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0
|
||||
|
||||
योगदान 👩💻🧑💻
|
||||
हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें।
|
||||
|
||||
यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए।
|
||||
|
||||
लेखक
|
||||
<div align="center"> <a href="https://github.com/usebruno/bruno/graphs/contributors"> <img src="https://contrib.rocks/image?repo=usebruno/bruno" /> </a> </div>
|
||||
|
||||
लाइसेंस 📄
|
||||
MIT
|
||||
|
||||
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Name').press('Tab');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByText('GETr1').click();
|
||||
await page.getByRole('button', { name: 'Clear response' }).click();
|
||||
await page.locator('body').press('ControlOrMeta+Enter');
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
|
||||
}
|
||||
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.parallel('Run Testbench Requests', () => {
|
||||
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Developer Mode(use only if').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
|
||||
|
||||
// Open Preferences
|
||||
await page.getByLabel('Open Preferences').click();
|
||||
|
||||
// Verify Support tab
|
||||
await page.getByRole('tab', { name: 'Support' }).click();
|
||||
|
||||
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
|
||||
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
|
||||
|
||||
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
|
||||
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
|
||||
|
||||
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
|
||||
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
|
||||
|
||||
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
|
||||
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
|
||||
|
||||
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
|
||||
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
|
||||
|
||||
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { test, expect } from '../playwright';
|
||||
|
||||
test('test-app-start', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
@@ -38,4 +38,4 @@ module.exports = defineConfig([
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
]);
|
||||
3004
package-lock.json
generated
3004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
@@ -71,4 +72,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/bruno-app/babel.config.js
Normal file
9
packages/bruno-app/babel.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'automatic'
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
@@ -12,5 +12,14 @@ module.exports = {
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'node'
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(nanoid|xml-formatter)/)'
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
@@ -11,7 +12,6 @@
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
@@ -82,19 +82,28 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@rsbuild/core": "^1.1.2",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.2.0",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"postcss": "8.4.47",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -102,4 +111,4 @@
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ if (!SERVER_RENDERED) {
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
@@ -80,12 +81,14 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()'
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -363,7 +366,7 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
username: username || '',
|
||||
password: basicAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
username: basicAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: digestAuth.password
|
||||
username: username || '',
|
||||
password: digestAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: digestAuth.username,
|
||||
password: password
|
||||
username: digestAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
username: username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
|
||||
}
|
||||
})
|
||||
@@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: password,
|
||||
domain: ntlmAuth.domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: ntlmAuth.password,
|
||||
domain: domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username,
|
||||
password: wsseAuth.password
|
||||
username: username || '',
|
||||
password: wsseAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: wsseAuth.username,
|
||||
password
|
||||
username: wsseAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const auth = get(collection, 'root.request.auth', {}).mode;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{auth !== 'none' && <ContentIndicator />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
|
||||
@@ -11,6 +11,12 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -9,6 +9,14 @@ import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/Passwo
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
|
||||
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
|
||||
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -37,12 +45,132 @@ const Auth = ({ collection, folder }) => {
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const getTreePathFromCollectionToFolder = (collection, _folder) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _folder?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<BasicAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'bearer': {
|
||||
return (
|
||||
<BearerAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'digest': {
|
||||
return (
|
||||
<DigestAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ntlm': {
|
||||
return (
|
||||
<NTLMAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'wsse': {
|
||||
return (
|
||||
<WsseAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'apikey': {
|
||||
return (
|
||||
<ApiKeyAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'awsv4': {
|
||||
return (
|
||||
<AwsV4Auth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
@@ -56,6 +184,17 @@ const Auth = ({ collection, folder }) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
@@ -64,6 +203,7 @@ const Auth = ({ collection, folder }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
|
||||
@@ -35,6 +35,51 @@ const AuthMode = ({ collection, folder }) => {
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
@@ -44,6 +89,33 @@ const AuthMode = ({ collection, folder }) => {
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('inherit');
|
||||
}}
|
||||
>
|
||||
Inherit
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -130,7 +130,7 @@ class MultiLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -5,21 +5,23 @@ import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
const ApiKeyAuth = ({ item, collection }) => {
|
||||
const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
|
||||
const apikeyAuth = get(request, 'auth.apikey', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -90,7 +92,7 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'header');
|
||||
}}
|
||||
>
|
||||
@@ -99,11 +101,11 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'queryparams');
|
||||
}}
|
||||
>
|
||||
Query Params
|
||||
Query Param
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,17 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
|
||||
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ item, collection }) => {
|
||||
const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,16 +7,18 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ item, collection }) => {
|
||||
const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = item.draft
|
||||
? get(item, 'draft.request.auth.bearer.token', '')
|
||||
: get(item, 'request.auth.bearer.token', '');
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
|
||||
@@ -3,18 +3,20 @@ import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ item, collection }) => {
|
||||
const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NTLMAuth = ({ item, collection }) => {
|
||||
const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -26,7 +29,6 @@ const NTLMAuth = ({ item, collection }) => {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ item, collection }) => {
|
||||
const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
@@ -55,6 +58,7 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +71,8 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,6 +7,8 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -27,6 +29,16 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Create a request object to pass to the auth components
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
// Save function for request level
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
@@ -59,28 +71,28 @@ const Auth = ({ item, collection }) => {
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} item={item} />;
|
||||
return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} />;
|
||||
return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'digest': {
|
||||
return <DigestAuth collection={collection} item={item} />;
|
||||
return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'ntlm': {
|
||||
return <NTLMAuth collection={collection} item={item} />;
|
||||
return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} />;
|
||||
return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'wsse': {
|
||||
return <WsseAuth collection={collection} item={item} />;
|
||||
return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
@@ -7,8 +7,10 @@ import Dropdown from '../../Dropdown';
|
||||
|
||||
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
|
||||
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
|
||||
const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');
|
||||
const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };
|
||||
|
||||
let {
|
||||
schema,
|
||||
|
||||
@@ -64,9 +64,10 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
mode="application/json"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
item={item}
|
||||
item={item}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
@@ -58,13 +58,14 @@ const RequestBody = ({ item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyMode === 'file') {
|
||||
return <FileBody item={item} collection={collection}/>
|
||||
return <FileBody item={item} collection={collection} />;
|
||||
}
|
||||
|
||||
if (bodyMode === 'formUrlEncoded') {
|
||||
@@ -77,4 +78,4 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
export default RequestBody;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.warning-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 10%;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.warning-description {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
.size-highlight {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.current-size {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => props.theme.colors.text.danger}15;
|
||||
}
|
||||
|
||||
.supported-size {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
background: ${(props) => props.theme.colors.text.yellow}15;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background: ${(props) => props.theme.button.secondary.bg};
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { IconDownload, IconCopy, IconEye, IconAlertTriangle } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { formatSize } from 'utils/common/index';
|
||||
|
||||
const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
|
||||
.then(() => {
|
||||
toast.success('Response saved to file');
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const copyResponse = () => {
|
||||
try {
|
||||
const textToCopy = typeof response.data === 'string'
|
||||
? response.data
|
||||
: JSON.stringify(response.data, null, 2);
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
toast.success('Response copied to clipboard');
|
||||
}).catch(() => {
|
||||
toast.error('Failed to copy response');
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy response');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="warning-container">
|
||||
<div className="warning-icon">
|
||||
<IconAlertTriangle size={45} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="warning-content">
|
||||
<div className="warning-title">
|
||||
Large Response Warning
|
||||
</div>
|
||||
<div className="warning-description">
|
||||
Handling responses over <span className="size-highlight supported-size">{formatSize(10 * 1024 * 1024)}</span> could degrade performance.
|
||||
<br />
|
||||
Size of current response: <span className="size-highlight current-size">{formatSize(responseSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="warning-actions">
|
||||
<button
|
||||
className="btn-reveal"
|
||||
onClick={onRevealResponse}
|
||||
title="Show response content"
|
||||
>
|
||||
<IconEye size={18} strokeWidth={1.5} />
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="btn-save"
|
||||
onClick={saveResponseToFile}
|
||||
disabled={!response.dataBuffer}
|
||||
title="Save response to file"
|
||||
>
|
||||
<IconDownload size={18} strokeWidth={1.5} />
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn-copy"
|
||||
onClick={copyResponse}
|
||||
disabled={!response.data}
|
||||
title="Copy response to clipboard"
|
||||
>
|
||||
<IconCopy size={18} strokeWidth={1.5} />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeResponseWarning;
|
||||
@@ -17,7 +17,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
<StopWatch requestTimestamp={item?.requestSent?.timestamp} />
|
||||
<StopWatch startTime={item?.requestStartTime} />
|
||||
</div>
|
||||
</div>
|
||||
<IconRefresh size={24} className="loading-icon" />
|
||||
|
||||
@@ -11,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
|
||||
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (data === undefined || !dataBuffer || !mode) {
|
||||
@@ -77,6 +78,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const [showLargeResponse, setShowLargeResponse] = useState(false);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
const formattedData = useMemo(
|
||||
() => formatResponse(data, dataBuffer, responseEncoding, mode, filter),
|
||||
@@ -84,6 +86,25 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const responseSize = useMemo(() => {
|
||||
const response = item.response || {};
|
||||
if (typeof response.size === 'number') {
|
||||
return response.size;
|
||||
}
|
||||
|
||||
if (!dataBuffer) return 0;
|
||||
|
||||
try {
|
||||
// dataBuffer is base64 encoded, so we need to calculate the actual size
|
||||
const buffer = Buffer.from(dataBuffer, 'base64');
|
||||
return buffer.length;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}, [dataBuffer, item.response]);
|
||||
|
||||
const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
setFilter(e.target.value);
|
||||
}, 250);
|
||||
@@ -160,6 +181,12 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : isLargeResponse && !showLargeResponse ? (
|
||||
<LargeResponseWarning
|
||||
item={item}
|
||||
responseSize={responseSize}
|
||||
onRevealResponse={() => setShowLargeResponse(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 relative">
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import ResponseSize from './index';
|
||||
|
||||
// Create minimal theme with only the properties needed for the component
|
||||
const theme = {
|
||||
requestTabPanel: {
|
||||
responseStatus: '#666'
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap component with theme provider for styled-components
|
||||
const renderWithTheme = (component) => {
|
||||
return render(
|
||||
<ThemeProvider theme={theme}>
|
||||
{component}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('ResponseSize', () => {
|
||||
describe('Invalid or excluded size values', () => {
|
||||
it('should not render when size is undefined', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={undefined} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is null', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={null} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is NaN', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={NaN} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is Infinity', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={Infinity} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is -Infinity', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={-Infinity} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is a string', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size="1024" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render when size is an object', () => {
|
||||
const { container } = renderWithTheme(<ResponseSize size={{value: 1024}} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Valid size values', () => {
|
||||
it('should handle zero bytes', () => {
|
||||
renderWithTheme(<ResponseSize size={0} />);
|
||||
const element = screen.getByText(/0B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^0B$/);
|
||||
expect(element).toHaveAttribute('title', '0B');
|
||||
});
|
||||
|
||||
it('should render bytes when size is less than 1024', () => {
|
||||
renderWithTheme(<ResponseSize size={500} />);
|
||||
const element = screen.getByText(/500B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^500B$/);
|
||||
expect(element).toHaveAttribute('title', '500B');
|
||||
});
|
||||
|
||||
it('should handle exactly 1024 bytes as size', () => {
|
||||
renderWithTheme(<ResponseSize size={1024} />);
|
||||
const element = screen.getByText(/1024B/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^1024B$/);
|
||||
expect(element).toHaveAttribute('title', '1,024B');
|
||||
});
|
||||
|
||||
it('should render kilobytes when size is greater than 1024', () => {
|
||||
renderWithTheme(<ResponseSize size={1500} />);
|
||||
const element = screen.getByText(/1\.46KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', '1,500B');
|
||||
});
|
||||
|
||||
it('should handle large size numbers', () => {
|
||||
renderWithTheme(<ResponseSize size={10240} />);
|
||||
const element = screen.getByText(/10\.0KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', '10,240B');
|
||||
});
|
||||
|
||||
it('should handle decimal size numbers', () => {
|
||||
renderWithTheme(<ResponseSize size={1126.5} />);
|
||||
const element = screen.getByText(/1\.10KB/);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.textContent).toMatch(/^\d+\.\d+KB$/);
|
||||
expect(element).toHaveAttribute('title', '1,126.5B');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,22 +6,48 @@ import StyledWrapper from './StyledWrapper';
|
||||
const ScriptError = ({ item, onClose }) => {
|
||||
const preRequestError = item?.preRequestScriptErrorMessage;
|
||||
const postResponseError = item?.postResponseScriptErrorMessage;
|
||||
const testScriptError = item?.testScriptErrorMessage;
|
||||
|
||||
if (!preRequestError && !postResponseError) return null;
|
||||
if (!preRequestError && !postResponseError && !testScriptError) return null;
|
||||
|
||||
const errorMessage = preRequestError || postResponseError;
|
||||
const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error';
|
||||
const errors = [];
|
||||
|
||||
if (preRequestError) {
|
||||
errors.push({
|
||||
title: 'Pre-Request Script Error',
|
||||
message: preRequestError
|
||||
});
|
||||
}
|
||||
|
||||
if (postResponseError) {
|
||||
errors.push({
|
||||
title: 'Post-Response Script Error',
|
||||
message: postResponseError
|
||||
});
|
||||
}
|
||||
|
||||
if (testScriptError) {
|
||||
errors.push({
|
||||
title: 'Test Script Error',
|
||||
message: testScriptError
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">
|
||||
{errorTitle}
|
||||
</div>
|
||||
<div className="error-message">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{errors.map((error, index) => (
|
||||
<div key={index}>
|
||||
{index > 0 && <div className="border-t border-gray-300 my-3 dark:border-gray-600"></div>}
|
||||
<div className="error-title">
|
||||
{error.title}
|
||||
</div>
|
||||
<div className="error-message">
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="close-button flex-shrink-0 cursor-pointer"
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.test-summary {
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
@@ -9,12 +21,24 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.test-success-count {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure-count {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
.test-results-list {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,63 +1,151 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconCircleCheck,
|
||||
IconCircleX
|
||||
} from '@tabler/icons';
|
||||
|
||||
const TestResults = ({ results, assertionResults }) => {
|
||||
const ResultIcon = ({ status }) => (
|
||||
<span className={`inline-flex items-center ${status === 'pass' ? 'test-success' : 'test-failure'}`}>
|
||||
{status === 'pass' ? (
|
||||
<IconCircleCheck size={14} className="mr-1" aria-label="Test passed" />
|
||||
) : (
|
||||
<IconCircleX size={14} className="mr-1" aria-label="Test failed" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
const ErrorMessage = ({ error }) => error && (
|
||||
<>
|
||||
<br />
|
||||
<span className="error-message pl-8" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const ResultItem = ({ result, type }) => (
|
||||
<div className="test-result-item">
|
||||
<ResultIcon status={result.status} />
|
||||
<span className={result.status === 'pass' ? 'test-success' : 'test-failure'}>
|
||||
{type === 'assertion'
|
||||
? `${result.lhsExpr}: ${result.rhsExpr}`
|
||||
: result.description
|
||||
}
|
||||
</span>
|
||||
<ErrorMessage error={result.error} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const TestSection = ({
|
||||
title,
|
||||
results,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
type = 'test'
|
||||
}) => {
|
||||
const passedResults = results.filter((result) => result.status === 'pass');
|
||||
const failedResults = results.filter((result) => result.status === 'fail');
|
||||
|
||||
if (results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<div
|
||||
className="font-medium test-summary flex items-center cursor-pointer hover:bg-opacity-10 hover:bg-gray-500 rounded py-2"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className="dropdown-icon mr-2 flex items-center">
|
||||
{isExpanded ?
|
||||
<IconChevronDown size={18} stroke={1.5} /> :
|
||||
<IconChevronRight size={18} stroke={1.5} />
|
||||
}
|
||||
</span>
|
||||
<span className="flex-grow">
|
||||
{title} ({results.length}), Passed: {passedResults.length}, Failed: {failedResults.length}
|
||||
</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<ul className="ml-5">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
<ResultItem result={result} type={type} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TestResults = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
preRequestTestResults = preRequestTestResults || [];
|
||||
postResponseTestResults = postResponseTestResults || [];
|
||||
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
preRequest: true,
|
||||
tests: true,
|
||||
postResponse: true,
|
||||
assertions: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedSections({
|
||||
preRequest: preRequestTestResults.length > 0,
|
||||
tests: results.length > 0,
|
||||
postResponse: postResponseTestResults.length > 0,
|
||||
assertions: assertionResults.length > 0
|
||||
});
|
||||
}, [results.length, assertionResults.length, preRequestTestResults.length, postResponseTestResults.length]);
|
||||
|
||||
const toggleSection = (section) => {
|
||||
setExpandedSections({
|
||||
...expandedSections,
|
||||
[section]: !expandedSections[section]
|
||||
});
|
||||
};
|
||||
|
||||
if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {
|
||||
return <div className="px-3">No tests found</div>;
|
||||
}
|
||||
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
const passedAssertions = assertionResults.filter((result) => result.status === 'pass');
|
||||
const failedAssertions = assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">✔ {result.description}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">✘ {result.description}</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<StyledWrapper className="flex flex-col px-3">
|
||||
<TestSection
|
||||
title="Pre-Request Tests"
|
||||
results={preRequestTestResults}
|
||||
isExpanded={expandedSections.preRequest}
|
||||
onToggle={() => toggleSection('preRequest')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Assertions ({assertionResults.length}/{assertionResults.length}), Passed: {passedAssertions.length}, Failed:{' '}
|
||||
{failedAssertions.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{assertionResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TestSection
|
||||
title="Post-Response Tests"
|
||||
results={postResponseTestResults}
|
||||
isExpanded={expandedSections.postResponse}
|
||||
onToggle={() => toggleSection('postResponse')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<TestSection
|
||||
title="Tests"
|
||||
results={results}
|
||||
isExpanded={expandedSections.tests}
|
||||
onToggle={() => toggleSection('tests')}
|
||||
type="test"
|
||||
/>
|
||||
|
||||
<TestSection
|
||||
title="Assertions"
|
||||
results={assertionResults}
|
||||
isExpanded={expandedSections.assertions}
|
||||
onToggle={() => toggleSection('assertions')}
|
||||
type="assertion"
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react';
|
||||
import { IconCircleCheck, IconCircleX } from '@tabler/icons';
|
||||
|
||||
const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
const TestResultsLabel = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {
|
||||
results = results || [];
|
||||
assertionResults = assertionResults || [];
|
||||
if (!results.length && !assertionResults.length) {
|
||||
preRequestTestResults = preRequestTestResults || [];
|
||||
postResponseTestResults = postResponseTestResults || [];
|
||||
|
||||
if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
@@ -13,8 +17,14 @@ const TestResultsLabel = ({ results, assertionResults }) => {
|
||||
const numberOfAssertions = assertionResults.length;
|
||||
const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions;
|
||||
const numberOfPreRequestTests = preRequestTestResults.length;
|
||||
const numberOfFailedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const numberOfPostResponseTests = postResponseTestResults.length;
|
||||
const numberOfFailedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail').length;
|
||||
|
||||
const totalNumberOfTests = numberOfTests + numberOfAssertions + numberOfPreRequestTests + numberOfPostResponseTests;
|
||||
const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions + numberOfFailedPreRequestTests + numberOfFailedPostResponseTests;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -33,10 +33,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
}
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]);
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -73,7 +73,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
return <TestResults
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -122,8 +127,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
@@ -139,14 +144,19 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} assertionResults={item.assertionResults} />
|
||||
<TestResultsLabel
|
||||
results={item.testResults}
|
||||
assertionResults={item.assertionResults}
|
||||
preRequestTestResults={item.preRequestTestResults}
|
||||
postResponseTestResults={item.postResponseTestResults}
|
||||
/>
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{hasScriptError && !showScriptErrorCard && (
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
@@ -164,26 +174,32 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
className={`flex flex-col min-h-0 relative pl-3 pr-4 auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
}}
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
<ScriptError
|
||||
item={item}
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
width={rightPaneWidth}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
<div className='flex-1 min-h-[200px]'>
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
width={rightPaneWidth}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const { requestSent, responseReceived, testResults, assertionResults, error } = item;
|
||||
const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', []);
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
@@ -49,7 +49,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <RunnerTimeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
return <TestResults
|
||||
results={testResults}
|
||||
assertionResults={assertionResults}
|
||||
preRequestTestResults={preRequestTestResults}
|
||||
postResponseTestResults={postResponseTestResults}
|
||||
/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -86,7 +91,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} assertionResults={assertionResults} />
|
||||
<TestResultsLabel
|
||||
results={testResults}
|
||||
assertionResults={assertionResults}
|
||||
preRequestTestResults={preRequestTestResults}
|
||||
postResponseTestResults={postResponseTestResults}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
|
||||
@@ -16,6 +16,28 @@ const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
return path.join(dir, name);
|
||||
};
|
||||
|
||||
const getTestStatus = (results) => {
|
||||
if (!results || !results.length) return 'pass';
|
||||
const failed = results.filter((result) => result.status === 'fail');
|
||||
return failed.length ? 'fail' : 'pass';
|
||||
};
|
||||
|
||||
const allTestsPassed = (item) => {
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
};
|
||||
|
||||
const anyTestFailed = (item) => {
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
@@ -56,19 +78,10 @@ export default function RunnerResults({ collection }) {
|
||||
displayName: getDisplayName(collection.pathname, info.pathname, info.name)
|
||||
};
|
||||
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
|
||||
if (newItem.testResults) {
|
||||
const failed = newItem.testResults.filter((result) => result.status === 'fail');
|
||||
newItem.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.testStatus = 'pass';
|
||||
}
|
||||
|
||||
if (newItem.assertionResults) {
|
||||
const failed = newItem.assertionResults.filter((result) => result.status === 'fail');
|
||||
newItem.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.assertionStatus = 'pass';
|
||||
}
|
||||
newItem.testStatus = getTestStatus(newItem.testResults);
|
||||
newItem.assertionStatus = getTestStatus(newItem.assertionResults);
|
||||
newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults);
|
||||
newItem.postResponseTestStatus = getTestStatus(newItem.postResponseTestResults);
|
||||
}
|
||||
return newItem;
|
||||
})
|
||||
@@ -95,12 +108,8 @@ export default function RunnerResults({ collection }) {
|
||||
};
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter((item) => {
|
||||
return item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass';
|
||||
});
|
||||
const failedRequests = items.filter((item) => {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
@@ -176,18 +185,18 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.testStatus === 'pass' && item.assertionStatus === 'pass' ?
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ?
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ? 'danger' : ''}`}
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
@@ -208,6 +217,46 @@ export default function RunnerResults({ collection }) {
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
@@ -271,10 +320,10 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' && selectedItem.assertionStatus === 'pass' ?
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'error' || selectedItem.testStatus === 'fail' || selectedItem.assertionStatus === 'fail' ?
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
|
||||
@@ -4,12 +4,60 @@ import CodeView from './CodeView';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { get } from 'lodash';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Function to resolve inherited auth
|
||||
const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return {
|
||||
...request
|
||||
};
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let source = 'collection';
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
source = 'folder';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
@@ -46,6 +94,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
@@ -94,16 +145,10 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
request:
|
||||
item.request.url !== ''
|
||||
? {
|
||||
...item.request,
|
||||
url: finalUrl
|
||||
}
|
||||
: {
|
||||
...item.draft.request,
|
||||
url: finalUrl
|
||||
}
|
||||
request: {
|
||||
...resolvedRequest,
|
||||
url: finalUrl
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -83,7 +83,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
@@ -107,7 +107,7 @@ class SingleLineEditor extends Component {
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onChange) {
|
||||
if (this.props.onChange && (this.props.value !== this.cachedValue)) {
|
||||
this.props.onChange(this.cachedValue);
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
@@ -146,7 +146,7 @@ class SingleLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const StopWatch = () => {
|
||||
const [milliseconds, setMilliseconds] = useState(0);
|
||||
|
||||
const tickInterval = 100;
|
||||
const tick = () => {
|
||||
setMilliseconds(_milliseconds => _milliseconds + tickInterval);
|
||||
};
|
||||
|
||||
const StopWatch = ({ startTime }) => {
|
||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
let timerID = setInterval(() => {
|
||||
tick()
|
||||
}, tickInterval);
|
||||
return () => {
|
||||
clearTimeout(timerID);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (milliseconds < 250) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
if (!startTime) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [startTime]);
|
||||
|
||||
if (!startTime) return <span>Loading...</span>;
|
||||
|
||||
const elapsedTime = currentTime - startTime;
|
||||
if (elapsedTime < 250) return <span>Loading...</span>;
|
||||
|
||||
const seconds = elapsedTime / 1000;
|
||||
return <span>{seconds.toFixed(1)}s</span>;
|
||||
};
|
||||
|
||||
|
||||
@@ -211,13 +211,15 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
const currentCollection = getCurrentCollection();
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showEnvSettingsModal && (
|
||||
<EnvironmentSettings collection={getCurrentCollection()} onClose={() => setShowEnvSettingsModal(false)} />
|
||||
<EnvironmentSettings collection={currentCollection} onClose={() => setShowEnvSettingsModal(false)} />
|
||||
)}
|
||||
{showNewRequestModal && (
|
||||
<NewRequest collection={getCurrentCollection()} onClose={() => setShowNewRequestModal(false)} />
|
||||
<NewRequest collectionUid={currentCollection?.uid} onClose={() => setShowNewRequestModal(false)} />
|
||||
)}
|
||||
<div>{props.children}</div>
|
||||
</HotkeysContext.Provider>
|
||||
|
||||
@@ -36,7 +36,8 @@ import {
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
initRunRequestEvent
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -220,15 +221,26 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const itemUid = item?.uid;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const itemCopy = cloneDeep(item || {});
|
||||
|
||||
let collectionCopy = cloneDeep(collection);
|
||||
|
||||
const itemCopy = cloneDeep(item);
|
||||
|
||||
const requestUid = uuid();
|
||||
itemCopy.requestUid = requestUid;
|
||||
|
||||
await dispatch(initRunRequestEvent({
|
||||
requestUid,
|
||||
itemUid,
|
||||
collectionUid
|
||||
}));
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
@@ -247,8 +259,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: serializedResponse
|
||||
})
|
||||
);
|
||||
@@ -259,8 +271,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
console.log('>> request cancelled');
|
||||
dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: null
|
||||
})
|
||||
);
|
||||
@@ -277,8 +289,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: errorResponse
|
||||
})
|
||||
);
|
||||
@@ -381,7 +393,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
@@ -417,7 +434,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
|
||||
@@ -276,6 +276,8 @@ export const collectionsSlice = createSlice({
|
||||
if (item) {
|
||||
item.response = null;
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -288,6 +290,7 @@ export const collectionsSlice = createSlice({
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
@@ -1593,6 +1596,27 @@ export const collectionsSlice = createSlice({
|
||||
case 'oauth2':
|
||||
set(folder, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'basic':
|
||||
set(folder, 'root.request.auth.basic', action.payload.content);
|
||||
break;
|
||||
case 'bearer':
|
||||
set(folder, 'root.request.auth.bearer', action.payload.content);
|
||||
break;
|
||||
case 'digest':
|
||||
set(folder, 'root.request.auth.digest', action.payload.content);
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(folder, 'root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(folder, 'root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
case 'awsv4':
|
||||
set(folder, 'root.request.auth.awsv4', action.payload.content);
|
||||
break;
|
||||
case 'wsse':
|
||||
set(folder, 'root.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1933,26 +1957,44 @@ export const collectionsSlice = createSlice({
|
||||
collection.runnerResult = null;
|
||||
}
|
||||
},
|
||||
initRunRequestEvent: (state, action) => {
|
||||
const { requestUid, itemUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item) return;
|
||||
|
||||
item.requestState = null;
|
||||
item.requestUid = requestUid;
|
||||
item.requestStartTime = Date.now();
|
||||
},
|
||||
runRequestEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, type, requestUid, hasError } = action.payload;
|
||||
const { itemUid, collectionUid, type, requestUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
// ignore outdated updates in case multiple requests are fired rapidly to avoid state inconsistency
|
||||
if (item.requestUid !== requestUid) return;
|
||||
|
||||
if (type === 'pre-request-script-execution') {
|
||||
item.requestUid = requestUid;
|
||||
item.preRequestScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'post-response-script-execution') {
|
||||
item.requestUid = requestUid;
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'test-script-execution') {
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'request-queued') {
|
||||
const { cancelTokenUid } = action.payload;
|
||||
item.requestUid = requestUid;
|
||||
// ignore if request is already in progress or completed
|
||||
if (['sending', 'received'].includes(item.requestState)) return;
|
||||
item.requestState = 'queued';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
@@ -1960,10 +2002,9 @@ export const collectionsSlice = createSlice({
|
||||
if (type === 'request-sent') {
|
||||
const { cancelTokenUid, requestSent } = action.payload;
|
||||
item.requestSent = requestSent;
|
||||
|
||||
|
||||
// sometimes the response is received before the request-sent event arrives
|
||||
if (item.requestUid === requestUid && item.requestState === 'queued') {
|
||||
item.requestUid = requestUid;
|
||||
if (item.requestState === 'queued') {
|
||||
item.requestState = 'sending';
|
||||
item.cancelTokenUid = cancelTokenUid;
|
||||
}
|
||||
@@ -1978,6 +2019,16 @@ export const collectionsSlice = createSlice({
|
||||
const { results } = action.payload;
|
||||
item.testResults = results;
|
||||
}
|
||||
|
||||
if (type === 'test-results-pre-request') {
|
||||
const { results } = action.payload;
|
||||
item.preRequestTestResults = results;
|
||||
}
|
||||
|
||||
if (type === 'test-results-post-response') {
|
||||
const { results } = action.payload;
|
||||
item.postResponseTestResults = results;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2034,6 +2085,16 @@ export const collectionsSlice = createSlice({
|
||||
item.testResults = action.payload.testResults;
|
||||
}
|
||||
|
||||
if (type === 'test-results-pre-request') {
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.preRequestTestResults = action.payload.preRequestTestResults;
|
||||
}
|
||||
|
||||
if (type === 'test-results-post-response') {
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.postResponseTestResults = action.payload.postResponseTestResults;
|
||||
}
|
||||
|
||||
if (type === 'assertion-results') {
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.assertionResults = action.payload.assertionResults;
|
||||
@@ -2165,6 +2226,7 @@ export const collectionsSlice = createSlice({
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
@@ -2173,8 +2235,9 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export const {
|
||||
@@ -2275,17 +2338,18 @@ export const {
|
||||
collectionAddEnvFileEvent,
|
||||
collectionRenamedEvent,
|
||||
resetRunResults,
|
||||
initRunRequestEvent,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
moveCollection
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -7,7 +7,7 @@ const lightTheme = {
|
||||
colors: {
|
||||
text: {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
danger: '#B91C1C',
|
||||
muted: '#838383',
|
||||
purple: '#8e44ad',
|
||||
yellow: '#d97706'
|
||||
|
||||
@@ -74,11 +74,11 @@ export class MaskedEditor {
|
||||
} else {
|
||||
for (let line = 0; line < lineCount; line++) {
|
||||
const lineLength = this.editor.getLine(line).length;
|
||||
const maskedNode = document.createTextNode('*'.repeat(lineLength));
|
||||
const maskedNode = document.createTextNode('*'.repeat(lineLength));
|
||||
this.editor.markText(
|
||||
{ line, ch: 0 },
|
||||
{ line, ch: lineLength },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: false }
|
||||
{ replacedWith: maskedNode, handleMouseEvents: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,18 @@ export class MaskedEditor {
|
||||
};
|
||||
}
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
||||
/**
|
||||
* Defines a custom CodeMirror mode for Bruno variables highlighting.
|
||||
* This function creates a specialized mode that can highlight both Bruno template
|
||||
* variables (in the format {{variable}}) and URL path parameters (in the format /:param).
|
||||
*
|
||||
* @param {Object} _variables - The variables object containing data to validate against
|
||||
* @param {string} mode - The base CodeMirror mode to extend (e.g., 'javascript', 'application/json')
|
||||
* @param {boolean} highlightPathParams - Whether to highlight URL path parameters
|
||||
* @param {boolean} highlightVariables - Whether to highlight template variables
|
||||
* @returns {void} - Registers the mode with CodeMirror for later use
|
||||
*/
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams, highlightVariables) => {
|
||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||
const { pathParams = {}, ...variables } = _variables || {};
|
||||
const variablesOverlay = {
|
||||
@@ -139,13 +150,15 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa
|
||||
}
|
||||
};
|
||||
|
||||
let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
let baseMode = CodeMirror.getMode(config, parserConfig.backdrop || mode);
|
||||
|
||||
if (highlightPathParams) {
|
||||
return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
} else {
|
||||
return baseMode;
|
||||
if (highlightVariables) {
|
||||
baseMode = CodeMirror.overlayMode(baseMode, variablesOverlay);
|
||||
}
|
||||
if (highlightPathParams) {
|
||||
baseMode = CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
}
|
||||
return baseMode;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -83,29 +83,40 @@ export const normalizeFileName = (name) => {
|
||||
};
|
||||
|
||||
export const getContentType = (headers) => {
|
||||
const headersArray = typeof headers === 'object' ? Object.entries(headers) : [];
|
||||
|
||||
if (headersArray.length > 0) {
|
||||
let contentType = headersArray
|
||||
.filter((header) => header[0].toLowerCase() === 'content-type')
|
||||
.map((header) => {
|
||||
return header[1];
|
||||
});
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) {
|
||||
return 'image/svg+xml';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType[0];
|
||||
}
|
||||
// Return empty string for invalid headers
|
||||
if (!headers || typeof headers !== 'object' || Object.keys(headers).length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
// Get content-type header value
|
||||
const contentTypeHeader = Object.entries(headers)
|
||||
.find(([key]) => key.toLowerCase() === 'content-type');
|
||||
|
||||
const contentType = contentTypeHeader && contentTypeHeader[1];
|
||||
|
||||
// Return empty string if no content-type or not a string
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return '';
|
||||
}
|
||||
// This pattern matches content types like application/json, application/ld+json, text/json, etc.
|
||||
const JSON_PATTERN = /^[\w\-]+\/([\w\-]+\+)?json/;
|
||||
// This pattern matches content types like image/svg.
|
||||
const SVG_PATTERN = /^image\/svg/i;
|
||||
// This pattern matches content types like application/xml, text/xml, application/atom+xml, etc.
|
||||
const XML_PATTERN = /^[\w\-]+\/([\w\-]+\+)?xml/;
|
||||
|
||||
if (JSON_PATTERN.test(contentType)) {
|
||||
return 'application/ld+json';
|
||||
} else if (SVG_PATTERN.test(contentType)) {
|
||||
return 'image/svg+xml';
|
||||
} else if (XML_PATTERN.test(contentType)) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
|
||||
export const startsWith = (str, search) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
@@ -185,4 +196,23 @@ export const getEncoding = (headers) => {
|
||||
|
||||
export const multiLineMsg = (...messages) => {
|
||||
return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n');
|
||||
}
|
||||
|
||||
export const formatSize = (bytes) => {
|
||||
// Handle invalid inputs
|
||||
if (isNaN(bytes) || typeof bytes !== 'number') {
|
||||
return '0B';
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return bytes + 'B';
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return (bytes / 1024).toFixed(1) + 'KB';
|
||||
}
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
|
||||
}
|
||||
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType, formatSize } from './index';
|
||||
|
||||
describe('common utils', () => {
|
||||
describe('normalizeFileName', () => {
|
||||
@@ -107,4 +107,81 @@ describe('common utils', () => {
|
||||
expect(relativeDate(date)).toBe('2 months ago');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentType', () => {
|
||||
it('should handle JSON content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'application/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'content-type': 'application/ld+json' })).toBe('application/ld+json');
|
||||
});
|
||||
|
||||
it('should handle XML content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'text/xml' })).toBe('application/xml');
|
||||
expect(getContentType({ 'content-type': 'application/xml' })).toBe('application/xml');
|
||||
expect(getContentType({ 'content-type': 'application/atom+xml' })).toBe('application/xml');
|
||||
});
|
||||
|
||||
it('should handle image content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'image/svg+xml;charset=utf-8' })).toBe('image/svg+xml');
|
||||
expect(getContentType({ 'content-type': 'IMAGE/SVG+xml' })).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should return original content type when no pattern matches', () => {
|
||||
expect(getContentType({ 'content-type': 'image/jpeg' })).toBe('image/jpeg');
|
||||
expect(getContentType({ 'content-type': 'application/pdf' })).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should not be case sensitive', () => {
|
||||
expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'Content-Type': 'text/json' })).toBe('application/ld+json');
|
||||
});
|
||||
|
||||
it('should handle empty content type', () => {
|
||||
expect(getContentType({ 'content-type': '' })).toBe('');
|
||||
expect(getContentType({ 'content-type': null })).toBe('');
|
||||
expect(getContentType({ 'content-type': undefined })).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty or invalid inputs', () => {
|
||||
expect(getContentType({})).toBe('');
|
||||
expect(getContentType(null)).toBe('');
|
||||
expect(getContentType(undefined)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSize', () => {
|
||||
it('should format bytes', () => {
|
||||
expect(formatSize(0)).toBe('0B');
|
||||
expect(formatSize(1023)).toBe('1023B');
|
||||
});
|
||||
|
||||
it('should format kilobytes', () => {
|
||||
expect(formatSize(1024)).toBe('1.0KB');
|
||||
expect(formatSize(1048575)).toBe('1024.0KB');
|
||||
});
|
||||
|
||||
it('should format megabytes', () => {
|
||||
expect(formatSize(1048576)).toBe('1.0MB');
|
||||
expect(formatSize(1073741823)).toBe('1024.0MB');
|
||||
});
|
||||
|
||||
it('should format gigabytes', () => {
|
||||
expect(formatSize(1073741824)).toBe('1.0GB');
|
||||
expect(formatSize(1099511627776)).toBe('1024.0GB');
|
||||
});
|
||||
|
||||
it('should format decimal values', () => {
|
||||
expect(formatSize(1126.5)).toBe('1.1KB');
|
||||
expect(formatSize(1153433.6)).toBe('1.1MB');
|
||||
expect(formatSize(1153433600)).toBe('1.1GB');
|
||||
expect(formatSize(1024.1)).toBe('1.0KB');
|
||||
expect(formatSize(1048576.1)).toBe('1.0MB');
|
||||
});
|
||||
|
||||
it('should format invalid inputs', () => {
|
||||
expect(formatSize(null)).toBe('0B');
|
||||
expect(formatSize(undefined)).toBe('0B');
|
||||
expect(formatSize(NaN)).toBe('0B');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,7 +183,13 @@ const curlToJson = (curlCommand) => {
|
||||
|
||||
if (request.query) {
|
||||
requestJson.queries = getQueries(request);
|
||||
} else if (request.multipartUploads || request.isDataBinary) {
|
||||
} else if (request.multipartUploads) {
|
||||
requestJson.data = request.multipartUploads;
|
||||
if (!requestJson.headers) {
|
||||
requestJson.headers = {};
|
||||
}
|
||||
requestJson.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else if (request.isDataBinary) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
Object.assign(requestJson, getDataString(request));
|
||||
|
||||
@@ -37,7 +37,8 @@ const parseCurlCommand = (curlCommand) => {
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent',
|
||||
u: 'user'
|
||||
u: 'user',
|
||||
F: 'form'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,17 +96,31 @@ const parseCurlCommand = (curlCommand) => {
|
||||
cookieString = parsedArguments.cookie;
|
||||
}
|
||||
let multipartUploads;
|
||||
if (parsedArguments.F) {
|
||||
multipartUploads = {};
|
||||
if (!Array.isArray(parsedArguments.F)) {
|
||||
parsedArguments.F = [parsedArguments.F];
|
||||
}
|
||||
parsedArguments.F.forEach((multipartArgument) => {
|
||||
// input looks like key=value. value could be json or a file path prepended with an @
|
||||
const splitArguments = multipartArgument.split('=', 2);
|
||||
const key = splitArguments[0];
|
||||
const value = splitArguments[1];
|
||||
multipartUploads[key] = value;
|
||||
// Handle multipart form data specified via -F or --form flags
|
||||
// Example: curl -F 'id=123' -F 'file=@/path/to/file.txt'
|
||||
if (parsedArguments.F || parsedArguments.form) {
|
||||
multipartUploads = [];
|
||||
const formArgs = parsedArguments.F || parsedArguments.form;
|
||||
const formArray = Array.isArray(formArgs) ? formArgs : [formArgs];
|
||||
|
||||
formArray.forEach((multipartArgument) => {
|
||||
// Parse each form field using regex:
|
||||
// - Group 1: Field name before =
|
||||
// - Group 2: Value in quotes after = (for text fields)
|
||||
// - Group 3: Value after @ (for file fields)
|
||||
const match = multipartArgument.match(/^([^=]+)=(?:@?"([^"]*)"|([^@]*))?$/);
|
||||
if (match) {
|
||||
const key = match[1];
|
||||
const value = match[2] || match[3] || '';
|
||||
const isFile = multipartArgument.includes('@');
|
||||
|
||||
multipartUploads.push({
|
||||
name: key,
|
||||
value: value,
|
||||
type: isFile ? 'file' : 'text',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (cookieString) {
|
||||
|
||||
145
packages/bruno-app/src/utils/curl/parse-curl.spec.js
Normal file
145
packages/bruno-app/src/utils/curl/parse-curl.spec.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
import parseCurlCommand from './parse-curl';
|
||||
|
||||
describe('parseCurlCommand', () => {
|
||||
describe('basic functionality', () => {
|
||||
it('should handle basic GET request', () => {
|
||||
const result = parseCurlCommand('curl https://api.example.com/users');
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/users',
|
||||
urlWithoutQuery: 'https://api.example.com/users',
|
||||
method: 'get'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse explicit POST method', () => {
|
||||
const result = parseCurlCommand('curl -X POST https://api.example.com/users');
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/users',
|
||||
urlWithoutQuery: 'https://api.example.com/users',
|
||||
method: 'post'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('headers handling', () => {
|
||||
it('should parse multiple headers', () => {
|
||||
const result = parseCurlCommand(
|
||||
`curl -H 'Content-Type: application/json' -H 'Authorization: Bearer token' https://api.example.com`
|
||||
);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse user-agent', () => {
|
||||
const result = parseCurlCommand(`curl -A 'Custom Agent' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
headers: {
|
||||
'User-Agent': 'Custom Agent'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth handling', () => {
|
||||
it('should parse basic auth', () => {
|
||||
const result = parseCurlCommand(`curl -u user:pass https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'user',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data handling', () => {
|
||||
it('should parse POST data', () => {
|
||||
const result = parseCurlCommand(`curl -d 'foo=bar&baz=qux' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'post',
|
||||
data: 'foo=bar&baz=qux'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle data-binary', () => {
|
||||
const result = parseCurlCommand(`curl --data-binary '@file.json' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'post',
|
||||
data: '@file.json',
|
||||
isDataBinary: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('form data handling', () => {
|
||||
it('should parse complex form data with multiple fields and file upload', () => {
|
||||
const curlCommand = `curl --location 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d' \
|
||||
--form 'id="1"' \
|
||||
--form 'documentid="ADMINN_ID"' \
|
||||
--form 'appoinID="12376"' \
|
||||
--form 'autoclose="false"' \
|
||||
--form 'fileData=@"/path/to/file"'`;
|
||||
|
||||
const result = parseCurlCommand(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d',
|
||||
urlWithoutQuery: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d',
|
||||
method: 'post',
|
||||
multipartUploads: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '1',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'documentid',
|
||||
value: 'ADMINN_ID',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'appoinID',
|
||||
value: '12376',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'autoclose',
|
||||
value: 'false',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'fileData',
|
||||
value: '/path/to/file',
|
||||
type: 'file',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,23 @@ const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname } = require('../utils/collection');
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection');
|
||||
const command = 'run [paths...]';
|
||||
const desc = 'Run one or more requests/folders';
|
||||
|
||||
const formatTestSummary = (label, maxLength, passed, failed, total, errorCount = 0, skippedCount = 0) => {
|
||||
const parts = [
|
||||
`${rpad(label, maxLength)} ${chalk.green(`${passed} passed`)}`
|
||||
];
|
||||
|
||||
if (failed > 0) parts.push(chalk.red(`${failed} failed`));
|
||||
if (errorCount > 0) parts.push(chalk.red(`${errorCount} error`));
|
||||
if (skippedCount > 0) parts.push(chalk.magenta(`${skippedCount} skipped`));
|
||||
|
||||
parts.push(`${total} total`);
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
|
||||
const printRunSummary = (results) => {
|
||||
const {
|
||||
@@ -28,38 +42,40 @@ const printRunSummary = (results) => {
|
||||
failedAssertions,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
failedTests,
|
||||
totalPreRequestTests,
|
||||
passedPreRequestTests,
|
||||
failedPreRequestTests,
|
||||
totalPostResponseTests,
|
||||
passedPostResponseTests,
|
||||
failedPostResponseTests
|
||||
} = getRunnerSummary(results);
|
||||
|
||||
const maxLength = 12;
|
||||
|
||||
let requestSummary = `${rpad('Requests:', maxLength)} ${chalk.green(`${passedRequests} passed`)}`;
|
||||
if (failedRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
|
||||
}
|
||||
if (errorRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${errorRequests} error`)}`;
|
||||
}
|
||||
if (skippedRequests > 0) {
|
||||
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
|
||||
}
|
||||
requestSummary += `, ${totalRequests} total`;
|
||||
const requestSummary = formatTestSummary('Requests:', maxLength, passedRequests, failedRequests, totalRequests, errorRequests, skippedRequests);
|
||||
const testSummary = formatTestSummary('Tests:', maxLength, passedTests, failedTests, totalTests);
|
||||
const assertSummary = formatTestSummary('Assertions:', maxLength, passedAssertions, failedAssertions, totalAssertions);
|
||||
|
||||
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
|
||||
if (failedTests > 0) {
|
||||
assertSummary += `, ${chalk.red(`${failedTests} failed`)}`;
|
||||
let preRequestTestSummary = '';
|
||||
if (totalPreRequestTests > 0) {
|
||||
preRequestTestSummary = formatTestSummary('Pre-Request Tests:', maxLength, passedPreRequestTests, failedPreRequestTests, totalPreRequestTests);
|
||||
}
|
||||
assertSummary += `, ${totalTests} total`;
|
||||
|
||||
let testSummary = `${rpad('Assertions:', maxLength)} ${chalk.green(`${passedAssertions} passed`)}`;
|
||||
if (failedAssertions > 0) {
|
||||
testSummary += `, ${chalk.red(`${failedAssertions} failed`)}`;
|
||||
let postResponseTestSummary = '';
|
||||
if (totalPostResponseTests > 0) {
|
||||
postResponseTestSummary = formatTestSummary('Post-Response Tests:', maxLength, passedPostResponseTests, failedPostResponseTests, totalPostResponseTests);
|
||||
}
|
||||
testSummary += `, ${totalAssertions} total`;
|
||||
|
||||
console.log('\n' + chalk.bold(requestSummary));
|
||||
console.log(chalk.bold(assertSummary));
|
||||
if (preRequestTestSummary) {
|
||||
console.log(chalk.bold(preRequestTestSummary));
|
||||
}
|
||||
if (postResponseTestSummary) {
|
||||
console.log(chalk.bold(postResponseTestSummary));
|
||||
}
|
||||
console.log(chalk.bold(testSummary));
|
||||
console.log(chalk.bold(assertSummary));
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
@@ -72,7 +88,13 @@ const printRunSummary = (results) => {
|
||||
failedAssertions,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
failedTests,
|
||||
totalPreRequestTests,
|
||||
passedPreRequestTests,
|
||||
failedPreRequestTests,
|
||||
totalPostResponseTests,
|
||||
passedPostResponseTests,
|
||||
failedPostResponseTests
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +128,10 @@ const builder = async (yargs) => {
|
||||
describe: 'Environment variables',
|
||||
type: 'string'
|
||||
})
|
||||
.option('env-file', {
|
||||
describe: 'Path to environment file (.bru) - can be absolute or relative path',
|
||||
type: 'string'
|
||||
})
|
||||
.option('env-var', {
|
||||
describe: 'Overwrite a single environment variable, multiple usages possible',
|
||||
type: 'string'
|
||||
@@ -164,14 +190,21 @@ const builder = async (yargs) => {
|
||||
type: 'string',
|
||||
description: 'Path to the Client certificate config file used for securing the connection in the request'
|
||||
})
|
||||
.option('--noproxy', {
|
||||
type: 'boolean',
|
||||
description: 'Disable all proxy settings (both collection-defined and system proxies)',
|
||||
default: false
|
||||
})
|
||||
.option('delay', {
|
||||
type:"number",
|
||||
description: "Delay between each requests (in miliseconds)"
|
||||
})
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
.example('$0 run folder -r', 'Run all requests in a folder recursively')
|
||||
.example('$0 run request.bru folder', 'Run a request and all requests in a folder')
|
||||
.example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output')
|
||||
.example(
|
||||
'$0 run --reporter-skip-headers "Authorization"',
|
||||
@@ -197,7 +230,6 @@ const builder = async (yargs) => {
|
||||
'$0 run request.bru --reporter-junit results.xml --reporter-html results.html',
|
||||
'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory'
|
||||
)
|
||||
|
||||
.example('$0 run request.bru --tests-only', 'Run all requests that have a test')
|
||||
.example(
|
||||
'$0 run request.bru --cacert myCustomCA.pem',
|
||||
@@ -208,17 +240,19 @@ const builder = async (yargs) => {
|
||||
'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'
|
||||
)
|
||||
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
|
||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.');
|
||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')
|
||||
.example('$0 run --noproxy', 'Run requests with system proxy disabled');
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
try {
|
||||
let {
|
||||
filename,
|
||||
paths,
|
||||
cacert,
|
||||
ignoreTruststore,
|
||||
disableCookies,
|
||||
env,
|
||||
envFile,
|
||||
envVar,
|
||||
insecure,
|
||||
r: recursive,
|
||||
@@ -233,6 +267,7 @@ const handler = async function (argv) {
|
||||
reporterSkipAllHeaders,
|
||||
reporterSkipHeaders,
|
||||
clientCertConfig,
|
||||
noproxy,
|
||||
delay
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
@@ -274,33 +309,31 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if (!pathExists) {
|
||||
console.error(chalk.red(`File or directory ${filename} does not exist`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||
}
|
||||
} else {
|
||||
filename = './';
|
||||
recursive = true;
|
||||
}
|
||||
|
||||
const runtimeVariables = {};
|
||||
let envVars = {};
|
||||
|
||||
if (env) {
|
||||
const envFile = path.join(collectionPath, 'environments', `${env}.bru`);
|
||||
const envPathExists = await exists(envFile);
|
||||
if (env && envFile) {
|
||||
console.error(chalk.red(`Cannot use both --env and --env-file options together`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE);
|
||||
}
|
||||
|
||||
if (envFile || env) {
|
||||
const envFilePath = envFile
|
||||
? path.resolve(collectionPath, envFile)
|
||||
: path.join(collectionPath, 'environments', `${env}.bru`);
|
||||
|
||||
const envFileExists = await exists(envFilePath);
|
||||
if (!envFileExists) {
|
||||
const errorPath = envFile || `environments/${env}.bru`;
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(errorPath));
|
||||
|
||||
if (!envPathExists) {
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
|
||||
}
|
||||
|
||||
const envBruContent = fs.readFileSync(envFile, 'utf8');
|
||||
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const envJson = bruToEnvJson(envBruContent);
|
||||
envVars = getEnvVars(envJson);
|
||||
envVars.__name__ = env;
|
||||
envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
|
||||
}
|
||||
|
||||
if (envVar) {
|
||||
@@ -339,6 +372,9 @@ const handler = async function (argv) {
|
||||
if (disableCookies) {
|
||||
options['disableCookies'] = true;
|
||||
}
|
||||
if (noproxy) {
|
||||
options['noproxy'] = true;
|
||||
}
|
||||
if (cacert && cacert.length) {
|
||||
if (insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
@@ -392,45 +428,34 @@ const handler = async function (argv) {
|
||||
});
|
||||
}
|
||||
|
||||
const _isFile = isFile(filename);
|
||||
let requestItems = [];
|
||||
let results = [];
|
||||
|
||||
let requestItems = [];
|
||||
|
||||
if (_isFile) {
|
||||
console.log(chalk.yellow('Running Request \n'));
|
||||
const bruContent = fs.readFileSync(filename, 'utf8');
|
||||
const requestItem = bruToJson(bruContent);
|
||||
requestItem.pathname = path.resolve(collectionPath, filename);
|
||||
requestItems.push(requestItem);
|
||||
if (!paths || !paths.length) {
|
||||
paths = ['./'];
|
||||
recursive = true;
|
||||
}
|
||||
|
||||
const _isDirectory = isDirectory(filename);
|
||||
if (_isDirectory) {
|
||||
if (!recursive) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
} else {
|
||||
console.log(chalk.yellow('Running Folder Recursively \n'));
|
||||
}
|
||||
const resolvedFilepath = path.resolve(filename);
|
||||
if (resolvedFilepath === collectionPath) {
|
||||
requestItems = getAllRequestsInFolder(collection?.items, recursive);
|
||||
} else {
|
||||
const folderItem = findItemInCollection(collection, resolvedFilepath);
|
||||
if (folderItem) {
|
||||
requestItems = getAllRequestsInFolder(folderItem.items, recursive);
|
||||
}
|
||||
}
|
||||
const resolvedPaths = paths.map(p => path.resolve(process.cwd(), p));
|
||||
|
||||
if (testsOnly) {
|
||||
requestItems = requestItems.filter((iter) => {
|
||||
const requestHasTests = iter.request?.tests;
|
||||
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
|
||||
return requestHasTests || requestHasActiveAsserts;
|
||||
});
|
||||
for (const resolvedPath of resolvedPaths) {
|
||||
const pathExists = await exists(resolvedPath);
|
||||
if (!pathExists) {
|
||||
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
||||
|
||||
if (testsOnly) {
|
||||
requestItems = requestItems.filter((iter) => {
|
||||
const requestHasTests = iter.request?.tests;
|
||||
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
|
||||
return requestHasTests || requestHasActiveAsserts;
|
||||
});
|
||||
}
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
|
||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||
@@ -489,7 +514,7 @@ const handler = async function (argv) {
|
||||
if(Number.isNaN(delay) && !isLastRun){
|
||||
console.log(chalk.red(`Ignoring delay because it's not a valid number.`));
|
||||
}
|
||||
|
||||
|
||||
results.push({
|
||||
...result,
|
||||
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
|
||||
@@ -530,7 +555,9 @@ const handler = async function (argv) {
|
||||
const requestFailure = result?.error && !result?.skipped;
|
||||
const testFailure = result?.testResults?.find((iter) => iter.status === 'fail');
|
||||
const assertionFailure = result?.assertionResults?.find((iter) => iter.status === 'fail');
|
||||
if (requestFailure || testFailure || assertionFailure) {
|
||||
const preRequestTestFailure = result?.preRequestTestResults?.find((iter) => iter.status === 'fail');
|
||||
const postResponseTestFailure = result?.postResponseTestResults?.find((iter) => iter.status === 'fail');
|
||||
if (requestFailure || testFailure || assertionFailure || preRequestTestFailure || postResponseTestFailure) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -541,7 +568,7 @@ const handler = async function (argv) {
|
||||
if (result?.shouldStopRunnerExecution) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
@@ -608,7 +635,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) {
|
||||
if ((summary.failedAssertions + summary.failedTests + summary.failedPreRequestTests + summary.failedPostResponseTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) {
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -32,6 +32,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers,
|
||||
name: item.name,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
@@ -46,7 +47,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'bearer') {
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'apikey') {
|
||||
@@ -173,7 +174,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'wsse') {
|
||||
|
||||
@@ -45,10 +45,33 @@ const runSingleRequest = async function (
|
||||
) {
|
||||
const { pathname: itemPathname } = item;
|
||||
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
||||
|
||||
const logResults = (results, title) => {
|
||||
if (results?.length) {
|
||||
if (title) {
|
||||
console.log(chalk.dim(title));
|
||||
}
|
||||
each(results, (r) => {
|
||||
const message = r.description || `${r.lhsExpr}: ${r.rhsExpr}`;
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(message));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(message));
|
||||
if (r.error) {
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
let shouldStopRunnerExecution = false;
|
||||
let preRequestTestResults = [];
|
||||
let postResponseTestResults = [];
|
||||
|
||||
request = prepareRequest(item, collection);
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
@@ -58,6 +81,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
const collectionName = collection?.brunoConfig?.name
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
@@ -69,7 +93,8 @@ const runSingleRequest = async function (
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
@@ -101,9 +126,13 @@ const runSingleRequest = async function (
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults: result?.results || [],
|
||||
postResponseTestResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
|
||||
preRequestTestResults = result?.results || [];
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -115,6 +144,7 @@ const runSingleRequest = async function (
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const noproxy = get(options, 'noproxy', false);
|
||||
const httpsAgentRequestFields = {};
|
||||
if (insecure) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
@@ -179,15 +209,22 @@ const runSingleRequest = async function (
|
||||
|
||||
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false);
|
||||
if (collectionProxyEnabled === true) {
|
||||
|
||||
if (noproxy) {
|
||||
// If noproxy flag is set, don't use any proxy
|
||||
proxyMode = 'off';
|
||||
} else if (collectionProxyEnabled === true) {
|
||||
// If collection proxy is enabled, use it
|
||||
proxyConfig = collectionProxyConfig;
|
||||
proxyMode = 'on';
|
||||
} else {
|
||||
// if the collection level proxy is not set, pick the system level proxy by default, to maintain backward compatibility
|
||||
} else if (collectionProxyEnabled === 'global') {
|
||||
// If collection proxy is set to 'global', use system proxy
|
||||
const { http_proxy, https_proxy } = getSystemProxyEnvVariables();
|
||||
if (http_proxy?.length || https_proxy?.length) {
|
||||
proxyMode = 'system';
|
||||
}
|
||||
} else {
|
||||
proxyMode = 'off';
|
||||
}
|
||||
|
||||
if (proxyMode === 'on') {
|
||||
@@ -201,8 +238,8 @@ const runSingleRequest = async function (
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));
|
||||
const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
@@ -304,6 +341,14 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
|
||||
// Set default value for requestMaxRedirects if not explicitly set
|
||||
if (requestMaxRedirects === undefined) {
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
}
|
||||
|
||||
// Handle OAuth2 authentication
|
||||
if (request.oauth2) {
|
||||
try {
|
||||
@@ -334,7 +379,7 @@ const runSingleRequest = async function (
|
||||
let response, responseTime;
|
||||
try {
|
||||
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
let axiosInstance = makeAxiosInstance({ requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies });
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
@@ -410,6 +455,8 @@ const runSingleRequest = async function (
|
||||
status: 'error',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
@@ -423,6 +470,9 @@ const runSingleRequest = async function (
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// Log pre-request test results
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests');
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
@@ -452,7 +502,8 @@ const runSingleRequest = async function (
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
@@ -461,9 +512,11 @@ const runSingleRequest = async function (
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
postResponseTestResults = result?.results || [];
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
@@ -476,15 +529,6 @@ const runSingleRequest = async function (
|
||||
runtimeVariables,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
@@ -502,7 +546,8 @@ const runSingleRequest = async function (
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
@@ -513,17 +558,12 @@ const runSingleRequest = async function (
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
}
|
||||
|
||||
if (testResults?.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logResults(assertionResults, 'Assertions');
|
||||
|
||||
return {
|
||||
test: {
|
||||
@@ -546,6 +586,8 @@ const runSingleRequest = async function (
|
||||
status: 'pass',
|
||||
assertionResults,
|
||||
testResults,
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
@@ -571,7 +613,9 @@ const runSingleRequest = async function (
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
testResults: [],
|
||||
preRequestTestResults: [],
|
||||
postResponseTestResults: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
const axios = require('axios');
|
||||
const { CLI_VERSION } = require('../constants');
|
||||
const { addCookieToJar, getCookieStringForUrl } = require('./cookies');
|
||||
|
||||
const redirectResponseCodes = [301, 302, 303, 307, 308];
|
||||
const METHOD_CHANGING_REDIRECTS = [301, 302, 303];
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (headers['set-cookie']) {
|
||||
let setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createRedirectConfig = (error, redirectUrl) => {
|
||||
const requestConfig = {
|
||||
...error.config,
|
||||
url: redirectUrl,
|
||||
headers: { ...error.config.headers }
|
||||
};
|
||||
|
||||
const statusCode = error.response.status;
|
||||
const originalMethod = (error.config.method || 'get').toLowerCase();
|
||||
|
||||
// For 301, 302, 303: change method to GET unless it was HEAD
|
||||
if (METHOD_CHANGING_REDIRECTS.includes(statusCode) && originalMethod !== 'head') {
|
||||
requestConfig.method = 'get';
|
||||
requestConfig.data = undefined;
|
||||
|
||||
// Clean up headers that are no longer relevant
|
||||
delete requestConfig.headers['content-length'];
|
||||
delete requestConfig.headers['Content-Length'];
|
||||
delete requestConfig.headers['content-type'];
|
||||
delete requestConfig.headers['Content-Type'];
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that configures axios with timing interceptors
|
||||
@@ -7,10 +49,13 @@ const { CLI_VERSION } = require('../constants');
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) {
|
||||
let redirectCount = 0;
|
||||
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
proxy: false,
|
||||
maxRedirects: 0,
|
||||
headers: {
|
||||
"User-Agent": `bruno-runtime/${CLI_VERSION}`
|
||||
}
|
||||
@@ -18,6 +63,15 @@ function makeAxiosInstance() {
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers['request-start-time'] = Date.now();
|
||||
|
||||
// Add cookies to request if available and not disabled
|
||||
if (!disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(config.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
config.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -26,6 +80,8 @@ function makeAxiosInstance() {
|
||||
const end = Date.now();
|
||||
const start = response.config.headers['request-start-time'];
|
||||
response.headers['request-duration'] = end - start;
|
||||
redirectCount = 0;
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
@@ -33,6 +89,42 @@ function makeAxiosInstance() {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
|
||||
if (redirectResponseCodes.includes(error.response.status)) {
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
// todo: needs to be discussed whether the original error response message should be modified or not
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const locationHeader = error.response.headers.location;
|
||||
if (!locationHeader) {
|
||||
// todo: needs to be discussed whether the original error response message should be modified or not
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
redirectCount++;
|
||||
let redirectUrl = locationHeader;
|
||||
|
||||
if (!locationHeader.match(/^https?:\/\//i)) {
|
||||
const URL = require('url');
|
||||
redirectUrl = URL.resolve(error.config.url, locationHeader);
|
||||
}
|
||||
|
||||
if (!disableCookies){
|
||||
saveCookies(redirectUrl, error.response.headers);
|
||||
}
|
||||
|
||||
const requestConfig = createRedirectConfig(error, redirectUrl);
|
||||
|
||||
if (!disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(redirectUrl);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
requestConfig.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
return instance(requestConfig);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -349,6 +349,39 @@ const getAllRequestsAtFolderRoot = (folderItems = []) => {
|
||||
return getAllRequestsInFolder(folderItems, false);
|
||||
}
|
||||
|
||||
const getCallStack = (resolvedPaths = [], collection, {recursive}) => {
|
||||
let requestItems = [];
|
||||
|
||||
|
||||
if (!resolvedPaths || !resolvedPaths.length) {
|
||||
return requestItems;
|
||||
}
|
||||
|
||||
for (const resolvedPath of resolvedPaths) {
|
||||
if (!resolvedPath || !resolvedPath.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resolvedPath === collection.pathname) {
|
||||
requestItems = requestItems.concat(getAllRequestsInFolder(collection.items, recursive));
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, resolvedPath);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.type === 'folder') {
|
||||
requestItems = requestItems.concat(getAllRequestsInFolder(item.items, recursive));
|
||||
} else {
|
||||
requestItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return requestItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Safe write file implementation to handle errors
|
||||
* @param {string} filePath - Path to write file
|
||||
@@ -489,5 +522,6 @@ module.exports = {
|
||||
createCollectionFromBrunoObject,
|
||||
mergeAuth,
|
||||
getAllRequestsInFolder,
|
||||
getAllRequestsAtFolderRoot
|
||||
getAllRequestsAtFolderRoot,
|
||||
getCallStack
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
@@ -11,7 +12,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url);
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
|
||||
460
packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js
Normal file
460
packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js
Normal file
@@ -0,0 +1,460 @@
|
||||
const { describe, it, expect, beforeEach } = require('@jest/globals');
|
||||
const { getCallStack } = require('../../../src/utils/collection');
|
||||
|
||||
const collection = {
|
||||
brunoConfig: {
|
||||
version: '1',
|
||||
name: 'multirun-cli',
|
||||
type: 'collection',
|
||||
ignore: ['node_modules', '.git']
|
||||
},
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: {},
|
||||
script: {},
|
||||
vars: {},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20',
|
||||
items: [
|
||||
{
|
||||
name: 'root-folder',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder',
|
||||
type: 'folder',
|
||||
items: [
|
||||
{
|
||||
name: 'root-child-folder',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder',
|
||||
type: 'folder',
|
||||
items: [
|
||||
{
|
||||
name: 'root-child-child-folder',
|
||||
pathname:
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder',
|
||||
type: 'folder',
|
||||
items: [
|
||||
{
|
||||
name: 'root-child-child-child-req-0',
|
||||
pathname:
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru',
|
||||
type: 'http-request',
|
||||
seq: 1,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-child-child-file-0")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'root-child-child-child-req-1',
|
||||
pathname:
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru',
|
||||
type: 'http-request',
|
||||
seq: 2,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-child-child-file-1")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: {},
|
||||
script: {},
|
||||
vars: {},
|
||||
tests: ''
|
||||
},
|
||||
meta: {
|
||||
name: 'root-child-child-folder',
|
||||
seq: 3
|
||||
}
|
||||
},
|
||||
seq: 3
|
||||
},
|
||||
{
|
||||
name: 'root-child-child-req-0',
|
||||
pathname:
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',
|
||||
type: 'http-request',
|
||||
seq: 4,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-child-file-0")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'root-child-child-req-1',
|
||||
pathname:
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru',
|
||||
type: 'http-request',
|
||||
seq: 5,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-child-file-1")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: {},
|
||||
script: {},
|
||||
vars: {},
|
||||
tests: ''
|
||||
},
|
||||
meta: {
|
||||
name: 'root-child-folder',
|
||||
seq: 6
|
||||
}
|
||||
},
|
||||
seq: 6
|
||||
},
|
||||
{
|
||||
name: 'root-child-req-0',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru',
|
||||
type: 'http-request',
|
||||
seq: 7,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-file-0")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'root-child-req-1',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru',
|
||||
type: 'http-request',
|
||||
seq: 8,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-child-file-1")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
auth: {},
|
||||
script: {},
|
||||
vars: {},
|
||||
tests: ''
|
||||
},
|
||||
meta: {
|
||||
name: 'root-folder',
|
||||
seq: 9
|
||||
}
|
||||
},
|
||||
seq: 9
|
||||
},
|
||||
{
|
||||
name: 'root-req-0',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',
|
||||
type: 'http-request',
|
||||
seq: 10,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-file-0")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'root-req-1',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru',
|
||||
type: 'http-request',
|
||||
seq: 11,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-file-1")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'root-req-2',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru',
|
||||
type: 'http-request',
|
||||
seq: 12,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://g.cn',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {
|
||||
req: 'console.log("root-file-2")'
|
||||
},
|
||||
tests: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const sequenceChangedCollection = {
|
||||
brunoConfig: {
|
||||
version: '1',
|
||||
name: 'sequenceChangedCollection',
|
||||
type: 'collection',
|
||||
ignore: ['node_modules', '.git']
|
||||
},
|
||||
root: {},
|
||||
pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection',
|
||||
items: [
|
||||
{
|
||||
name: 'three',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',
|
||||
type: 'http-request',
|
||||
seq: 1,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://usebruno.com',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'one',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',
|
||||
type: 'http-request',
|
||||
seq: 2,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://usebruno.com',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {},
|
||||
tests: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'two',
|
||||
pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru',
|
||||
type: 'http-request',
|
||||
seq: 2,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://usebruno.com',
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
},
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
},
|
||||
vars: [],
|
||||
assertions: [],
|
||||
script: {},
|
||||
tests: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe('getCallStack', () => {
|
||||
it('should return all requests in the collection', () => {
|
||||
const callStack = getCallStack(['/Users/tempo/Downloads/t-temp/multirun-cli-20'], collection, { recursive: true });
|
||||
const expectedCallStack = [
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'
|
||||
];
|
||||
expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);
|
||||
});
|
||||
|
||||
it('should return all requests in the collection when sequence is changed', () => {
|
||||
const callStack = getCallStack(
|
||||
['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'],
|
||||
sequenceChangedCollection,
|
||||
{
|
||||
recursive: true
|
||||
}
|
||||
);
|
||||
const expectedCallStack = [
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru'
|
||||
];
|
||||
expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCallStack with collection sequence changed', () => {
|
||||
it('should return an empty array', () => {
|
||||
const callStack = getCallStack(
|
||||
['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'],
|
||||
sequenceChangedCollection,
|
||||
{
|
||||
recursive: true
|
||||
}
|
||||
);
|
||||
const expectedCallStack = [
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',
|
||||
'/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru'
|
||||
];
|
||||
expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCallStack with muliple folders and requests run', () => {
|
||||
it('should return an empty array', () => {
|
||||
const callStack = getCallStack(
|
||||
[
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'
|
||||
],
|
||||
collection,
|
||||
{
|
||||
recursive: true
|
||||
}
|
||||
);
|
||||
const expectedCallStack = [
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',
|
||||
'/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'
|
||||
];
|
||||
expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);
|
||||
});
|
||||
});
|
||||
@@ -13,12 +13,20 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
let totalPreRequestTests = 0;
|
||||
let passedPreRequestTests = 0;
|
||||
let failedPreRequestTests = 0;
|
||||
let totalPostResponseTests = 0;
|
||||
let passedPostResponseTests = 0;
|
||||
let failedPostResponseTests = 0;
|
||||
|
||||
for (const result of results || []) {
|
||||
const { status, testResults, assertionResults } = result;
|
||||
const { status, testResults, assertionResults, preRequestTestResults, postResponseTestResults } = result;
|
||||
totalRequests += 1;
|
||||
totalTests += Number(testResults?.length) || 0;
|
||||
totalAssertions += Number(assertionResults?.length) || 0;
|
||||
totalPreRequestTests += Number(preRequestTestResults?.length) || 0;
|
||||
totalPostResponseTests += Number(postResponseTestResults?.length) || 0;
|
||||
|
||||
if (status === 'skipped') {
|
||||
skippedRequests += 1;
|
||||
@@ -42,6 +50,22 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
for (const preRequestTestResult of preRequestTestResults || []) {
|
||||
if (preRequestTestResult.status === "pass") {
|
||||
passedPreRequestTests += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedPreRequestTests += 1;
|
||||
}
|
||||
}
|
||||
for (const postResponseTestResult of postResponseTestResults || []) {
|
||||
if (postResponseTestResult.status === "pass") {
|
||||
passedPostResponseTests += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedPostResponseTests += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyFailed && status !== "error") {
|
||||
passedRequests += 1;
|
||||
@@ -64,5 +88,11 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests,
|
||||
totalPreRequestTests,
|
||||
passedPreRequestTests,
|
||||
failedPreRequestTests,
|
||||
totalPostResponseTests,
|
||||
passedPostResponseTests,
|
||||
failedPostResponseTests,
|
||||
};
|
||||
};
|
||||
@@ -89,6 +89,8 @@ export type T_RunnerRequestExecutionResult = {
|
||||
error: null | undefined | string;
|
||||
assertionResults?: T_AssertionResult[];
|
||||
testResults?: T_TestResult[];
|
||||
preRequestTestResults?: T_TestResult[];
|
||||
postResponseTestResults?: T_TestResult[];
|
||||
runDuration: number;
|
||||
}
|
||||
|
||||
@@ -112,4 +114,10 @@ export type T_RunSummary = {
|
||||
totalTests: number;
|
||||
passedTests: number;
|
||||
failedTests: number;
|
||||
totalPreRequestTests: number;
|
||||
passedPreRequestTests: number;
|
||||
failedPreRequestTests: number;
|
||||
totalPostResponseTests: number;
|
||||
passedPostResponseTests: number;
|
||||
failedPostResponseTests: number;
|
||||
}
|
||||
@@ -1,11 +1,26 @@
|
||||
import { mockDataFunctions } from "./faker-functions";
|
||||
|
||||
describe("mockDataFunctions Regex Validation", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test("timestamp and isoTimestamp should return mocked time values", () => {
|
||||
const expectedTimestamp = '1704067200';
|
||||
const expectedIsoTimestamp = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
expect(mockDataFunctions.timestamp()).toBe(expectedTimestamp);
|
||||
expect(mockDataFunctions.isoTimestamp()).toBe(expectedIsoTimestamp);
|
||||
});
|
||||
|
||||
test("all values should match their expected patterns", () => {
|
||||
const patterns: Record<string, RegExp> = {
|
||||
guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
timestamp: /^\d{13,}$/,
|
||||
isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
||||
randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
randomAlphaNumeric: /^[\w]$/,
|
||||
randomBoolean: /^(true|false)$/,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { faker } from '@faker-js/faker';
|
||||
|
||||
export const mockDataFunctions = {
|
||||
guid: () => faker.string.uuid(),
|
||||
timestamp: () => faker.date.anytime().getTime().toString(),
|
||||
isoTimestamp: () => faker.date.anytime().toISOString(),
|
||||
timestamp: () => Math.floor(Date.now() / 1000).toString(),
|
||||
isoTimestamp: () => new Date().toISOString(),
|
||||
randomUUID: () => faker.string.uuid(),
|
||||
randomAlphaNumeric: () => faker.string.alphanumeric(),
|
||||
randomBoolean: () => faker.datatype.boolean(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
@@ -288,6 +289,9 @@ const parseInsomniaCollection = (data) => {
|
||||
|
||||
export const insomniaToBruno = (insomniaCollection) => {
|
||||
try {
|
||||
if(typeof insomniaCollection !== 'object') {
|
||||
insomniaCollection = jsyaml.load(insomniaCollection);
|
||||
}
|
||||
let collection;
|
||||
if (isInsomniaV5Export(insomniaCollection)) {
|
||||
collection = parseInsomniaV5Collection(insomniaCollection);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
@@ -7,14 +8,22 @@ const ensureUrl = (url) => {
|
||||
return url.replace(/([^:])\/{2,}/g, '$1/');
|
||||
};
|
||||
|
||||
const buildEmptyJsonBody = (bodySchema) => {
|
||||
const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
|
||||
// Check for circular references
|
||||
if (visited.has(bodySchema)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Add this schema to visited map
|
||||
visited.set(bodySchema, true);
|
||||
|
||||
let _jsonBody = {};
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
if (prop.type === 'object') {
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop);
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop, visited);
|
||||
} else if (prop.type === 'array') {
|
||||
if (prop.items && prop.items.type === 'object') {
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items)];
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items, visited)];
|
||||
} else {
|
||||
_jsonBody[name] = [];
|
||||
}
|
||||
@@ -422,6 +431,10 @@ export const parseOpenApiCollection = (data) => {
|
||||
|
||||
export const openApiToBruno = (openApiSpecification) => {
|
||||
try {
|
||||
if(typeof openApiSpecification !== 'object') {
|
||||
openApiSpecification = jsyaml.load(openApiSpecification);
|
||||
}
|
||||
|
||||
const collection = parseOpenApiCollection(openApiSpecification);
|
||||
const transformedCollection = transformItemsInCollection(collection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
|
||||
@@ -103,14 +103,14 @@ const importScriptsFromEvents = (events, requestObject) => {
|
||||
}
|
||||
|
||||
if (event.listen === 'test') {
|
||||
if (!requestObject.tests) {
|
||||
requestObject.tests = {};
|
||||
if (!requestObject.script) {
|
||||
requestObject.script = {};
|
||||
}
|
||||
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.tests = postmanTranslation(event.script.exec)
|
||||
requestObject.script.res = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
requestObject.script.res = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
@@ -376,16 +376,17 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorke
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request?.tests) {
|
||||
brunoRequestItem.request.tests = {};
|
||||
if (!brunoRequestItem.request?.script) {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (event.script.exec && event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.tests = postmanTranslation(event.script.exec)
|
||||
brunoRequestItem.request.script.res = postmanTranslation(event.script.exec)
|
||||
} else {
|
||||
brunoRequestItem.request.tests = '';
|
||||
brunoRequestItem.request.script.res = '';
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -581,15 +582,12 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
|
||||
if (!item.root.request.script) {
|
||||
item.root.request.script = {};
|
||||
}
|
||||
if (!item.root.request.tests) {
|
||||
item.root.request.tests = '';
|
||||
}
|
||||
|
||||
const script = translatedScripts.get(item.uid).request?.script?.req;
|
||||
const tests = translatedScripts.get(item.uid).request?.tests;
|
||||
const tests = translatedScripts.get(item.uid).request?.script?.res;
|
||||
|
||||
item.root.request.script.req = script && script.length > 0 ? script : '';
|
||||
item.root.request.tests = tests && tests.length > 0 ? tests : '';
|
||||
item.root.request.script.res = tests && tests.length > 0 ? tests : '';
|
||||
}
|
||||
|
||||
// Recursively apply to nested items
|
||||
@@ -601,15 +599,12 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) =>
|
||||
if (!item.request.script) {
|
||||
item.request.script = {};
|
||||
}
|
||||
if (!item.request.tests) {
|
||||
item.request.tests = '';
|
||||
}
|
||||
|
||||
const script = translatedScripts.get(item.uid).request?.script?.req;
|
||||
const tests = translatedScripts.get(item.uid).request?.tests;
|
||||
const tests = translatedScripts.get(item.uid).request?.script?.res;
|
||||
|
||||
item.request.script.req = script && script.length > 0 ? script : '';
|
||||
item.request.tests = tests && tests.length > 0 ? tests : '';
|
||||
item.request.script.res = tests && tests.length > 0 ? tests : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ const replacements = {
|
||||
'pm\\.environment\\.set\\(': 'bru.setEnvVar(',
|
||||
'pm\\.variables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.variables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.variables\\.replaceIn\\(': 'bru.interpolate(',
|
||||
'pm\\.collectionVariables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.collectionVariables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.collectionVariables\\.has\\(': 'bru.hasVar(',
|
||||
@@ -33,6 +34,7 @@ const replacements = {
|
||||
'pm\\.request\\.method': 'req.getMethod()',
|
||||
'pm\\.request\\.headers': 'req.getHeaders()',
|
||||
'pm\\.request\\.body': 'req.getBody()',
|
||||
'pm\\.info\\.requestName': 'req.getName()',
|
||||
// deprecated translations
|
||||
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
|
||||
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sendRequestTransformer from './send-request-transformer';
|
||||
const j = require('jscodeshift');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
|
||||
@@ -52,7 +53,7 @@ const simpleTranslations = {
|
||||
'pm.variables.get': 'bru.getVar',
|
||||
'pm.variables.set': 'bru.setVar',
|
||||
'pm.variables.has': 'bru.hasVar',
|
||||
|
||||
'pm.variables.replaceIn': 'bru.interpolate',
|
||||
// Collection variables
|
||||
'pm.collectionVariables.get': 'bru.getVar',
|
||||
'pm.collectionVariables.set': 'bru.setVar',
|
||||
@@ -67,6 +68,9 @@ const simpleTranslations = {
|
||||
'pm.expect': 'expect',
|
||||
'pm.expect.fail': 'expect.fail',
|
||||
|
||||
// Info
|
||||
'pm.info.requestName': 'req.getName()',
|
||||
|
||||
// Request properties
|
||||
'pm.request.url': 'req.getUrl()',
|
||||
'pm.request.method': 'req.getMethod()',
|
||||
@@ -96,7 +100,14 @@ const simpleTranslations = {
|
||||
* as a separate statement, which allows a single Postman expression to be
|
||||
* transformed into multiple Bruno statements (e.g. for complex assertions).
|
||||
*/
|
||||
|
||||
const complexTransformations = [
|
||||
// pm.sendRequest transformation
|
||||
{
|
||||
pattern: 'pm.sendRequest',
|
||||
transform: sendRequestTransformer
|
||||
},
|
||||
|
||||
// pm.environment.has requires special handling
|
||||
{
|
||||
pattern: 'pm.environment.has',
|
||||
|
||||
277
packages/bruno-converters/src/utils/send-request-transformer.js
Normal file
277
packages/bruno-converters/src/utils/send-request-transformer.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Convert Postman header array format to Bruno headers object
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} arrayValue - Array expression of key-value pair objects
|
||||
* @returns {Object} - Object expression with key-value pairs
|
||||
*/
|
||||
const convertArrayToObject = (j, arrayValue) => {
|
||||
const obj = j.objectExpression([]);
|
||||
|
||||
if (arrayValue.type === 'ArrayExpression') {
|
||||
arrayValue.elements.forEach(elem => {
|
||||
if (elem.type === 'ObjectExpression') {
|
||||
const keyProp = elem.properties.find(p => (p.key.name === 'key' || p.key.value === 'key'));
|
||||
const valueProp = elem.properties.find(p => (p.key.name === 'value' || p.key.value === 'value'));
|
||||
|
||||
if (keyProp && valueProp) {
|
||||
obj.properties.push(
|
||||
j.property(
|
||||
'init',
|
||||
j.literal(keyProp.value.value),
|
||||
valueProp.value
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add or update a specific header in the request options
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
* @param {string} headerName - Header name to add/update
|
||||
* @param {string} headerValue - Header value
|
||||
*/
|
||||
const addOrUpdateHeader = (j, requestOptions, headerName, headerValue) => {
|
||||
let headersProp = requestOptions.properties.find(p => (p.key.name === 'headers' || p.key.value === 'headers'));
|
||||
|
||||
if (!headersProp) {
|
||||
headersProp = j.property('init', j.identifier('headers'), j.objectExpression([]));
|
||||
requestOptions.properties.push(headersProp);
|
||||
} else if (headersProp.value.type !== 'ObjectExpression') {
|
||||
headersProp.value = j.objectExpression([]);
|
||||
}
|
||||
|
||||
// filter out existing header with same name (case-insensitive)
|
||||
headersProp.value.properties = headersProp.value.properties.filter(p =>
|
||||
p.key.type !== 'Literal' ||
|
||||
p.key.value.toLowerCase() !== headerName.toLowerCase()
|
||||
);
|
||||
|
||||
headersProp.value.properties.push(
|
||||
j.property(
|
||||
'init',
|
||||
j.literal(headerName),
|
||||
j.literal(headerValue)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform headers property from array to object format
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
*/
|
||||
const transformHeaders = (j, requestOptions) => {
|
||||
if (requestOptions.type !== 'ObjectExpression') return;
|
||||
|
||||
requestOptions.properties.forEach(prop => {
|
||||
// find and rename 'header' property to 'headers'
|
||||
if (prop.key.name === 'header' || prop.key.value === 'header') {
|
||||
prop.key.name = 'headers';
|
||||
prop.key.value = 'headers';
|
||||
|
||||
// Handle array of header objects
|
||||
if (prop.value.type === 'ArrayExpression') {
|
||||
prop.value = convertArrayToObject(j, prop.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform body property based on body mode
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
* @returns {Array|null} - Array of statements if formdata is used, null otherwise
|
||||
*/
|
||||
const transformBody = (j, requestOptions) => {
|
||||
if (requestOptions.type !== 'ObjectExpression') return null;
|
||||
|
||||
requestOptions.properties.forEach(prop => {
|
||||
if (prop.key.name === 'body' || prop.key.value === 'body') {
|
||||
if (prop.value.type === 'ObjectExpression') {
|
||||
const bodyProps = prop.value.properties;
|
||||
const modeProp = bodyProps.find(p => (p.key.name === 'mode' || p.key.value === 'mode'));
|
||||
|
||||
if (modeProp && modeProp.value.type === 'Literal') {
|
||||
const bodyMode = modeProp.value.value;
|
||||
|
||||
// Handle raw mode (text, json, xml, etc.)
|
||||
if (bodyMode === 'raw') {
|
||||
const rawProp = bodyProps.find(p => (p.key.name === 'raw' || p.key.value === 'raw'));
|
||||
|
||||
if (rawProp) {
|
||||
// Replace body with data
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
prop.value = rawProp.value;
|
||||
}
|
||||
}
|
||||
// Handle urlencoded mode
|
||||
else if (bodyMode === 'urlencoded') {
|
||||
const urlencodedProp = bodyProps.find(p => (p.key.name === 'urlencoded' || p.key.value === 'urlencoded') && p.value.type === 'ArrayExpression');
|
||||
|
||||
if (urlencodedProp) {
|
||||
// Replace the body property with a 'data' property
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
|
||||
// Transform the urlencoded array to an object
|
||||
prop.value = convertArrayToObject(j, urlencodedProp.value);
|
||||
|
||||
// Add Content-Type header for urlencoded
|
||||
addOrUpdateHeader(j, requestOptions, 'Content-Type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
}
|
||||
// Handle formdata mode
|
||||
else if (bodyMode === 'formdata') {
|
||||
const formdataProp = bodyProps.find(p => (p.key.name === 'formdata' || p.key.value === 'formdata') && p.value.type === 'ArrayExpression');
|
||||
|
||||
if (formdataProp) {
|
||||
// Replace the body property with a 'data' property
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
|
||||
// Transform the urlencoded array to an object
|
||||
prop.value = convertArrayToObject(j, formdataProp.value);
|
||||
|
||||
// Add Content-Type header for urlencoded
|
||||
addOrUpdateHeader(j, requestOptions, 'Content-Type', 'multipart/form-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform callback function to Bruno format
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} callback - Callback function expression
|
||||
* @returns {Object} - Transformed callback function
|
||||
*/
|
||||
const transformCallback = (j, callback) => {
|
||||
if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) return null;
|
||||
|
||||
const params = callback.params;
|
||||
const callbackBody = callback.body;
|
||||
|
||||
// Get the response parameter name (typically the second param)
|
||||
let responseVarName = 'response'; // Default if not found
|
||||
if (params.length >= 2 && params[1].type === 'Identifier') {
|
||||
responseVarName = params[1].name;
|
||||
}
|
||||
|
||||
let errorVarName = 'error'; // Default if not found
|
||||
if (params.length >= 1 && params[0].type === 'Identifier') {
|
||||
errorVarName = params[0].name;
|
||||
}
|
||||
|
||||
// Define translations for callback response properties
|
||||
const responsePropertyMap = {
|
||||
'json': 'data',
|
||||
'text': 'data',
|
||||
'code': 'status',
|
||||
'status': 'statusText',
|
||||
};
|
||||
|
||||
// Process the callback body to transform response property references
|
||||
j(callbackBody).find(j.MemberExpression, {
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: responseVarName
|
||||
}
|
||||
}).forEach(memberPath => {
|
||||
const property = memberPath.node.property;
|
||||
|
||||
// Handle property access
|
||||
if (property.type === 'Identifier' && responsePropertyMap[property.name]) {
|
||||
const bruProperty = responsePropertyMap[property.name];
|
||||
if (bruProperty) {
|
||||
// Check if memberPath is part of a CallExpression
|
||||
const parentPath = memberPath.parent;
|
||||
if (parentPath && parentPath.node.type === 'CallExpression') {
|
||||
// Replace the entire CallExpression with a property access
|
||||
j(parentPath).replaceWith(
|
||||
j.memberExpression(
|
||||
j.identifier(responseVarName),
|
||||
j.identifier(bruProperty)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Regular property access replacement
|
||||
j(memberPath).replaceWith(
|
||||
j.memberExpression(
|
||||
j.identifier(responseVarName),
|
||||
j.identifier(bruProperty)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create the callback block
|
||||
return j.functionExpression(
|
||||
null,
|
||||
[j.identifier(errorVarName), j.identifier(responseVarName)],
|
||||
j.blockStatement(callbackBody.body)
|
||||
);
|
||||
};
|
||||
|
||||
const sendRequestTransformer = (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
if (callExpr.type !== 'CallExpression') return;
|
||||
|
||||
// Clone the argument object for modification
|
||||
const args = [...callExpr.arguments];
|
||||
if (!args.length) return;
|
||||
|
||||
const requestOptions = args[0];
|
||||
const callback = args[1];
|
||||
|
||||
// Check if original call was awaited
|
||||
const wasAwaited = path.parent.parent.value.type === 'AwaitExpression';
|
||||
|
||||
// transform the request config options
|
||||
if (requestOptions.type === 'ObjectExpression') {
|
||||
// Transform headers
|
||||
transformHeaders(j, requestOptions);
|
||||
// Transform body
|
||||
transformBody(j, requestOptions);
|
||||
}
|
||||
|
||||
// Create the callback block and promise chain if there's a callback
|
||||
if (callback) {
|
||||
const transformedCallback = transformCallback(j, callback);
|
||||
|
||||
// Add async keyword to the callback function
|
||||
if (transformedCallback && (transformedCallback.type === 'FunctionExpression' || transformedCallback.type === 'ArrowFunctionExpression')) {
|
||||
transformedCallback.async = true;
|
||||
}
|
||||
|
||||
// Create expression: await bru.sendRequest(requestConfig, callback);
|
||||
const sendRequestCall = j.callExpression(
|
||||
j.identifier('bru.sendRequest'),
|
||||
transformedCallback ? [requestOptions, transformedCallback] : [requestOptions]
|
||||
);
|
||||
|
||||
return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall);
|
||||
}
|
||||
|
||||
// If there's no callback, just transform to await bru.sendRequest
|
||||
const sendRequestCall = j.callExpression(
|
||||
j.identifier('bru.sendRequest'),
|
||||
[requestOptions]
|
||||
);
|
||||
|
||||
return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall);
|
||||
};
|
||||
|
||||
export default sendRequestTransformer;
|
||||
@@ -6,8 +6,7 @@ parentPort.on('message', (workerData) => {
|
||||
const { scripts } = workerData;
|
||||
const modScripts = scripts.map(([uid, { events }]) => {
|
||||
const requestObject = {
|
||||
script: {},
|
||||
tests: {}
|
||||
script: {}
|
||||
}
|
||||
|
||||
if (events && Array.isArray(events)) {
|
||||
@@ -23,9 +22,9 @@ parentPort.on('message', (workerData) => {
|
||||
|
||||
if(event.listen === 'test') {
|
||||
if(event.script.exec && event.script.exec.length > 0) {
|
||||
requestObject.tests = postmanTranslation(event.script.exec);
|
||||
requestObject.script.res = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
requestObject.script.res = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';
|
||||
import jsyaml from 'js-yaml';
|
||||
|
||||
describe('insomnia-collection', () => {
|
||||
it('should correctly import a valid Insomnia v5 collection file', async () => {
|
||||
const brunoCollection = insomniaToBruno(jsyaml.load(insomniaCollection));
|
||||
const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('openapi-circular-references', () => {
|
||||
it('should handle simple circular references in schema correctly', async () => {
|
||||
const brunoCollection = openApiToBruno(circularRefsData);
|
||||
|
||||
expect(brunoCollection).toMatchObject(circularRefsOutput);
|
||||
});
|
||||
|
||||
it('should handle complex circular reference chains correctly', async () => {
|
||||
const brunoCollection = openApiToBruno(complexCircularRefsData);
|
||||
|
||||
expect(brunoCollection).toMatchObject(circularRefsOutput);
|
||||
});
|
||||
});
|
||||
|
||||
const circularRefsData = {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"schema_1": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_1",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_2": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_2",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_1",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
},
|
||||
"operation": {
|
||||
"description": "operation",
|
||||
"enum": ["ANY", "ALL"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "circular reference openapi sample json spec",
|
||||
"title": "circular reference openapi sample json spec",
|
||||
"version": "0.1"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/": {
|
||||
"post": {
|
||||
"deprecated": false,
|
||||
"description": "echo ping api",
|
||||
"operationId": "echo ping",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "echo ping api",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": "ping"
|
||||
}
|
||||
},
|
||||
"description": "Returned if the request is successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [{ "url": "https://echo.usebruno.com" }]
|
||||
};
|
||||
|
||||
// More complex circular reference test with a longer chain
|
||||
const complexCircularRefsData = {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"schema_1": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_1",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_1",
|
||||
"items": { "$ref": "#/components/schemas/schema_2" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_2": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_2",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_2",
|
||||
"items": { "$ref": "#/components/schemas/schema_3" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_3": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_3",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_3",
|
||||
"items": { "$ref": "#/components/schemas/schema_4" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_4": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_4",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_4",
|
||||
"items": { "$ref": "#/components/schemas/schema_5" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_5": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_4",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_5",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_6": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_3",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_3",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
},
|
||||
"operation": {
|
||||
"description": "operation",
|
||||
"enum": ["ANY", "ALL"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "circular reference openapi sample json spec",
|
||||
"title": "circular reference openapi sample json spec",
|
||||
"version": "0.1"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/": {
|
||||
"post": {
|
||||
"deprecated": false,
|
||||
"description": "echo ping api",
|
||||
"operationId": "echo ping",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "echo ping api",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": "ping"
|
||||
}
|
||||
},
|
||||
"description": "Returned if the request is successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [{ "url": "https://echo.usebruno.com" }]
|
||||
};
|
||||
|
||||
const circularRefsOutput = {
|
||||
"environments": [
|
||||
{
|
||||
"name": "Environment 1",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "baseUrl",
|
||||
"secret": false,
|
||||
"type": "text",
|
||||
"value": "https://echo.usebruno.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"name": "echo ping",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "{{baseUrl}}/",
|
||||
"method": "POST",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "json",
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"name": "circular reference openapi sample json spec",
|
||||
"version": "1",
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../src/openapi/openapi-to-bruno';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('openapi-collection', () => {
|
||||
it('should correctly import a valid OpenAPI file', async () => {
|
||||
const openApiSpecification = jsyaml.load(openApiCollectionString);
|
||||
const brunoCollection = openApiToBruno(openApiSpecification);
|
||||
const brunoCollection = openApiToBruno(openApiCollectionString);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ describe('postmanTranslations - request commands', () => {
|
||||
const requestMethod = pm.request.method;
|
||||
const requestHeaders = pm.request.headers;
|
||||
const requestBody = pm.request.body;
|
||||
const requestName = pm.info.requestName;
|
||||
|
||||
pm.test('Request method is POST', function() {
|
||||
pm.expect(pm.request.method).to.equal('POST');
|
||||
@@ -17,6 +18,7 @@ describe('postmanTranslations - request commands', () => {
|
||||
const requestMethod = req.getMethod();
|
||||
const requestHeaders = req.getHeaders();
|
||||
const requestBody = req.getBody();
|
||||
const requestName = req.getName();
|
||||
|
||||
test('Request method is POST', function() {
|
||||
expect(req.getMethod()).to.equal('POST');
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => {
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));";
|
||||
const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
import translateCode from '../../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Send Request Translation', () => {
|
||||
describe('Raw Body Mode', () => {
|
||||
it('should transform raw JSON string body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({
|
||||
"x": 1
|
||||
})
|
||||
}
|
||||
}, async function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify({
|
||||
"x": 1
|
||||
})
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform raw JSON object body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: {
|
||||
"x": 1
|
||||
}
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
"x": 1
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform raw text body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: 'Hello World'
|
||||
}
|
||||
}, function (error, response) {
|
||||
console.log(response.text());
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: 'Hello World'
|
||||
}, async function(error, response) {
|
||||
console.log(response.data);
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL-encoded Body Mode', () => {
|
||||
it('should transform urlencoded body with single key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key", value: "value" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: {
|
||||
"key": "value"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body with multiple key-value pairs', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
console.log(response.json());
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
console.log(response.data);
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body when no Content-Type header exists', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key1", value: "value1" },
|
||||
{ key: "key2", value: "value2" }
|
||||
]
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
|
||||
data: {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
},
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body with incorrect Content-Type header', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key1", value: "value1" },
|
||||
{ key: "key2", value: "value2" }
|
||||
]
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-part Form Data Body Mode', () => {
|
||||
it('should transform formdata body with single key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "key", value: "value" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: {
|
||||
"key": "value"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body with multiple key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body when no Content-Type header exists', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
},
|
||||
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body with incorrect Content-Type header', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, async function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.data;
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers and Content-Type Handling', () => {
|
||||
it('should rename header property to headers', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Custom-Header': 'custom-value',
|
||||
'Authorization': 'Bearer token'
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Custom-Header': 'custom-value',
|
||||
'Authorization': 'Bearer token'
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle header array format', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
header: [
|
||||
{ key: 'X-Custom-Header', value: 'custom-value' },
|
||||
{ key: 'Authorization', value: 'Bearer token' }
|
||||
]
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"X-Custom-Header": 'custom-value',
|
||||
"Authorization": 'Bearer token'
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Response Handling', () => {
|
||||
it('should transform response property access', () => {
|
||||
const code = `
|
||||
pm.sendRequest('https://echo.usebruno.com', function (error, response) {
|
||||
const status = response.code;
|
||||
const statusText = response.status;
|
||||
const headers = response.headers;
|
||||
const body = response.json();
|
||||
const responseTime = response.responseTime;
|
||||
const text = response.text();
|
||||
|
||||
if (status === 200) {
|
||||
console.log('Success!');
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain(`const status = response.status;
|
||||
const statusText = response.statusText;`);
|
||||
expect(translatedCode).toContain('const headers = response.headers');
|
||||
expect(translatedCode).toContain('const body = response.data');
|
||||
expect(translatedCode).toContain('const responseTime = response.responseTime');
|
||||
expect(translatedCode).toContain('const text = response.data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async/Await', () => {
|
||||
it('Should not add await if already present', () => {
|
||||
const code = `
|
||||
try {
|
||||
const response = await pm.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
console.log(response.json());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
try {
|
||||
const response = await bru.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
console.log(response.json());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Should handle arrow function callbacks', () => {
|
||||
const code = `
|
||||
try {
|
||||
pm.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
}, (error, response) => {
|
||||
console.log(response.json());
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
try {
|
||||
await bru.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
}, async function(error, response) {
|
||||
console.log(response.data);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Should handle async arrow function callbacks', () => {
|
||||
const code = `
|
||||
try {
|
||||
pm.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
}, async (error, response) => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000)
|
||||
});
|
||||
console.log(response.json());
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
try {
|
||||
await bru.sendRequest({
|
||||
url: "https://echo.usebruno.com",
|
||||
method: "GET"
|
||||
}, async function(error, response) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000)
|
||||
});
|
||||
console.log(response.data);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,55 +5,104 @@ describe('Variables Translation', () => {
|
||||
it('should translate pm.variables.get', () => {
|
||||
const code = 'pm.variables.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getVar("test");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.set', () => {
|
||||
const code = 'pm.variables.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("test", "value");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.has', () => {
|
||||
const code = 'pm.variables.has("userId");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.hasVar("userId");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn', () => {
|
||||
const code = 'pm.variables.replaceIn("Hello {{name}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.interpolate("Hello {{name}}");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn with variables and expressions', () => {
|
||||
const code = 'const greeting = pm.variables.replaceIn("Hello {{name}}, your user id is {{userId}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const greeting = bru.interpolate("Hello {{name}}, your user id is {{userId}}");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn within complex expressions', () => {
|
||||
const code = 'const url = baseUrl + pm.variables.replaceIn("/users/{{userId}}/profile");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const url = baseUrl + bru.interpolate("/users/{{userId}}/profile");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn with multiple nested variable references', () => {
|
||||
const code = 'const template = pm.variables.replaceIn("{{prefix}}-{{env}}-{{suffix}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const template = bru.interpolate("{{prefix}}-{{env}}-{{suffix}}");');
|
||||
});
|
||||
|
||||
it('should translate aliased variables.replaceIn', () => {
|
||||
const code = `
|
||||
const variables = pm.variables;
|
||||
const message = variables.replaceIn("Welcome, {{username}}!");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const message = bru.interpolate("Welcome, {{username}}!");
|
||||
`);
|
||||
});
|
||||
|
||||
// Collection variables tests
|
||||
it('should translate pm.collectionVariables.get', () => {
|
||||
const code = 'pm.collectionVariables.get("apiUrl");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getVar("apiUrl");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.set', () => {
|
||||
const code = 'pm.collectionVariables.set("token", jsonData.token);';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("token", jsonData.token);');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.has', () => {
|
||||
const code = 'pm.collectionVariables.has("authToken");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.hasVar("authToken");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.unset', () => {
|
||||
const code = 'pm.collectionVariables.unset("tempVar");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.deleteVar("tempVar");');
|
||||
});
|
||||
|
||||
it('should handle pm.globals.get', () => {
|
||||
const code = 'pm.globals.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getGlobalEnvVar("test");');
|
||||
});
|
||||
|
||||
it('should handle pm.globals.set', () => {
|
||||
const code = 'pm.globals.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setGlobalEnvVar("test", "value");');
|
||||
});
|
||||
|
||||
@@ -66,6 +115,7 @@ describe('Variables Translation', () => {
|
||||
const get = vars.get("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
@@ -83,6 +133,7 @@ describe('Variables Translation', () => {
|
||||
const unset = collVars.unset("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
@@ -98,6 +149,7 @@ describe('Variables Translation', () => {
|
||||
const set = globals.set("test", "value");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const get = bru.getGlobalEnvVar("test");
|
||||
const set = bru.setGlobalEnvVar("test", "value");
|
||||
@@ -108,6 +160,7 @@ describe('Variables Translation', () => {
|
||||
it('should handle conditional expressions with variable calls', () => {
|
||||
const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";');
|
||||
});
|
||||
|
||||
@@ -148,6 +201,7 @@ describe('Variables Translation', () => {
|
||||
it('should handle more complex nested expressions with variables', () => {
|
||||
const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));');
|
||||
});
|
||||
});
|
||||
@@ -36,9 +36,22 @@ const config = {
|
||||
},
|
||||
win: {
|
||||
artifactName: '${name}_${version}_${arch}_win.${ext}',
|
||||
icon: 'resources/icons/png',
|
||||
certificateFile: `${process.env.WIN_CERT_FILEPATH}`,
|
||||
certificatePassword: `${process.env.WIN_CERT_PASSWORD}`
|
||||
icon: 'resources/icons/win/icon.ico',
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64']
|
||||
}
|
||||
],
|
||||
sign: null,
|
||||
publisherName: 'Bruno Software Inc'
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
allowElevation: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"private": true,
|
||||
"main": "src/index.js",
|
||||
"author": "Anoop M D <anoop.md1421@gmail.com> (https://helloanoop.com/)",
|
||||
|
||||
@@ -565,7 +565,7 @@ class Watcher {
|
||||
`\nCould not start watcher for ${watchPath}:`,
|
||||
'ENOSPC: System limit for number of file watchers reached!',
|
||||
'Trying again with polling, this will be slower!\n',
|
||||
'Update you system config to allow more concurrently watched files with:',
|
||||
'Update your system config to allow more concurrently watched files with:',
|
||||
'"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
|
||||
);
|
||||
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread);
|
||||
|
||||
@@ -14,16 +14,11 @@ const { format } = require('url');
|
||||
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
if (isDev && process.env.ELECTRON_APP_NAME) {
|
||||
const appName = process.env.ELECTRON_APP_NAME;
|
||||
const userDataPath = path.join(app.getPath("appData"), appName);
|
||||
if (isDev && process.env.ELECTRON_USER_DATA_PATH) {
|
||||
console.debug("`ELECTRON_USER_DATA_PATH` found, modifying `userData` path: \n"
|
||||
+ `\t${app.getPath("userData")} -> ${process.env.ELECTRON_USER_DATA_PATH}`);
|
||||
|
||||
console.log("`ELECTRON_APP_NAME` found, overriding `appName` and `userData` path: \n"
|
||||
+ `\t${app.getName()} -> ${appName}\n`
|
||||
+ `\t${app.getPath("userData")} -> ${userDataPath}`);
|
||||
|
||||
app.setName(appName);
|
||||
app.setPath("userData", userDataPath);
|
||||
app.setPath('userData', process.env.ELECTRON_USER_DATA_PATH);
|
||||
}
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
|
||||
@@ -309,6 +309,27 @@ function makeAxiosInstance({
|
||||
},
|
||||
};
|
||||
|
||||
// Apply proper HTTP redirect behavior based on status code
|
||||
const statusCode = error.response.status;
|
||||
const originalMethod = (error.config.method || 'get').toLowerCase();
|
||||
|
||||
// For 301, 302, 303: change method to GET unless it was HEAD
|
||||
if ([301, 302, 303].includes(statusCode) && originalMethod !== 'head') {
|
||||
requestConfig.method = 'get';
|
||||
requestConfig.data = undefined;
|
||||
delete requestConfig.headers['content-length'];
|
||||
delete requestConfig.headers['Content-Length'];
|
||||
|
||||
delete requestConfig.headers['content-type'];
|
||||
delete requestConfig.headers['Content-Type'];
|
||||
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Changed method from ${originalMethod.toUpperCase()} to GET for ${statusCode} redirect and removed request body`,
|
||||
});
|
||||
}
|
||||
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(redirectUrl);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
@@ -316,7 +337,7 @@ function makeAxiosInstance({
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
|
||||
@@ -9,7 +9,7 @@ const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep } = require('lodash');
|
||||
const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -24,7 +24,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
@@ -317,6 +317,77 @@ const configureRequest = async (
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => {
|
||||
try {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, _request);
|
||||
// Create a clone of the request to avoid mutating the original
|
||||
const resolvedRequest = cloneDeep(_request);
|
||||
// mergeVars modifies the request in place, but we'll assign it to ensure consistency
|
||||
mergeVars(collection, resolvedRequest, requestTreePath);
|
||||
const envVars = getEnvVars(environment);
|
||||
|
||||
const globalEnvironmentVars = collection.globalEnvironmentVariables;
|
||||
const folderVars = resolvedRequest.folderVariables;
|
||||
const requestVariables = resolvedRequest.requestVariables;
|
||||
const collectionVariables = resolvedRequest.collectionVariables;
|
||||
const runtimeVars = collection.runtimeVariables;
|
||||
|
||||
// Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars
|
||||
const resolvedVars = merge(
|
||||
{},
|
||||
globalEnvironmentVars,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
folderVars,
|
||||
requestVariables,
|
||||
runtimeVars
|
||||
);
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
|
||||
|
||||
request.timeout = preferencesUtil.getRequestTimeout();
|
||||
|
||||
if (!preferencesUtil.shouldVerifyTls()) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
const collectionPath = collection.pathname;
|
||||
const processEnvVars = getProcessEnvVars(collection.uid);
|
||||
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
request,
|
||||
envVars,
|
||||
collection.runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
const response = await axiosInstance(request);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const registerNetworkIpc = (mainWindow) => {
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -341,6 +412,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@@ -353,7 +425,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -447,6 +520,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run post-response script
|
||||
const responseScript = get(request, 'script.res');
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runResponseScript(
|
||||
@@ -459,7 +533,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -482,7 +557,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const cancelTokenUid = uuid();
|
||||
const requestUid = uuid();
|
||||
// requestUid is passed when a request is triggered; defaults to uuid() if not provided (e.g., bru.runRequest())
|
||||
const requestUid = item.requestUid || uuid();
|
||||
|
||||
const runRequestByItemPathname = async (relativeItemPathname) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@@ -520,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
|
||||
try {
|
||||
await runPreRequest(
|
||||
const preRequestScriptResult = await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
@@ -533,6 +609,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (preRequestScriptResult?.results) {
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results-pre-request',
|
||||
results: preRequestScriptResult.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'pre-request-script-execution',
|
||||
requestUid,
|
||||
@@ -624,7 +710,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
|
||||
return {
|
||||
statusText: error.statusText,
|
||||
error: error.message,
|
||||
error: error.message || 'Error occured while executing the request!',
|
||||
timeline: error.timeline
|
||||
}
|
||||
}
|
||||
@@ -648,7 +734,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
|
||||
try {
|
||||
await runPostResponse(
|
||||
const postResponseScriptResult = await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
@@ -661,6 +747,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (postResponseScriptResult?.results) {
|
||||
mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results-post-response',
|
||||
results: postResponseScriptResult.results,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
});
|
||||
}
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'post-response-script-execution',
|
||||
requestUid,
|
||||
@@ -706,20 +802,42 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
let testResults = null;
|
||||
let testError = null;
|
||||
|
||||
try {
|
||||
testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
} catch (error) {
|
||||
testError = error;
|
||||
|
||||
if (error.partialResults) {
|
||||
testResults = error.partialResults;
|
||||
} else {
|
||||
testResults = {
|
||||
request,
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
globalEnvironmentVariables: request?.globalEnvironmentVariables || {},
|
||||
results: [],
|
||||
nextRequestName: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
@@ -741,6 +859,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
const testScriptExecutionEvent = {
|
||||
type: 'test-script-execution',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
errorMessage: null,
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
const errorMessage = testError?.message || 'An error occurred in test script';
|
||||
testScriptExecutionEvent.errorMessage = errorMessage;
|
||||
}
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', testScriptExecutionEvent);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -760,7 +892,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
|
||||
return {
|
||||
status: error?.status,
|
||||
error: error?.message || 'an error ocurred: debug',
|
||||
error: error?.message || 'Error occured while executing the request!',
|
||||
timeline: error?.timeline
|
||||
};
|
||||
}
|
||||
@@ -798,84 +930,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => {
|
||||
try {
|
||||
const envVars = getEnvVars(environment);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const request = prepareGqlIntrospectionRequest(endpoint, envVars, _request, collectionRoot);
|
||||
|
||||
request.timeout = preferencesUtil.getRequestTimeout();
|
||||
|
||||
if (!preferencesUtil.shouldVerifyTls()) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
const requestUid = uuid();
|
||||
const collectionPath = collection.pathname;
|
||||
const collectionUid = collection.uid;
|
||||
const runtimeVariables = collection.runtimeVariables;
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collection.uid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
request,
|
||||
envVars,
|
||||
collection.runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
const response = await axiosInstance(request);
|
||||
|
||||
await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
// handler for fetch-gql-schema
|
||||
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler)
|
||||
|
||||
ipcMain.handle(
|
||||
'renderer:run-collection-folder',
|
||||
@@ -996,6 +1052,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
|
||||
// Send pre-request test results if available
|
||||
if (preRequestScriptResult?.results) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results-pre-request',
|
||||
preRequestTestResults: preRequestScriptResult.results,
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
|
||||
if (preRequestScriptResult?.skipRequest) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'runner-request-skipped',
|
||||
@@ -1127,7 +1192,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
const postRequestScriptResult = await runPostResponse(
|
||||
const postResponseScriptResult = await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
@@ -1141,14 +1206,23 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
if (postRequestScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postRequestScriptResult.nextRequestName;
|
||||
if (postResponseScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postResponseScriptResult.nextRequestName;
|
||||
}
|
||||
|
||||
if (postRequestScriptResult?.stopExecution) {
|
||||
if (postResponseScriptResult?.stopExecution) {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
|
||||
// Send post-response test results if available
|
||||
if (postResponseScriptResult?.results) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results-post-response',
|
||||
postResponseTestResults: postResponseScriptResult.results,
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
@@ -1171,42 +1245,69 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
try {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
nextRequestName = testResults.nextRequestName;
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
nextRequestName = testResults.nextRequestName;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
} catch (testError) {
|
||||
|
||||
if (testError.partialResults && testError.partialResults.results.length > 0) {
|
||||
|
||||
// Send the partial test results
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testError.partialResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testError.partialResults.envVariables,
|
||||
runtimeVariables: testError.partialResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testError.partialResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testError.partialResults.globalEnvironmentVariables;
|
||||
}
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -1334,3 +1435,4 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
module.exports = registerNetworkIpc;
|
||||
module.exports.configureRequest = configureRequest;
|
||||
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
|
||||
module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;
|
||||
|
||||
@@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common');
|
||||
const { getIntrospectionQuery } = require('graphql');
|
||||
const { setAuthHeaders } = require('./prepare-request');
|
||||
|
||||
const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => {
|
||||
const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collectionRoot) => {
|
||||
if (endpoint && endpoint.length) {
|
||||
endpoint = interpolate(endpoint, envVars);
|
||||
endpoint = interpolate(endpoint, resolvedVars);
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
@@ -16,7 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRo
|
||||
method: 'POST',
|
||||
url: endpoint,
|
||||
headers: {
|
||||
...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])),
|
||||
...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars),
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
@@ -26,19 +26,20 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRo
|
||||
return setAuthHeaders(axiosRequest, request, collectionRoot);
|
||||
};
|
||||
|
||||
const mapHeaders = (requestHeaders, collectionHeaders) => {
|
||||
const mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => {
|
||||
const headers = {};
|
||||
|
||||
each(requestHeaders, (h) => {
|
||||
// Add collection headers first
|
||||
each(collectionHeaders, (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
headers[h.name] = interpolate(h.value, resolvedVars);
|
||||
}
|
||||
});
|
||||
|
||||
// collection headers
|
||||
each(collectionHeaders, (h) => {
|
||||
// Then add request headers, which will overwrite if names overlap
|
||||
each(requestHeaders, (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
headers[h.name] = interpolate(h.value, resolvedVars);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'bearer':
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;
|
||||
break;
|
||||
case 'digest':
|
||||
axiosRequest.digestConfig = {
|
||||
@@ -152,7 +152,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'bearer':
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;
|
||||
break;
|
||||
case 'digest':
|
||||
axiosRequest.digestConfig = {
|
||||
@@ -301,6 +301,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
name: item.name,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
@@ -27,17 +27,13 @@ class EnvironmentSecretsStore {
|
||||
});
|
||||
}
|
||||
|
||||
isValidValue(val) {
|
||||
return typeof val === 'string' && val.length >= 0;
|
||||
}
|
||||
|
||||
storeEnvSecrets(collectionPathname, environment) {
|
||||
const envVars = [];
|
||||
_.each(environment.variables, (v) => {
|
||||
if (v.secret) {
|
||||
envVars.push({
|
||||
name: v.name,
|
||||
value: this.isValidValue(v.value) ? encryptString(v.value) : ''
|
||||
value: encryptString(v.value)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,15 +10,11 @@ class GlobalEnvironmentsStore {
|
||||
});
|
||||
}
|
||||
|
||||
isValidValue(val) {
|
||||
return typeof val === 'string' && val.length >= 0;
|
||||
}
|
||||
|
||||
encryptGlobalEnvironmentVariables({ globalEnvironments }) {
|
||||
return globalEnvironments?.map(env => {
|
||||
const variables = env.variables?.map(v => ({
|
||||
...v,
|
||||
value: v?.secret ? (this.isValidValue(v.value) ? encryptString(v.value) : '') : v?.value
|
||||
value: v?.secret ? encryptString(v.value) : v?.value
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -32,7 +28,7 @@ class GlobalEnvironmentsStore {
|
||||
return globalEnvironments?.map(env => {
|
||||
const variables = env.variables?.map(v => ({
|
||||
...v,
|
||||
value: v?.secret ? (this.isValidValue(v.value) ? decryptString(v.value) : '') : v?.value
|
||||
value: v?.secret ? decryptString(v.value) : v?.value
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const moment = require('moment');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
@@ -12,7 +13,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url);
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user