mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
68 Commits
feature/pl
...
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 | ||
|
|
cb611c6510 | ||
|
|
6f9daadcfb | ||
|
|
8d5d952026 | ||
|
|
afb2d3dffd | ||
|
|
9f1aed3209 | ||
|
|
ce1110bdd4 | ||
|
|
788569a5f4 | ||
|
|
91397eaf57 | ||
|
|
c293ceefcf | ||
|
|
256f63dd38 | ||
|
|
0948964677 | ||
|
|
1b52bb27f7 | ||
|
|
3e714ab9f8 | ||
|
|
f2e9a6a502 | ||
|
|
b924e15afa | ||
|
|
b0c74909ba | ||
|
|
548a6b4319 | ||
|
|
9c9afaf78f | ||
|
|
6cde453032 | ||
|
|
8f06889996 | ||
|
|
52662f0766 | ||
|
|
5567e1b7f2 | ||
|
|
3cd18d1e16 | ||
|
|
9d3e42b5d4 | ||
|
|
0f318c26c2 | ||
|
|
6598d23ff0 | ||
|
|
c83436655c | ||
|
|
62595c519c | ||
|
|
8e91640084 | ||
|
|
0ca2891166 | ||
|
|
5000bb8db3 | ||
|
|
9927424826 | ||
|
|
ad3f5de99a | ||
|
|
2de7ba0d0c | ||
|
|
b5861dae39 |
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -113,6 +113,10 @@ jobs:
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Install dependencies for test collection environment
|
||||
run: |
|
||||
npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
|
||||
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
|
||||
|
||||
@@ -21,7 +21,7 @@ test.describe.parallel('Run Testbench Requests', () => {
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
@@ -44,6 +44,6 @@ test.describe.parallel('Run Testbench Requests', () => {
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
});
|
||||
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",
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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' ?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,9 +35,9 @@ import {
|
||||
responseReceived,
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig,
|
||||
setRequestStartTime,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
initRunRequestEvent
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -221,21 +221,26 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const itemUid = item?.uid;
|
||||
|
||||
dispatch(setRequestStartTime({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const itemCopy = cloneDeep(item || {});
|
||||
|
||||
let collectionCopy = cloneDeep(collection);
|
||||
|
||||
const itemCopy = cloneDeep(item);
|
||||
|
||||
const requestUid = uuid();
|
||||
itemCopy.requestUid = requestUid;
|
||||
|
||||
await dispatch(initRunRequestEvent({
|
||||
requestUid,
|
||||
itemUid,
|
||||
collectionUid
|
||||
}));
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
@@ -254,8 +259,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: serializedResponse
|
||||
})
|
||||
);
|
||||
@@ -266,8 +271,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
console.log('>> request cancelled');
|
||||
dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: null
|
||||
})
|
||||
);
|
||||
@@ -284,8 +289,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
response: errorResponse
|
||||
})
|
||||
);
|
||||
|
||||
@@ -276,6 +276,8 @@ export const collectionsSlice = createSlice({
|
||||
if (item) {
|
||||
item.response = null;
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -288,6 +290,7 @@ export const collectionsSlice = createSlice({
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
@@ -1954,26 +1957,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;
|
||||
}
|
||||
@@ -1981,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;
|
||||
}
|
||||
@@ -1999,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2055,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;
|
||||
@@ -2105,17 +2145,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
setRequestStartTime: (state, action) => {
|
||||
const { itemUid, collectionUid, timestamp } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.requestStartTime = timestamp;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -2309,13 +2338,13 @@ export const {
|
||||
collectionAddEnvFileEvent,
|
||||
collectionRenamedEvent,
|
||||
resetRunResults,
|
||||
initRunRequestEvent,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
setRequestStartTime,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
|
||||
@@ -7,7 +7,7 @@ const lightTheme = {
|
||||
colors: {
|
||||
text: {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
danger: '#B91C1C',
|
||||
muted: '#838383',
|
||||
purple: '#8e44ad',
|
||||
yellow: '#d97706'
|
||||
|
||||
@@ -196,4 +196,23 @@ export const getEncoding = (headers) => {
|
||||
|
||||
export const multiLineMsg = (...messages) => {
|
||||
return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n');
|
||||
}
|
||||
|
||||
export const formatSize = (bytes) => {
|
||||
// Handle invalid inputs
|
||||
if (isNaN(bytes) || typeof bytes !== 'number') {
|
||||
return '0B';
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return bytes + 'B';
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return (bytes / 1024).toFixed(1) + 'KB';
|
||||
}
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
|
||||
}
|
||||
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType } from './index';
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType, formatSize } from './index';
|
||||
|
||||
describe('common utils', () => {
|
||||
describe('normalizeFileName', () => {
|
||||
@@ -148,4 +148,40 @@ describe('common utils', () => {
|
||||
expect(getContentType(undefined)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSize', () => {
|
||||
it('should format bytes', () => {
|
||||
expect(formatSize(0)).toBe('0B');
|
||||
expect(formatSize(1023)).toBe('1023B');
|
||||
});
|
||||
|
||||
it('should format kilobytes', () => {
|
||||
expect(formatSize(1024)).toBe('1.0KB');
|
||||
expect(formatSize(1048575)).toBe('1024.0KB');
|
||||
});
|
||||
|
||||
it('should format megabytes', () => {
|
||||
expect(formatSize(1048576)).toBe('1.0MB');
|
||||
expect(formatSize(1073741823)).toBe('1024.0MB');
|
||||
});
|
||||
|
||||
it('should format gigabytes', () => {
|
||||
expect(formatSize(1073741824)).toBe('1.0GB');
|
||||
expect(formatSize(1099511627776)).toBe('1024.0GB');
|
||||
});
|
||||
|
||||
it('should format decimal values', () => {
|
||||
expect(formatSize(1126.5)).toBe('1.1KB');
|
||||
expect(formatSize(1153433.6)).toBe('1.1MB');
|
||||
expect(formatSize(1153433600)).toBe('1.1GB');
|
||||
expect(formatSize(1024.1)).toBe('1.0KB');
|
||||
expect(formatSize(1048576.1)).toBe('1.0MB');
|
||||
});
|
||||
|
||||
it('should format invalid inputs', () => {
|
||||
expect(formatSize(null)).toBe('0B');
|
||||
expect(formatSize(undefined)).toBe('0B');
|
||||
expect(formatSize(NaN)).toBe('0B');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
@@ -175,8 +201,10 @@ const builder = async (yargs) => {
|
||||
})
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
.example('$0 run folder -r', 'Run all requests in a folder recursively')
|
||||
.example('$0 run 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"',
|
||||
@@ -219,11 +247,12 @@ const builder = async (yargs) => {
|
||||
const handler = async function (argv) {
|
||||
try {
|
||||
let {
|
||||
filename,
|
||||
paths,
|
||||
cacert,
|
||||
ignoreTruststore,
|
||||
disableCookies,
|
||||
env,
|
||||
envFile,
|
||||
envVar,
|
||||
insecure,
|
||||
r: recursive,
|
||||
@@ -280,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) {
|
||||
@@ -401,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) => {
|
||||
@@ -498,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,
|
||||
@@ -539,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;
|
||||
}
|
||||
}
|
||||
@@ -550,7 +568,7 @@ const handler = async function (argv) {
|
||||
if (result?.shouldStopRunnerExecution) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
@@ -617,7 +635,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if ((summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) {
|
||||
if ((summary.failedAssertions + summary.failedTests + summary.failedPreRequestTests + summary.failedPostResponseTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) {
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -47,7 +47,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'bearer') {
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'apikey') {
|
||||
@@ -174,7 +174,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'wsse') {
|
||||
|
||||
@@ -45,10 +45,33 @@ const runSingleRequest = async function (
|
||||
) {
|
||||
const { pathname: itemPathname } = item;
|
||||
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
||||
|
||||
const logResults = (results, title) => {
|
||||
if (results?.length) {
|
||||
if (title) {
|
||||
console.log(chalk.dim(title));
|
||||
}
|
||||
each(results, (r) => {
|
||||
const message = r.description || `${r.lhsExpr}: ${r.rhsExpr}`;
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(message));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(message));
|
||||
if (r.error) {
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
let shouldStopRunnerExecution = false;
|
||||
let preRequestTestResults = [];
|
||||
let postResponseTestResults = [];
|
||||
|
||||
request = prepareRequest(item, collection);
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
@@ -103,9 +126,13 @@ const runSingleRequest = async function (
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults: result?.results || [],
|
||||
postResponseTestResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
|
||||
preRequestTestResults = result?.results || [];
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -211,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 {
|
||||
@@ -428,6 +455,8 @@ const runSingleRequest = async function (
|
||||
status: 'error',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
@@ -441,6 +470,9 @@ const runSingleRequest = async function (
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// Log pre-request test results
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests');
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
@@ -480,9 +512,11 @@ const runSingleRequest = async function (
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
postResponseTestResults = result?.results || [];
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
}
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
@@ -495,15 +529,6 @@ const runSingleRequest = async function (
|
||||
runtimeVariables,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
each(assertionResults, (r) => {
|
||||
if (r.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`));
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// run tests
|
||||
@@ -533,17 +558,12 @@ const runSingleRequest = async function (
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
}
|
||||
|
||||
if (testResults?.length) {
|
||||
each(testResults, (testResult) => {
|
||||
if (testResult.status === 'pass') {
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(testResult.description));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logResults(assertionResults, 'Assertions');
|
||||
|
||||
return {
|
||||
test: {
|
||||
@@ -566,6 +586,8 @@ const runSingleRequest = async function (
|
||||
status: 'pass',
|
||||
assertionResults,
|
||||
testResults,
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
@@ -591,7 +613,9 @@ const runSingleRequest = async function (
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
testResults: [],
|
||||
preRequestTestResults: [],
|
||||
postResponseTestResults: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -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 : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sendRequestTransformer from './send-request-transformer';
|
||||
const j = require('jscodeshift');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
|
||||
@@ -99,7 +100,14 @@ const simpleTranslations = {
|
||||
* as a separate statement, which allows a single Postman expression to be
|
||||
* transformed into multiple Bruno statements (e.g. for complex assertions).
|
||||
*/
|
||||
|
||||
const complexTransformations = [
|
||||
// pm.sendRequest transformation
|
||||
{
|
||||
pattern: 'pm.sendRequest',
|
||||
transform: sendRequestTransformer
|
||||
},
|
||||
|
||||
// pm.environment.has requires special handling
|
||||
{
|
||||
pattern: 'pm.environment.has',
|
||||
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -486,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) => {
|
||||
@@ -524,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
|
||||
try {
|
||||
await runPreRequest(
|
||||
const preRequestScriptResult = await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
@@ -537,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,
|
||||
@@ -628,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
|
||||
}
|
||||
}
|
||||
@@ -652,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,
|
||||
@@ -665,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,
|
||||
@@ -713,19 +805,39 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
let testResults = null;
|
||||
let testError = null;
|
||||
|
||||
try {
|
||||
testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
} catch (error) {
|
||||
testError = error;
|
||||
|
||||
if (error.partialResults) {
|
||||
testResults = error.partialResults;
|
||||
} else {
|
||||
testResults = {
|
||||
request,
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
globalEnvironmentVariables: request?.globalEnvironmentVariables || {},
|
||||
results: [],
|
||||
nextRequestName: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'test-results',
|
||||
@@ -747,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 {
|
||||
@@ -766,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
|
||||
};
|
||||
}
|
||||
@@ -804,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',
|
||||
@@ -1002,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',
|
||||
@@ -1133,7 +1192,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
const postRequestScriptResult = await runPostResponse(
|
||||
const postResponseScriptResult = await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
@@ -1147,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) {
|
||||
@@ -1179,42 +1247,67 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
try {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
response,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
nextRequestName = testResults.nextRequestName;
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
nextRequestName = testResults.nextRequestName;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
} catch (testError) {
|
||||
|
||||
if (testError.partialResults && testError.partialResults.results.length > 0) {
|
||||
|
||||
// Send the partial test results
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testError.partialResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testError.partialResults.envVariables,
|
||||
runtimeVariables: testError.partialResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testError.partialResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testError.partialResults.globalEnvironmentVariables;
|
||||
}
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'test-results',
|
||||
testResults: testResults.results,
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -1342,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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -89,6 +89,9 @@ function encryptString(str) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Encrypt failed: invalid string');
|
||||
}
|
||||
if (str.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let encryptedString = '';
|
||||
|
||||
@@ -104,9 +107,12 @@ function encryptString(str) {
|
||||
}
|
||||
|
||||
function decryptString(str) {
|
||||
if (!str) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Decrypt failed: unrecognized string format');
|
||||
}
|
||||
if (str.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Find the index of the first colon
|
||||
const colonIndex = str.indexOf(':');
|
||||
|
||||
@@ -324,8 +324,8 @@ function setupProxyAgents({
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));
|
||||
const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');
|
||||
const { fetchGqlSchemaHandler } = require('../../src/ipc/network');
|
||||
|
||||
// Mock only the prepare-gql-introspection-request to avoid network calls
|
||||
jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => {
|
||||
return jest.fn().mockImplementation((endpoint, vars, request, root) => {
|
||||
return {
|
||||
url: endpoint,
|
||||
method: 'POST',
|
||||
headers: request?.headers || {},
|
||||
data: {
|
||||
query: '{ __schema { types { name } } }'
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchGqlSchemaHandler - variable precedence', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should override global environment variables with environment variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: [
|
||||
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
|
||||
]
|
||||
};
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
};
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {},
|
||||
globalEnvironmentVariables: {
|
||||
SHARED_VAR: 'global-value'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [] // No collection variables
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'env-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
});
|
||||
|
||||
it('should override environment variables with folder-level variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: [
|
||||
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
|
||||
]
|
||||
};
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
};
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {},
|
||||
globalEnvironmentVariables: {},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-folder',
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'folder-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [] // No collection variables
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'folder-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
});
|
||||
|
||||
it('should override folder-level variables with request variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: []
|
||||
};
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {},
|
||||
globalEnvironmentVariables: {},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-folder',
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'folder-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [] // No collection variables
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'request-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
});
|
||||
|
||||
it('should override global environment variables with collection variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: []
|
||||
};
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
};
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {},
|
||||
globalEnvironmentVariables: {
|
||||
SHARED_VAR: 'global-value'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'collection-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'collection-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
});
|
||||
|
||||
it('should override collection variables with environment variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: [
|
||||
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
|
||||
]
|
||||
};
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
};
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {},
|
||||
globalEnvironmentVariables: {},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [] // No request variables
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'collection-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'env-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
});
|
||||
|
||||
it('should override request variables with runtime variables', async () => {
|
||||
const endpoint = 'https://example.com/';
|
||||
const environment = {
|
||||
variables: []
|
||||
};
|
||||
|
||||
const request = {
|
||||
uid: 'test-request',
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const collection = {
|
||||
uid: 'test-collection',
|
||||
pathname: '/test',
|
||||
runtimeVariables: {
|
||||
SHARED_VAR: 'runtime-value'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'test-request',
|
||||
request: {
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
root: {
|
||||
request: {
|
||||
headers: [],
|
||||
vars: {
|
||||
req: [] // No collection variables
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
|
||||
|
||||
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
|
||||
endpoint,
|
||||
expect.objectContaining({
|
||||
SHARED_VAR: 'runtime-value'
|
||||
}),
|
||||
request,
|
||||
collection.root
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');
|
||||
|
||||
describe('prepareGqlIntrospectionRequest', () => {
|
||||
const createBasicSetup = () => ({
|
||||
endpoint: 'https://example.com/',
|
||||
request: {
|
||||
headers: []
|
||||
},
|
||||
collectionRoot: {
|
||||
request: {
|
||||
headers: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle environment variables in headers', () => {
|
||||
const setup = createBasicSetup();
|
||||
setup.request.headers = [
|
||||
{ name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true }
|
||||
];
|
||||
const vars = {
|
||||
AUTH_TOKEN: 'token-value'
|
||||
};
|
||||
|
||||
const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot);
|
||||
|
||||
expect(result.headers['Authorization']).toBe('Bearer token-value');
|
||||
expect(result.method).toBe('POST');
|
||||
expect(result.url).toBe(setup.endpoint);
|
||||
});
|
||||
|
||||
it('should override collection headers with request headers', () => {
|
||||
const setup = createBasicSetup();
|
||||
setup.collectionRoot.request.headers = [
|
||||
{ name: 'X-Header', value: 'collection-value', enabled: true }
|
||||
];
|
||||
setup.request.headers = [
|
||||
{ name: 'X-Header', value: 'request-value', enabled: true }
|
||||
];
|
||||
|
||||
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
|
||||
|
||||
expect(result.headers['X-Header']).toBe('request-value');
|
||||
});
|
||||
|
||||
it('should handle enabled and disabled headers', () => {
|
||||
const setup = createBasicSetup();
|
||||
setup.request.headers = [
|
||||
{ name: 'X-Enabled', value: 'enabled', enabled: true },
|
||||
{ name: 'X-Disabled', value: 'disabled', enabled: false }
|
||||
];
|
||||
|
||||
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
|
||||
|
||||
expect(result.headers['X-Enabled']).toBe('enabled');
|
||||
expect(result.headers['X-Disabled']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should always include required GraphQL headers', () => {
|
||||
const setup = createBasicSetup();
|
||||
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
|
||||
expect(result.headers['Accept']).toBe('application/json');
|
||||
expect(result.headers['Content-Type']).toBe('application/json');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -12,13 +12,23 @@ describe('Encryption and Decryption Tests', () => {
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('should handle empty strings in encryptString', () => {
|
||||
const result = encryptString('');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty strings in decryptString', () => {
|
||||
const result = decryptString('');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('encrypt should throw an error for invalid string', () => {
|
||||
expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string');
|
||||
expect(() => encryptString(undefined)).toThrow('Encrypt failed: invalid string');
|
||||
});
|
||||
|
||||
it('decrypt should throw an error for invalid string', () => {
|
||||
expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format');
|
||||
expect(() => decryptString('')).toThrow('Decrypt failed: unrecognized string format');
|
||||
expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format');
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
const { sendRequest } = require('@usebruno/requests').scripting;
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -15,6 +16,7 @@ class Bru {
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
this.sendRequest = sendRequest;
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
|
||||
@@ -12,7 +12,10 @@ const { get } = require('lodash');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const BrunoResponse = require('../bruno-response');
|
||||
const Test = require('../test');
|
||||
const TestResults = require('../test-results');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { createBruTestResultMethods } = require('../utils/results');
|
||||
|
||||
// Inbuilt Library Support
|
||||
const ajv = require('ajv');
|
||||
@@ -57,6 +60,7 @@ class ScriptRuntime {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
@@ -78,9 +82,16 @@ class ScriptRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
// extend bru with result getter methods
|
||||
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
req
|
||||
req,
|
||||
test,
|
||||
expect: chai.expect,
|
||||
assert: chai.assert,
|
||||
__brunoTestResults: __brunoTestResults
|
||||
};
|
||||
|
||||
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
@@ -114,6 +125,7 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
@@ -168,6 +180,7 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
@@ -192,6 +205,7 @@ class ScriptRuntime {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
@@ -214,10 +228,17 @@ class ScriptRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
// extend bru with result getter methods
|
||||
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
res,
|
||||
test,
|
||||
expect: chai.expect,
|
||||
assert: chai.assert,
|
||||
__brunoTestResults: __brunoTestResults
|
||||
};
|
||||
|
||||
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
@@ -251,6 +272,7 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
@@ -305,6 +327,7 @@ class ScriptRuntime {
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution
|
||||
|
||||
@@ -16,6 +16,7 @@ const BrunoResponse = require('../bruno-response');
|
||||
const Test = require('../test');
|
||||
const TestResults = require('../test-results');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { createBruTestResultMethods } = require('../utils/results');
|
||||
|
||||
// Inbuilt Library Support
|
||||
const ajv = require('ajv');
|
||||
@@ -35,25 +36,6 @@ const cheerio = require('cheerio');
|
||||
const tv4 = require('tv4');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
const getResultsSummary = (results) => {
|
||||
const summary = {
|
||||
total: results.length,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
|
||||
results.forEach((r) => {
|
||||
const passed = r.status === "pass";
|
||||
if (passed) summary.passed += 1;
|
||||
else if (r.status === "fail") summary.failed += 1;
|
||||
else summary.skipped += 1;
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
|
||||
class TestRuntime {
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'vm2';
|
||||
@@ -99,9 +81,8 @@ class TestRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
const __brunoTestResults = new TestResults();
|
||||
|
||||
const test = Test(__brunoTestResults, chai);
|
||||
// extend bru with result getter methods
|
||||
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
||||
|
||||
if (!testsFile || !testsFile.length) {
|
||||
return {
|
||||
@@ -114,36 +95,6 @@ class TestRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
bru.getTestResults = async () => {
|
||||
let results = await __brunoTestResults.getResults();
|
||||
const summary = getResultsSummary(results);
|
||||
return {
|
||||
summary,
|
||||
results: results?.map?.(r => ({
|
||||
status: r?.status,
|
||||
description: r?.description,
|
||||
expected: r?.expected,
|
||||
actual: r?.actual,
|
||||
error: r?.error
|
||||
}))
|
||||
};
|
||||
}
|
||||
bru.getAssertionResults = async () => {
|
||||
let results = assertionResults;
|
||||
const summary = getResultsSummary(results);
|
||||
return {
|
||||
summary,
|
||||
results: results?.map?.(r => ({
|
||||
status: r?.status,
|
||||
lhsExpr: r?.lhsExpr,
|
||||
rhsExpr: r?.rhsExpr,
|
||||
operator: r?.operator,
|
||||
rhsOperand: r?.rhsOperand,
|
||||
error: r?.error
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
const context = {
|
||||
test,
|
||||
bru,
|
||||
@@ -173,56 +124,63 @@ class TestRuntime {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: testsFile,
|
||||
context: context
|
||||
});
|
||||
} else {
|
||||
// default runtime is vm2
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
external: true,
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
stream,
|
||||
util,
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
punycode,
|
||||
zlib,
|
||||
// 3rd party libs
|
||||
ajv,
|
||||
'ajv-formats': addFormats,
|
||||
btoa,
|
||||
atob,
|
||||
lodash,
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
'xml2js': xml2js,
|
||||
cheerio,
|
||||
tv4,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
let scriptError = null;
|
||||
|
||||
try {
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: testsFile,
|
||||
context: context
|
||||
});
|
||||
} else {
|
||||
// default runtime is vm2
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
external: true,
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
stream,
|
||||
util,
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
punycode,
|
||||
zlib,
|
||||
// 3rd party libs
|
||||
ajv,
|
||||
'ajv-formats': addFormats,
|
||||
btoa,
|
||||
atob,
|
||||
lodash,
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
'xml2js': xml2js,
|
||||
cheerio,
|
||||
tv4,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
});
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
}
|
||||
} catch (error) {
|
||||
scriptError = error;
|
||||
console.error('Test script execution error:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
const result = {
|
||||
request,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
@@ -230,6 +188,13 @@ class TestRuntime {
|
||||
results: cleanJson(__brunoTestResults.getResults()),
|
||||
nextRequestName: bru.nextRequest
|
||||
};
|
||||
|
||||
if (scriptError) {
|
||||
scriptError.partialResults = result;
|
||||
throw scriptError;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,10 +142,10 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
||||
|
||||
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
|
||||
|
||||
consoleFn && addConsoleShimToContext(vm, consoleFn);
|
||||
bru && addBruShimToContext(vm, bru);
|
||||
req && addBrunoRequestShimToContext(vm, req);
|
||||
res && addBrunoResponseShimToContext(vm, res);
|
||||
consoleFn && addConsoleShimToContext(vm, consoleFn);
|
||||
addLocalModuleLoaderShimToContext(vm, collectionPath);
|
||||
addPathShimToContext(vm);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { cleanJson } = require('../../../utils');
|
||||
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
||||
const { marshallToVm } = require('../utils');
|
||||
|
||||
const addBruShimToContext = (vm, bru) => {
|
||||
@@ -210,8 +210,7 @@ const addBruShimToContext = (vm, bru) => {
|
||||
bru
|
||||
.runRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
const { status, headers, data, dataBuffer, size, statusText } = response || {};
|
||||
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
|
||||
promise.resolve(marshallToVm(cleanCircularJson(response), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.resolve(
|
||||
@@ -228,6 +227,26 @@ const addBruShimToContext = (vm, bru) => {
|
||||
});
|
||||
runRequestHandle.consume((handle) => vm.setProp(bruObject, 'runRequest', handle));
|
||||
|
||||
let sendRequestHandle = vm.newFunction('_sendRequest', (args) => {
|
||||
const promise = vm.newPromise();
|
||||
bru
|
||||
.sendRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
promise.resolve(marshallToVm(cleanCircularJson(response), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.reject(
|
||||
marshallToVm(
|
||||
cleanJson(err),
|
||||
vm
|
||||
)
|
||||
);
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));
|
||||
|
||||
const sleep = vm.newFunction('sleep', (timer) => {
|
||||
const t = vm.getString(timer);
|
||||
const promise = vm.newPromise();
|
||||
@@ -242,6 +261,29 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'runner', bruRunnerObject);
|
||||
vm.setProp(vm.global, 'bru', bruObject);
|
||||
bruObject.dispose();
|
||||
|
||||
vm.evalCode(`
|
||||
globalThis.bru.sendRequest = async (requestConfig, callback) => {
|
||||
if (!callback) return await globalThis.bru._sendRequest(requestConfig);
|
||||
try {
|
||||
const response = await globalThis.bru._sendRequest(requestConfig);
|
||||
try {
|
||||
await callback(null, response);
|
||||
}
|
||||
catch(error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
try {
|
||||
await callback(JSON.parse(JSON.stringify(error)), null);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -144,10 +144,37 @@ const cleanJson = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
const cleanCircularJson = (data) => {
|
||||
try {
|
||||
// Handle circular references by keeping track of seen objects
|
||||
const seen = new WeakSet();
|
||||
|
||||
const replacer = (key, value) => {
|
||||
// Skip non-objects and null
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Detect circular reference
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
|
||||
seen.add(value);
|
||||
return value;
|
||||
};
|
||||
|
||||
return JSON.parse(JSON.stringify(data, replacer));
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
evaluateJsExpression,
|
||||
evaluateJsTemplateLiteral,
|
||||
createResponseParser,
|
||||
internalExpressionCache,
|
||||
cleanJson
|
||||
cleanJson,
|
||||
cleanCircularJson
|
||||
};
|
||||
|
||||
80
packages/bruno-js/src/utils/results.js
Normal file
80
packages/bruno-js/src/utils/results.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const TestResults = require('../test-results');
|
||||
const Test = require('../test');
|
||||
|
||||
// Calculate summary statistics for test results
|
||||
const getResultsSummary = (results) => {
|
||||
const summary = {
|
||||
total: results.length,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
|
||||
results.forEach((r) => {
|
||||
const passed = r.status === 'pass';
|
||||
if (passed) summary.passed += 1;
|
||||
else if (r.status === 'fail') summary.failed += 1;
|
||||
else summary.skipped += 1;
|
||||
});
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
const createBruTestResultMethods = (bru, assertionResults, chai) => {
|
||||
const __brunoTestResults = new TestResults();
|
||||
const test = Test(__brunoTestResults, chai);
|
||||
setupBruTestMethods(bru, __brunoTestResults, assertionResults);
|
||||
|
||||
return { __brunoTestResults, test };
|
||||
};
|
||||
|
||||
const setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => {
|
||||
const getTestResults = async () => {
|
||||
let results = await __brunoTestResults.getResults();
|
||||
const summary = getResultsSummary(results);
|
||||
return {
|
||||
summary,
|
||||
results: results.map(r => ({
|
||||
status: r.status,
|
||||
description: r.description,
|
||||
expected: r.expected,
|
||||
actual: r.actual,
|
||||
error: r.error
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
const getAssertionResults = async () => {
|
||||
let results = assertionResults;
|
||||
const summary = getResultsSummary(results);
|
||||
return {
|
||||
summary,
|
||||
results: results.map(r => ({
|
||||
status: r.status,
|
||||
lhsExpr: r.lhsExpr,
|
||||
rhsExpr: r.rhsExpr,
|
||||
operator: r.operator,
|
||||
rhsOperand: r.rhsOperand,
|
||||
error: r.error
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
// Set methods on bru object if provided
|
||||
if (bru) {
|
||||
bru.getTestResults = getTestResults;
|
||||
bru.getAssertionResults = getAssertionResults;
|
||||
}
|
||||
|
||||
// Also return the methods for direct use
|
||||
return {
|
||||
getTestResults,
|
||||
getAssertionResults
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getResultsSummary,
|
||||
createBruTestResultMethods,
|
||||
setupBruTestMethods
|
||||
};
|
||||
@@ -68,7 +68,7 @@ const mapArrayListToKeyValPairs = (arrayList = []) => {
|
||||
|
||||
return {
|
||||
name,
|
||||
value: null,
|
||||
value: '',
|
||||
enabled
|
||||
};
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ vars:secret [
|
||||
},
|
||||
{
|
||||
name: 'token',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
}
|
||||
@@ -220,19 +220,19 @@ vars:secret [
|
||||
},
|
||||
{
|
||||
name: 'access_token',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
},
|
||||
{
|
||||
name: 'access_secret',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
},
|
||||
{
|
||||
name: 'access_password',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: false,
|
||||
secret: true
|
||||
}
|
||||
@@ -262,7 +262,7 @@ vars:secret [access_key]
|
||||
},
|
||||
{
|
||||
name: 'access_key',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
}
|
||||
@@ -292,19 +292,19 @@ vars:secret [access_key,access_secret, access_password ]
|
||||
},
|
||||
{
|
||||
name: 'access_key',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
},
|
||||
{
|
||||
name: 'access_secret',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
},
|
||||
{
|
||||
name: 'access_password',
|
||||
value: null,
|
||||
value: '',
|
||||
enabled: true,
|
||||
secret: true
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"test": "jest",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w",
|
||||
"prepack": "npm run test && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"clean": "rimraf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w",
|
||||
"prepack": "npm run test && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -30,6 +31,7 @@
|
||||
"rollup": "3.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ function stripQuotes(str) {
|
||||
return str.replace(/"/g, '');
|
||||
}
|
||||
|
||||
function splitAuthHeaderKeyValue(str) {
|
||||
const indexOfEqual = str.indexOf('=');
|
||||
const key = str.substring(0, indexOfEqual).trim();
|
||||
const value = str.substring(indexOfEqual + 1);
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
function containsDigestHeader(response) {
|
||||
const authHeader = response?.headers?.['www-authenticate'];
|
||||
return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false;
|
||||
@@ -55,7 +62,7 @@ export function addDigestInterceptor(axiosInstance, request) {
|
||||
|
||||
const authDetails = error.response.headers['www-authenticate']
|
||||
.split(',')
|
||||
.map((pair) => pair.split('=').map((item) => item.trim()).map(stripQuotes))
|
||||
.map((pair) => splitAuthHeaderKeyValue(pair).map((item) => item.trim()).map(stripQuotes))
|
||||
.reduce((acc, [key, value]) => {
|
||||
const normalizedKey = key.toLowerCase().replace('digest ', '');
|
||||
if (normalizedKey && value !== undefined) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
export * as utils from './utils';
|
||||
|
||||
export * as network from './network';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
76
packages/bruno-requests/src/network/axios-instance.ts
Normal file
76
packages/bruno-requests/src/network/axios-instance.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { default as axios, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} customRequestConfig options - partial AxiosRequestConfig
|
||||
*
|
||||
* @returns {import('axios').AxiosInstance} Configured Axios instance
|
||||
*
|
||||
* @example
|
||||
* const instance = makeAxiosInstance({
|
||||
* maxRedirects: 0,
|
||||
* proxy: false,
|
||||
* headers: {
|
||||
* "User-Agent": `bruno-runtime/_version_`
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
type ModifiedInternalAxiosRequestConfig = InternalAxiosRequestConfig & {
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
type ModifiedAxiosResponse = AxiosResponse & {
|
||||
responseTime: number;
|
||||
}
|
||||
|
||||
const baseRequestConfig: Partial<AxiosRequestConfig> = {
|
||||
transformRequest: function transformRequest(data: any, headers: AxiosRequestHeaders) {
|
||||
const contentType = headers.getContentType() || '';
|
||||
const hasJSONContentType = contentType.includes('json');
|
||||
if (typeof data === 'string' && hasJSONContentType) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(axios.defaults.transformRequest)) {
|
||||
axios.defaults.transformRequest.forEach((tr) => {
|
||||
data = tr.call(this, data, headers);
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => {
|
||||
customRequestConfig = customRequestConfig || {};
|
||||
const axiosInstance = axios.create({
|
||||
...baseRequestConfig,
|
||||
...customRequestConfig
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const modifiedConfig: ModifiedInternalAxiosRequestConfig = {
|
||||
...config,
|
||||
startTime: Date.now()
|
||||
}
|
||||
return modifiedConfig;
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use((response: AxiosResponse) => {
|
||||
const config = response.config as ModifiedInternalAxiosRequestConfig;
|
||||
const startTime = config.startTime;
|
||||
const endTime = Date.now();
|
||||
const modifiedResponse: ModifiedAxiosResponse = {
|
||||
...response,
|
||||
responseTime: endTime - startTime
|
||||
};
|
||||
return modifiedResponse;
|
||||
});
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
export {
|
||||
makeAxiosInstance
|
||||
};
|
||||
1
packages/bruno-requests/src/network/index.ts
Normal file
1
packages/bruno-requests/src/network/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
1
packages/bruno-requests/src/scripting/index.ts
Normal file
1
packages/bruno-requests/src/scripting/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as sendRequest } from './send-request';
|
||||
30
packages/bruno-requests/src/scripting/send-request.ts
Normal file
30
packages/bruno-requests/src/scripting/send-request.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { makeAxiosInstance } from '../network';
|
||||
|
||||
type T_SendRequestCallback = (error: any, response: any) => void;
|
||||
|
||||
const sendRequest = async (requestConfig: AxiosRequestConfig, callback: T_SendRequestCallback) => {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
if (!callback) {
|
||||
return await axiosInstance(requestConfig);
|
||||
}
|
||||
try {
|
||||
const response = await axiosInstance(requestConfig);
|
||||
try {
|
||||
await callback(null, response);
|
||||
}
|
||||
catch(error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
await callback(error, null);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default sendRequest;
|
||||
@@ -0,0 +1,27 @@
|
||||
meta {
|
||||
name: Bearer Auth undefined
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/api/auth/bearer/protected
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{bearer_auth_token}}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.body.message: eq Unauthorized
|
||||
res.status: eq 401
|
||||
}
|
||||
|
||||
tests {
|
||||
test("selected auth overrides Authorization header always", function() {
|
||||
const authHeader = req.getHeader("Authorization")
|
||||
expect(authHeader).to.eql("Bearer ")
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ get {
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer your_secret_token
|
||||
Authorization: Bearer {{bearer_auth_token}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: send-request
|
||||
seq: 16
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: get-url-string
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
await test("send request with a get url string", async () => {
|
||||
const res = await bru.sendRequest("https://testbench-sanity.usebruno.com/ping");
|
||||
expect(res.data).to.eql('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
meta {
|
||||
name: usage-patterns
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
// pattern 1: using async/await
|
||||
await test("post request with async/await - success case", async () => {
|
||||
const res = await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
});
|
||||
expect(res.data).to.eql('ping');
|
||||
});
|
||||
|
||||
await test("post request with async/await - error case", async () => {
|
||||
try {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
expect(err.status).to.eql(404);
|
||||
}
|
||||
});
|
||||
|
||||
// pattern 2: using promise (.then/.catch)
|
||||
await test("post request with promise chain - success case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.data).to.eql('ping');
|
||||
});
|
||||
});
|
||||
|
||||
await test("post request with promise chain - error case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.status).to.eql(404);
|
||||
});
|
||||
});
|
||||
|
||||
// pattern 3: using callbacks
|
||||
await test("post request with callback - success case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
}, function(error, response) {
|
||||
expect(response.data).to.eql('ping');
|
||||
});
|
||||
});
|
||||
|
||||
await test("post request with callback - error case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
}, function(error, response) {
|
||||
expect(error.status).to.eql(404);
|
||||
});
|
||||
});
|
||||
}
|
||||
240
scripts/dev-hot-reload.js
Normal file
240
scripts/dev-hot-reload.js
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
# Bruno Development Script
|
||||
#
|
||||
# This script sets up and runs the Bruno development environment with hot-reloading.
|
||||
# It manages concurrent processes for various packages and provides cleanup on exit.
|
||||
#
|
||||
# Usage:
|
||||
# From the root of the project, run:
|
||||
# node ./scripts/dev-hot-reload.js [options]
|
||||
# or
|
||||
# npm run dev:watch -- [options]
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
// Get major version from .nvmrc (e.g. v22.1.0 -> v22)
|
||||
const NODE_VERSION = readFileSync('.nvmrc', 'utf8').trim().split('.')[0];
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
NODE_VERSION,
|
||||
ELECTRON_WATCH_PATHS: [
|
||||
'packages/**/dist/',
|
||||
'packages/bruno-electron/src/',
|
||||
'packages/bruno-lang/src/',
|
||||
'packages/bruno-lang/v2/src/',
|
||||
'packages/bruno-js/src/',
|
||||
'packages/bruno-schema/src/'
|
||||
],
|
||||
ELECTRON_START_DELAY: 10, // seconds
|
||||
NODEMON_WATCH_DELAY: 1000 // milliseconds
|
||||
};
|
||||
|
||||
const COLORS = {
|
||||
red: '\x1b[0;31m',
|
||||
green: '\x1b[0;32m',
|
||||
yellow: '\x1b[1;33m',
|
||||
blue: '\x1b[0;34m',
|
||||
nc: '\x1b[0m' // No Color
|
||||
};
|
||||
|
||||
const LOG_LEVELS = {
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR',
|
||||
DEBUG: 'DEBUG',
|
||||
SUCCESS: 'SUCCESS'
|
||||
};
|
||||
|
||||
function log(level, msg) {
|
||||
let color = COLORS.nc;
|
||||
switch (level) {
|
||||
case LOG_LEVELS.INFO:
|
||||
case LOG_LEVELS.SUCCESS: color = COLORS.green; break;
|
||||
case LOG_LEVELS.WARN: color = COLORS.yellow; break;
|
||||
case LOG_LEVELS.ERROR: color = COLORS.red; break;
|
||||
case LOG_LEVELS.DEBUG: color = COLORS.blue; break;
|
||||
}
|
||||
|
||||
const output = `${color}[${level}]${COLORS.nc} ${msg}`;
|
||||
if (level === LOG_LEVELS.ERROR) {
|
||||
console.error(output);
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
// Show help documentation
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Development Environment Setup for Bruno
|
||||
|
||||
Usage:
|
||||
From the root of the project, run:
|
||||
npm run dev:watch -- [options]
|
||||
or
|
||||
node scripts/dev-hot-reload.js [options]
|
||||
|
||||
Options:
|
||||
-s, --setup Clean all node_modules folders and re-install dependencies before starting
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
# Start development environment
|
||||
npm run dev:watch
|
||||
|
||||
# Start after cleaning node_modules
|
||||
npm run dev:watch -- --setup
|
||||
|
||||
# Show this help
|
||||
npm run dev:watch -- --help
|
||||
`);
|
||||
}
|
||||
|
||||
function commandExists(command) {
|
||||
try {
|
||||
execSync(`command -v ${command}`, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Install global NPM package if not present
|
||||
function ensureGlobalPackage(packageName) {
|
||||
if (!commandExists(packageName)) {
|
||||
log(LOG_LEVELS.INFO, `Installing ${packageName} globally...`);
|
||||
execSync(`npm install -g ${packageName}`, { stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure correct node version
|
||||
function ensureNodeVersion(requiredVersion) {
|
||||
const currentVersion = process.version;
|
||||
if (!currentVersion.includes(requiredVersion)) {
|
||||
log(LOG_LEVELS.ERROR, `Node ${requiredVersion} is required but currently installed version is ${currentVersion}`);
|
||||
log(LOG_LEVELS.ERROR, `Please install node ${requiredVersion} and try again.`);
|
||||
log(LOG_LEVELS.ERROR, `You can run 'nvm install ${requiredVersion}' to install it, or 'nvm use ${requiredVersion}' if it's already installed.`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanNodeModules() {
|
||||
log(LOG_LEVELS.INFO, 'Removing all node_modules directories...');
|
||||
execSync('find . -name "node_modules" -type d -prune -exec rm -rf {} +', { stdio: 'inherit' });
|
||||
log(LOG_LEVELS.SUCCESS, 'Node modules cleanup completed');
|
||||
}
|
||||
|
||||
function reinstallDependencies() {
|
||||
log(LOG_LEVELS.INFO, 'Re-installing dependencies...');
|
||||
execSync('npm install --legacy-peer-deps', { stdio: 'inherit' });
|
||||
log(LOG_LEVELS.SUCCESS, 'Dependencies re-installation completed');
|
||||
}
|
||||
|
||||
// Setup development environment
|
||||
function startDevelopment() {
|
||||
log(LOG_LEVELS.INFO, 'Starting development servers...');
|
||||
|
||||
const concurrently = require('concurrently');
|
||||
const watchPaths = CONFIG.ELECTRON_WATCH_PATHS.map(path => `--watch "${path}"`).join(' ');
|
||||
|
||||
// concurrently command objects: { command, name, prefixColor, env, cwd, ipc }
|
||||
const commandObjects = [
|
||||
{
|
||||
command: 'npm run watch --workspace=packages/bruno-common',
|
||||
name: 'common',
|
||||
prefixColor: 'magenta'
|
||||
},
|
||||
{
|
||||
command: 'npm run watch --workspace=packages/bruno-converters',
|
||||
name: 'converters',
|
||||
prefixColor: 'green'
|
||||
},
|
||||
{
|
||||
command: 'npm run watch --workspace=packages/bruno-query',
|
||||
name: 'query',
|
||||
prefixColor: 'blue'
|
||||
},
|
||||
{
|
||||
command: 'npm run watch --workspace=packages/bruno-graphql-docs',
|
||||
name: 'graphql',
|
||||
prefixColor: 'white'
|
||||
},
|
||||
{
|
||||
command: 'npm run watch --workspace=packages/bruno-requests',
|
||||
name: 'requests',
|
||||
prefixColor: 'gray'
|
||||
},
|
||||
{
|
||||
command: 'npm run dev:web',
|
||||
name: 'react',
|
||||
prefixColor: 'cyan'
|
||||
},
|
||||
{
|
||||
command: `sleep ${CONFIG.ELECTRON_START_DELAY} && nodemon ${watchPaths} --ext js,jsx,ts,tsx --delay ${CONFIG.NODEMON_WATCH_DELAY}ms --exec "npm run dev --workspace=packages/bruno-electron"`,
|
||||
name: 'electron',
|
||||
prefixColor: 'yellow',
|
||||
delay: CONFIG.ELECTRON_START_DELAY
|
||||
}
|
||||
];
|
||||
|
||||
const { result } = concurrently(commandObjects, {
|
||||
prefix: '[{name}: {pid}]',
|
||||
killOthers: ['failure', 'success'],
|
||||
restartTries: 3,
|
||||
restartDelay: 1000
|
||||
});
|
||||
|
||||
result
|
||||
.then(() => log(LOG_LEVELS.SUCCESS, 'All processes completed successfully'))
|
||||
.catch(err => {
|
||||
log(LOG_LEVELS.ERROR, 'Development environment failed to start');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Main function
|
||||
(async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let runSetup = false;
|
||||
|
||||
// Parse command line arguments
|
||||
for (const arg of args) {
|
||||
if (arg === '-s' || arg === '--setup') {
|
||||
runSetup = true;
|
||||
} else if (arg === '-h' || arg === '--help') {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
} else {
|
||||
log(LOG_LEVELS.ERROR, `Unknown parameter: ${arg}`);
|
||||
showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
log(LOG_LEVELS.INFO, 'Initializing Bruno development environment...');
|
||||
|
||||
// Ensure required global packages and node version
|
||||
ensureNodeVersion(CONFIG.NODE_VERSION);
|
||||
ensureGlobalPackage('nodemon');
|
||||
ensureGlobalPackage('concurrently');
|
||||
|
||||
// Run setup if requested
|
||||
if (runSetup) {
|
||||
cleanNodeModules();
|
||||
reinstallDependencies();
|
||||
}
|
||||
|
||||
// Start development environment
|
||||
startDevelopment();
|
||||
})().catch(err => {
|
||||
log(LOG_LEVELS.ERROR, 'An error occurred:');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user