Compare commits

...

87 Commits

Author SHA1 Message Date
lohit
d215cf740b fix: added input block to the cli workflow 2024-12-30 11:30:30 +05:30
lohit
78aa0d07ae Merge pull request #3707 from lohxt1/fix/multipart-tests
fix: multipart tests
2024-12-30 10:49:35 +05:30
lohit
e4574e3a56 Merge pull request #3701 from lohxt1/fix/cli-multipart
fix: cli multipart
2024-12-30 10:48:54 +05:30
lohxt1
795cd196f2 fix: multipart tests 2024-12-30 10:46:12 +05:30
lohxt1
43e892f9b0 fix: cli multipart 2024-12-27 16:50:12 +05:30
lohit
1f2bee1f90 Merge pull request #3676 from pooja-bruno/fix/string-json-response
fix: string json response error
2024-12-27 16:30:32 +05:30
Pooja Belaramani
767db75730 update: comment 2024-12-27 16:10:57 +05:30
Pooja Belaramani
b6b4b7362f fix: condition 2024-12-27 15:51:06 +05:30
Pooja Belaramani
54d8fbc478 rm: json stringfy from json string data res 2024-12-27 11:16:26 +05:30
Pooja Belaramani
9a2d8bfff3 adding quotes in text response 2024-12-27 11:12:46 +05:30
pooja-bruno
f8711a91d9 fix: plain text curl data (#3677) 2024-12-23 17:29:06 +05:30
Pooja Belaramani
582e8e5eac fix: condition 2024-12-23 13:36:58 +05:30
Pooja Belaramani
236bc48d98 fix: null data response 2024-12-23 12:11:46 +05:30
Pooja Belaramani
aea25842ce fix: string json response error 2024-12-18 15:01:06 +05:30
Anoop M D
f871bc0fa2 feat: setup script 2024-12-17 13:46:48 +05:30
lohit
db90d31b3f Merge pull request #3670 from lohxt1/fix/validations
fix: validations
2024-12-17 11:14:42 +05:30
lohxt1
e44dcad01a fix: updates 2024-12-16 22:13:26 +05:30
lohxt1
7a8d1624d1 fix: updates 2024-12-16 22:09:10 +05:30
lohxt1
0ad0af041b fix: validations updates 2024-12-16 18:46:16 +05:30
lohit
b9ec0acab4 Merge pull request #3662 from lohxt1/revert/rename-item-watcher-fix
revert: rename-item watcher fix -- tbd
2024-12-16 14:53:12 +05:30
lohit
0d126abfbd Merge branch 'main' into revert/rename-item-watcher-fix 2024-12-16 14:52:52 +05:30
lohxt1
a5096ce413 revert: rename-item watcher fix -- tbd 2024-12-16 14:51:02 +05:30
lohit
3bf98aab3b Merge pull request #3661 from Pragadesh-45/main
fix: update condition for renaming items in Windows OS
2024-12-16 12:12:24 +05:30
Pragadesh-45
9b83cd7b84 fix: update condition for renaming items in Windows OS 2024-12-16 12:01:42 +05:30
lohit
21f9e80706 Merge pull request #3660 from lohxt1/fix/add-tests-scripts-to-request
fix: add tests to request object
2024-12-16 10:14:24 +05:30
lohxt1
e8bc32b39b fix: add tests to request object 2024-12-16 10:12:25 +05:30
lohit
380047e025 Merge pull request #1043 from nelup20/bugfix/521-error_accessing_response_property_with_context_as_key
fix(#521): Allow "context" as the name of a key/var in a JS expression
2024-12-15 20:01:33 +05:30
lohit
434ae6c70f _BrunoNewFunctionInnerContext to __bruno__functionInnerContext 2024-12-15 20:00:57 +05:30
lohit
22612a7dbe _BrunoNewFunctionInnerContext to __bruno__functionInnerContext 2024-12-15 20:00:13 +05:30
lohit
06c0b7c78a Merge pull request #3659 from lohxt1/followup-pr/close-tab-after-rename-folders-only
fix: followup pr - only close folder tab on rename pr-3607
2024-12-15 19:56:32 +05:30
lohxt1
c154dec2b5 fix: followup pr - only close folder tab on rename 2024-12-15 19:55:15 +05:30
lohit
3abe611752 Merge pull request #3658 from lohxt1/followup-pr/multipart-content-type
refactor: multipart content-type pr review fixes
2024-12-15 19:15:00 +05:30
lohxt1
f3cfacdd43 fix: updates 2024-12-15 19:11:19 +05:30
lohit
af4b2105be Merge pull request #3657 from lohxt1/followup-pr/multipart-content-type
refactor: multipart content-type pr-2121
2024-12-15 18:25:01 +05:30
lohxt1
7ae64605c2 chore: updates 2024-12-15 18:19:55 +05:30
lohit
83bbbe3fb3 Merge pull request #2121 from end3rbyte/feature/1602-multipart-content-type
feature: Multi-part requests: user should be able to set content-type for each part in a multi-part request. #1602
2024-12-15 18:08:44 +05:30
lohit
73ea5f155d Merge branch 'main' into feature/1602-multipart-content-type 2024-12-15 18:08:05 +05:30
lohit
366bd99e92 Merge pull request #3642 from lohxt1/fix/remove-jsonbigint-from-cli
fix: cli -- json response parsing
2024-12-15 18:00:45 +05:30
lohit
6b6fc9a3dc Merge branch 'main' into fix/remove-jsonbigint-from-cli 2024-12-15 18:00:38 +05:30
lohit
6323b54c38 Merge pull request #3656 from sanjaikumar-bruno/move-createFormData-function
refactor createFormData function
2024-12-15 17:57:39 +05:30
Sanjai Kumar
57e6af703c refactor createFormData to handle data as an array of objects and improve file handling 2024-12-15 17:55:52 +05:30
lohit
09120a96e8 Merge pull request #3586 from sanjaikumar-bruno/fix/multipart-file-with-same-key-name
fix: refactor createFormData function
2024-12-15 17:38:49 +05:30
lohit
f1e6d5eefe add createFormData import from utils 2024-12-15 17:38:22 +05:30
lohit
ba41f17439 Merge branch 'main' into fix/multipart-file-with-same-key-name 2024-12-15 17:34:39 +05:30
lohit
d37e9aaafa Merge pull request #3522 from sanjai0py/feat/add-isNotEmpty-assertion
feat: add 'isNotEmpty' assertion operator
2024-12-15 17:28:23 +05:30
lohit
5a9bda2a0f use to.not.be.empty 2024-12-15 17:27:47 +05:30
Sanjai Kumar
eaa4f4e57b Fix/global env duplicate name error (#3590)
* fix: enhance environment name validation for global environments

* fix: improve environment name validation logic in CreateEnvironment component

* Update index.js

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-12-15 16:47:08 +05:30
_Pragadesh M
8992a457a8 Feat/displaying variable hints as secrets for Global Environment Secrets (#3591)
* feat: added new utility function `getEnvironmentVariablesMasked`

* refactor: Update `getAllVariables` function in `collections/index.js`

* refactor: Update `brunoVarInfo.js` to handle masked environment variables

* code cleanup

* feat: Add masked global environment variables functionality

* refactor: Rename variables for clarity in RequestTabPanel component

* feat: updates

* Update index.js

---------

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
2024-12-15 16:45:54 +05:30
Pragadesh-45
b181aba646 feat: enhance collection item deletion and renaming functionality (#3607) 2024-12-15 16:44:32 +05:30
Pragadesh-45
47179535d5 fix: update content-type header for XML requests to application/xml (#3648) 2024-12-15 16:40:12 +05:30
Pragadesh-45
316b632338 refactor: add sanitization for collection names and improve directory name handling (#3559) 2024-12-15 16:38:56 +05:30
Filip Gaľa
dc469afeea feat: folder documentation (#3206)
* add docs, save not working yet

* working folder docs

* revert unrelated changes

* prettier fix

* allow save folder with command

* include folder docs in `bruno-collection` json export

* docs

---------

Co-authored-by: Filip Gala <filip.gala@student.tuke.sk>
Co-authored-by: lohit <lohxt.space@gmail.com>
2024-12-15 16:33:30 +05:30
lohit
ee715a6dc6 chore: headers/vars/script merge fns refactor (#3654)
* chore: cli code refactoring
* chore: code refactoring
2024-12-15 15:40:49 +05:30
lohit
19ad0ecef7 fix: rsbuild related error fixes (#3655) 2024-12-15 15:30:58 +05:30
lohit
993424a2b8 fix: bruno-app tests (#3653) 2024-12-15 15:30:09 +05:30
Pragadesh-45
086c4c063e fix: update pathname validation to use basename for directory checks #2193 (#3550)
* fix: implement custom pathname validation #2193

* fix: update pathname validation to use basename for directory checks

* fix: improve error message for invalid pathname in collection.js
2024-12-15 15:08:36 +05:30
Sanjai Kumar
a6ac98b709 feat: add support for importing collection-level variables from PM collections (#3481)
* feat: add support for importing collection-level variables from Postman collections

* fix: add optional chaining for collection variables in Postman importer

* Update postman-collection.js

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-12-15 14:53:53 +05:30
Anoop M D
22ecd0284f chore: updated package-lock 2024-12-14 20:29:22 +05:30
lohit
33e86a9097 feat: replace nextjs with rsbuild (#3617)
* poc: bruno app rsbuild
* fix: updates

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-12-14 19:57:04 +05:30
Anoop M D
3efcdf254e fix(#3627): fix rename issues on windows os 2024-12-14 19:42:39 +05:30
lohit
ea1f385d1c fix: rename folder case insensitive (#3635)
* fix: rename folder case insensitive

* fix: add hasSubFolders utility and update folder moving logic for Windows OS

* fix: validations

* fix: updates

* fix: updates

---------

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
2024-12-14 18:55:28 +05:30
lohit
4dcaaab52c Merge branch 'main' into fix/remove-jsonbigint-from-cli 2024-12-12 17:31:59 +05:30
lohxt1
6326dc3d9c feat: updates 2024-12-12 17:28:15 +05:30
lohit
57d86eb118 Merge pull request #3624 from lohxt1/fix/cli-brutojson
fix: cli -- brutojson fn -- req script should be obj by default
2024-12-10 13:26:45 +05:30
lohxt1
85c6b2d97f fix: cli -- brutojson fn -- script obj 2024-12-10 12:57:03 +05:30
lohit
0c574aeb1e Merge pull request #3605 from lohxt1/fix/cli-updates
fix: cli - include collection level script, updated testbench
2024-12-09 18:52:23 +05:30
lohxt1
3fe0d43bdc fix: cli - collection level script, added tests 2024-12-06 22:58:02 +05:30
Anoop M D
67ead9739e chore: temporarily reverting npm cli test runs on windows 2024-12-06 19:33:53 +05:30
Anoop M D
36021b5b38 feat: run npm cli tests on macos and windows 2024-12-06 19:23:56 +05:30
lohit
1e45725ba1 Merge pull request #3601 from lohxt1/fix/cli--env
fix: cli -- envs path resolution
2024-12-06 17:51:57 +05:30
lohxt1
d4616c78c8 fix: cli envs path resolution 2024-12-06 17:47:27 +05:30
Sanjai Kumar
f110d898f5 fix: refactor createFormData function 2024-12-03 18:01:22 +05:30
Sanjai Kumar
4a4481a26f feat: add 'isNotEmpty' assertion operator 2024-11-21 12:00:25 +05:30
lohxt1
b206b70d2e feat: fix tests 2024-11-19 12:36:20 +05:30
lohxt1
d0ef70473d fix: remove json bigint from cli 2024-11-18 13:39:55 +05:30
lohxt1
df206dc4d9 fix: posthog api key as a process env var 2024-11-15 10:38:16 +05:30
busy-panda
62babef678 Merge branch 'main' into feature/1602-multipart-content-type
# Conflicts:
#	packages/bruno-lang/v2/src/jsonToBru.js
2024-09-09 11:49:38 +02:00
busy-panda
a703b84681 Merge branch 'main' into feature/1602-multipart-content-type
# Conflicts:
#	packages/bruno-lang/v2/src/bruToJson.js
#	packages/bruno-lang/v2/src/jsonToBru.js
2024-07-01 15:17:53 +02:00
busy-panda🐼🐼
9ba03a5f02 Merge branch 'usebruno:main' into feature/1602-multipart-content-type 2024-05-01 09:19:08 +02:00
busy-panda🐼🐼
cdf56fcec1 Merge branch 'usebruno:main' into feature/1602-multipart-content-type 2024-04-24 11:47:07 +02:00
busy-panda🐼🐼
f27e79cb01 Merge branch 'usebruno:main' into feature/1602-multipart-content-type 2024-04-20 10:25:02 +02:00
busy-panda
40872f6e9e Reduced the width of the Operator column in tab Assert 2024-04-19 17:14:44 +02:00
busy-panda
b7f4edac24 Reduced the width of the Operator column in tab Assert 2024-04-19 17:08:38 +02:00
busy-panda
8e99ed3258 moved assertions from Tests panel to Assert panel and 2024-04-19 17:06:49 +02:00
busy-panda
c3c91d61c8 added placeholder support to MultiLineEditor component 2024-04-19 15:01:41 +02:00
busy-panda
39f60daca7 feature: Multi-part requests: user should be able to set content-type for each part in a multi-part request. #1602 2024-04-18 15:43:09 +02:00
Nelu Platonov
dce1481185 fix(#521): Allow "context" as the name of a key/var in a JS expression 2023-11-23 23:38:43 +01:00
86 changed files with 7904 additions and 1915 deletions

View File

@@ -2,6 +2,11 @@ name: Bru CLI Tests (npm)
on:
workflow_dispatch:
inputs:
build:
description: 'Test Bru CLI (npm)'
required: true
default: 'true'
# Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168
@@ -15,7 +20,10 @@ permissions:
jobs:
test:
name: CLI Tests
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3

7655
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,7 @@
"ts-jest": "^29.0.5"
},
"scripts": {
"setup": "node ./scripts/setup.js",
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
@@ -51,6 +52,6 @@
"prepare": "husky install"
},
"overrides": {
"rollup":"3.29.5"
"rollup": "3.29.5"
}
}

View File

@@ -1,4 +1,4 @@
{
"presets": ["next/babel"],
"presets": ["@babel/preset-env"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -31,6 +31,6 @@ yarn-error.log*
# next.js
.next/
out/
dist/
.env

View File

@@ -1,22 +0,0 @@
module.exports = {
output: 'export',
reactStrictMode: false,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT,
ENV: process.env.ENV
},
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module
if (!isServer) {
config.resolve.fallback.fs = false;
}
Object.defineProperty(config, 'devtool', {
get() {
return 'source-map';
},
set() {},
});
return config;
},
};

View File

@@ -3,15 +3,15 @@
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "cross-env ENV=dev next dev -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint",
"dev": "rsbuild dev",
"build": "rsbuild build -m production",
"preview": "rsbuild preview",
"test": "jest",
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"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",
@@ -49,7 +49,6 @@
"markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "14.2.16",
"path": "^0.12.7",
"pdfjs-dist": "4.4.168",
"platform": "^1.3.6",
@@ -57,17 +56,17 @@
"prettier": "^2.7.1",
"qs": "^6.11.0",
"query-string": "^7.0.1",
"react": "18.2.0",
"react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react-dom": "19.0.0",
"react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2",
"react-pdf": "9.1.1",
"react-player": "^2.16.0",
"react-redux": "^7.2.6",
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"strip-json-comments": "^5.0.1",
@@ -79,13 +78,14 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-spread": "^7.16.7",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"@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",
"autoprefixer": "10.4.20",
"babel-loader": "^8.2.3",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"cross-env": "^7.0.3",
"css-loader": "7.1.2",
"file-loader": "^6.2.0",

View File

@@ -0,0 +1,27 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginStyledComponents } from '@rsbuild/plugin-styled-components';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
export default defineConfig({
plugins: [
pluginNodePolyfill(),
pluginReact(),
pluginStyledComponents(),
pluginSass(),
pluginBabel({
include: /\.(?:js|jsx|tsx)$/,
babelLoaderOptions(opts) {
opts.plugins?.unshift('babel-plugin-react-compiler');
}
})
],
source: {
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
},
html: {
title: 'Bruno'
},
});

View File

@@ -11,7 +11,6 @@ const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
// todo: Add this to global env too.
const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
};

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,63 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
const Documentation = ({ collection, folder }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [isEditing, setIsEditing] = useState(false);
const docs = get(folder, 'root.docs', '');
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
const onEdit = (value) => {
dispatch(
updateFolderDocs({
folderUid: folder.uid,
collectionUid: collection.uid,
docs: value
})
);
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
if (!folder) {
return null;
}
return (
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
{isEditing ? (
<CodeEditor
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}
</StyledWrapper>
);
};
export default Documentation;

View File

@@ -7,6 +7,7 @@ import Script from './Script';
import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
import Documentation from './Documentation';
import DotIcon from 'components/Icons/Dot';
const ContentIndicator = () => {
@@ -60,6 +61,9 @@ const FolderSettings = ({ collection, folder }) => {
case 'vars': {
return <Vars collection={collection} folder={folder} />;
}
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
}
};
@@ -89,6 +93,9 @@ const FolderSettings = ({ collection, folder }) => {
Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
</div>
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
</div>

View File

@@ -2,12 +2,19 @@ import React, { useEffect, useRef } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
const validateEnvironmentName = (name) => {
const trimmedName = name?.toLowerCase().trim();
return globalEnvs.every((env) => env?.name?.toLowerCase().trim() !== trimmedName);
};
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
@@ -17,9 +24,10 @@ const CreateEnvironment = ({ onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
.min(1, 'Must be at least 1 character')
.max(50, 'Must be 50 characters or less')
.required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}),
onSubmit: (values) => {
dispatch(addGlobalEnvironment({ name: values.name }))

View File

@@ -13,6 +13,12 @@ const StyledWrapper = styled.div`
line-height: 30px;
overflow: hidden;
pre.CodeMirror-placeholder {
color: ${(props) => props.theme.text};
padding-left: 0;
opacity: 0.5;
}
.CodeMirror-scroll {
overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */}

View File

@@ -30,6 +30,7 @@ class MultiLineEditor extends Component {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
placeholder: this.props.placeholder,
mode: 'brunovariables',
brunoVarInfo: {
variables

View File

@@ -1,8 +1,6 @@
import { createPortal } from 'react-dom';
function Portal({ children, wrapperId }) {
wrapperId = wrapperId || 'bruno-app-body';
return createPortal(children, document.getElementById(wrapperId));
function Portal({ children }) {
return createPortal(children, document.body);
}
export default Portal;

View File

@@ -20,6 +20,7 @@ import React from 'react';
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
@@ -51,6 +52,7 @@ const AssertionOperator = ({ operator, onChange }) => {
'endsWith',
'between',
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',

View File

@@ -24,6 +24,7 @@ import { useTheme } from 'providers/Theme';
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
@@ -61,6 +62,7 @@ const parseAssertionOperator = (str = '') => {
'endsWith',
'between',
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
@@ -75,6 +77,7 @@ const parseAssertionOperator = (str = '') => {
const unaryOperators = [
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
@@ -113,6 +116,7 @@ const parseAssertionOperator = (str = '') => {
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',

View File

@@ -20,8 +20,8 @@ const Wrapper = styled.div`
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
&:nth-child(2) {
width: 130px;
}
&:nth-child(4) {

View File

@@ -24,7 +24,15 @@ const Wrapper = styled.div`
width: 30%;
}
&:nth-child(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}

View File

@@ -54,6 +54,10 @@ const MultipartFormParams = ({ item, collection }) => {
param.value = e.target.value;
break;
}
case 'contentType': {
param.contentType = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
@@ -85,6 +89,7 @@ const MultipartFormParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Content-Type</td>
<td></td>
</tr>
</thead>
@@ -145,6 +150,27 @@ const MultipartFormParams = ({ item, collection }) => {
/>
)}
</td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input

View File

@@ -70,7 +70,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const handleGenerateCode = (e) => {
e.stopPropagation();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true);
} else {
toast.error('URL is required');

View File

@@ -20,7 +20,7 @@ import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings';
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
import { produce } from 'immer';
const MIN_LEFT_PANE_WIDTH = 300;
@@ -45,7 +45,9 @@ const RequestTabPanel = () => {
if (collection) {
// add selected global env variables to the collection object
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
collection.globalEnvironmentVariables = globalEnvironmentVariables;
collection.globalEnvSecrets = globalEnvSecrets;
}
});

View File

@@ -20,14 +20,14 @@ const formatResponse = (data, mode, filter) => {
}
if (data === null) {
return data;
return 'null';
}
if (mode.includes('json')) {
let isValidJSON = false;
try {
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object';
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
} catch (error) {
console.log('Error parsing JSON: ', error.message);
}

View File

@@ -12,10 +12,15 @@ const DeleteCollectionItem = ({ onClose, item, collection }) => {
const isFolder = isItemAFolder(item);
const onConfirm = () => {
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
if (isFolder) {
// close all tabs that belong to the folder
// including the folder itself and its children
const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid]
dispatch(
closeTabs({
tabUids: recursivelyGetAllItemUids(item.items)
tabUids: tabUids
})
);
} else {

View File

@@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -33,7 +34,8 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
}
dispatch(renameItem(values.name, item.uid, collection.uid))
.then(() => {
toast.success('Request renamed');
isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
onClose();
})
.catch((err) => {

View File

@@ -183,7 +183,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const handleGenerateCode = (e) => {
e.stopPropagation();
dropdownTippyRef.current.hide();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true);
} else {
toast.error('URL is required');

View File

@@ -63,16 +63,16 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => {
[activeColumnIndex, columns, minColumnWidth]
);
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners();
}, [removeListeners]);
const removeListeners = useCallback(() => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', removeListeners);
}, [handleMouseMove]);
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners?.();
}, [removeListeners]);
useEffect(() => {
if (activeColumnIndex !== null) {
window.addEventListener('mousemove', handleMouseMove);

View File

@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './pages/index';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}

View File

@@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/search/search');
require('codemirror/addon/search/searchcursor');
require('codemirror/addon/display/placeholder');
require('codemirror/keymap/sublime');
require('codemirror-graphql/hint');

View File

@@ -25,31 +25,7 @@ import '@fontsource/inter/900.css';
import { setupPolyfills } from 'utils/common/setupPolyfills';
setupPolyfills();
function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
}
function NoSsr({ children }) {
const SERVER_RENDERED = typeof window === 'undefined';
if (SERVER_RENDERED) {
return null;
}
return <>{children}</>;
}
function MyApp({ Component, pageProps }) {
const [domLoaded, setDomLoaded] = useState(false);
useEffect(() => {
setDomLoaded(true);
}, []);
if (!domLoaded) {
return null;
}
function Main({ children }) {
if (!window.ipcRenderer) {
return (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert">
@@ -65,23 +41,21 @@ function MyApp({ Component, pageProps }) {
return (
<ErrorBoundary>
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>
</SafeHydrate>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
{children}
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</ErrorBoundary>
);
}
export default MyApp;
export default Main;

View File

@@ -1,41 +0,0 @@
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head />
<body id="bruno-app-body">
<Main />
<NextScript />
</body>
</Html>
);
}
}

View File

@@ -1,20 +1,16 @@
import Head from 'next/head';
import Bruno from './Bruno';
import GlobalStyle from '../globalStyles';
import '../i18n';
import Main from './Main';
export default function Home() {
export default function App() {
return (
<div>
<Head>
<title>Bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GlobalStyle />
<main>
<Bruno />
<Main>
<GlobalStyle />
<Bruno />
</Main>
</main>
</div>
);

View File

@@ -7,21 +7,19 @@
*/
import { useEffect } from 'react';
import getConfig from 'next/config';
import { PostHog } from 'posthog-node';
import platformLib from 'platform';
import { uuid } from 'utils/common';
const { publicRuntimeConfig } = getConfig();
const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
let posthogClient = null;
const isPlaywrightTestRunning = () => {
return publicRuntimeConfig.PLAYWRIGHT ? true : false;
return process.env.PLAYWRIGHT ? true : false;
};
const isDevEnv = () => {
return publicRuntimeConfig.ENV === 'dev';
return import.meta.env.MODE === 'development';
};
const getPosthogClient = () => {

View File

@@ -6,7 +6,12 @@ import { useSelector, useDispatch } from 'react-redux';
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import {
sendRequest,
saveRequest,
saveCollectionRoot,
saveFolderRoot
} from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
import { getKeyBindingsForActionAllOS } from './keyMappings';
@@ -43,7 +48,11 @@ export const HotkeysProvider = (props) => {
if (collection) {
const item = findItemInCollection(collection, activeTab.uid);
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
if (activeTab.type === 'folder-settings') {
dispatch(saveFolderRoot(collection.uid, item.uid));
} else {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
}
} else if (activeTab.type === 'collection-settings') {
dispatch(saveCollectionRoot(collection.uid));
}

View File

@@ -1,4 +1,3 @@
import getConfig from 'next/config';
import { configureStore } from '@reduxjs/toolkit';
import tasksMiddleware from './middlewares/tasks/middleware';
import debugMiddleware from './middlewares/debug/middleware';
@@ -8,9 +7,8 @@ import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications';
import globalEnvironmentsReducer from './slices/global-environments';
const { publicRuntimeConfig } = getConfig();
const isDevEnv = () => {
return publicRuntimeConfig.ENV === 'dev';
return import.meta.env.MODE === 'development';
};
let middleware = [tasksMiddleware.middleware];

View File

@@ -1,6 +1,5 @@
import { uuid } from 'utils/common';
import path from 'path';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
import { createSlice } from '@reduxjs/toolkit';
import {
addDepth,
@@ -13,6 +12,7 @@ import {
findEnvironmentInCollection,
findItemInCollection,
findItemInCollectionByPathname,
isItemAFolder,
isItemARequest
} from 'utils/collections';
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
@@ -765,6 +765,7 @@ export const collectionsSlice = createSlice({
name: '',
value: action.payload.value,
description: '',
contentType: '',
enabled: true
});
}
@@ -786,6 +787,7 @@ export const collectionsSlice = createSlice({
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
param.contentType = action.payload.param.contentType;
param.enabled = action.payload.param.enabled;
}
}
@@ -1733,6 +1735,15 @@ export const collectionsSlice = createSlice({
item.draft.request.docs = action.payload.docs;
}
}
},
updateFolderDocs: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
if (isItemAFolder(folder)) {
set(folder, 'root.docs', action.payload.docs);
}
}
}
}
});
@@ -1827,7 +1838,8 @@ export const {
runRequestEvent,
runFolderEvent,
resetCollectionRunner,
updateRequestDocs
updateRequestDocs,
updateFolderDocs
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@@ -191,7 +191,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
// update existing values
variables = variables?.map?.(variable => ({
...variable,
value: stringifyIfNot(globalEnvironmentVariables?.[variable?.name])
value: globalEnvironmentVariables?.[variable?.name]
}));
// add new env values
@@ -201,7 +201,7 @@ export const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) =>
variables.push({
uid: uuid(),
name: key,
value: stringifyIfNot(value),
value,
type: 'text',
secret: false,
enabled: true

View File

@@ -35,7 +35,6 @@ export const notificationSlice = createSlice({
state.loading = action.payload.fetching;
},
setNotifications: (state, action) => {
console.log('notifications', notifications);
let notifications = action.payload.notifications || [];
let readNotificationIds = state.readNotificationIds;

View File

@@ -93,9 +93,6 @@ if (!SERVER_RENDERED) {
const box = target.getBoundingClientRect();
const hoverTime = getHoverTime(cm);
state.hoverTimeout = setTimeout(onHover, hoverTime);
const onMouseMove = function () {
clearTimeout(state.hoverTimeout);
state.hoverTimeout = setTimeout(onHover, hoverTime);
@@ -115,6 +112,9 @@ if (!SERVER_RENDERED) {
onMouseHover(cm, box);
};
const hoverTime = getHoverTime(cm);
state.hoverTimeout = setTimeout(onHover, hoverTime);
CodeMirror.on(document, 'mousemove', onMouseMove);
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
}

View File

@@ -404,7 +404,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
request: {}
};
let { request, meta } = si?.root || {};
let { request, meta, docs } = si?.root || {};
let { headers, script = {}, vars = {}, tests } = request || {};
// folder level headers
@@ -436,6 +436,11 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
di.root.request.tests = tests;
}
// folder level docs
if (docs?.length) {
di.root.docs = docs;
}
if (meta?.name) {
di.root.meta = {};
di.root.meta.name = meta?.name;
@@ -792,7 +797,7 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
if (environment) {
each(environment.variables, (variable) => {
if (variable.name && variable.value && variable.enabled) {
if (variable.name && variable.enabled) {
variables[variable.name] = variable.value;
}
});
@@ -800,6 +805,19 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
return variables;
};
export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
if (environment && Array.isArray(environment.variables)) {
return environment.variables
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
.map((variable) => variable.name);
}
return [];
};
export const getEnvironmentVariables = (collection) => {
let variables = {};
if (collection) {
@@ -873,8 +891,22 @@ export const getAllVariables = (collection, item) => {
...requestVariables,
...runtimeVariables
};
const maskedEnvVariables = getEnvironmentVariablesMasked(collection);
const mergedVariablesGlobal = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
}
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];
const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));
const filteredMaskedGlobalEnvVariables = maskedGlobalEnvVariables.filter((key) => !(key in mergedVariablesGlobal));
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
return {
...globalEnvironmentVariables,
@@ -886,7 +918,7 @@ export const getAllVariables = (collection, item) => {
pathParams: {
...pathParams
},
maskedEnvVariables: filteredMaskedEnvVariables,
maskedEnvVariables: uniqueMaskedVariables,
process: {
env: {
...processEnvVariables

View File

@@ -57,7 +57,7 @@ function getDataString(request) {
console.error('Failed to parse JSON data:', error);
return { data: request.data.toString() };
}
} else if (contentType && contentType.includes('application/xml')) {
} else if (contentType && (contentType.includes('application/xml') || contentType.includes('text/plain'))) {
return { data: request.data };
}
@@ -174,14 +174,14 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
if(request.auth.mode === 'basic'){
if (request.auth.mode === 'basic') {
requestJson.auth = {
mode: 'basic',
basic: {
username: repr(request.auth.basic?.username),
password: repr(request.auth.basic?.password)
}
}
};
}
}

View File

@@ -79,9 +79,9 @@ export const transformItemsInCollection = (collection) => {
// from 5 feb 2024, multipartFormData needs to have a type
// this was introduced when we added support for file uploads
// below logic is to make older collection exports backward compatible
let multipartFormData = _.get(item, 'request.body.multipartForm');
let multipartFormData = get(item, 'request.body.multipartForm');
if (multipartFormData) {
_.each(multipartFormData, (form) => {
each(multipartFormData, (form) => {
if (!form.type) {
form.type = 'text';
}

View File

@@ -166,6 +166,17 @@ const importScriptsFromEvents = (events, requestObject, options, pushTranslation
});
};
const importCollectionLevelVariables = (variables, requestObject) => {
const vars = variables.map((v) => ({
uid: uuid(),
name: v.key,
value: v.value,
enabled: true
}));
requestObject.vars.req = vars;
};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
@@ -494,6 +505,10 @@ const importPostmanV2Collection = (collection, options) => {
importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
}
if (collection?.variable){
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
}
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
return brunoCollection;

View File

@@ -37,7 +37,6 @@
"fs-extra": "^10.1.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",

View File

@@ -94,7 +94,7 @@ const printRunSummary = (results) => {
};
const createCollectionFromPath = (collectionPath) => {
const environmentsPath = `${collectionPath}/environments`;
const environmentsPath = path.join(collectionPath, `environments`);
const getFilesInOrder = (collectionPath) => {
let collection = {
pathname: collectionPath

View File

@@ -24,7 +24,7 @@ const EXIT_STATUS = {
// Invalid output format requested
ERROR_INCORRECT_OUTPUT_FORMAT: 9,
// Everything else
ERROR_GENERIC: 255,
ERROR_GENERIC: 255
};
module.exports = {

View File

@@ -86,11 +86,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
} catch (err) {}
}
} else if (contentType === 'multipart/form-data') {
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
try {
forOwn(request?.data, (value, key) => {
request.data[key] = _interpolate(value);
});
request.data = request?.data?.map(d => ({
...d,
value: _interpolate(d?.value)
}));
} catch (err) {}
}
} else {

View File

@@ -1,214 +1,8 @@
const { get, each, filter, find, compact } = require('lodash');
const fs = require('fs');
const os = require('os');
var JSONbig = require('json-bigint');
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
if (combinedPreReqScript.length) {
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
}
if (combinedPostResScript.length) {
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
}
if (combinedTests.length) {
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
}
};
const findItem = (items = [], pathname) => {
return find(items, (i) => i.pathname === pathname);
};
const findItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return findItem(flattenedItems, pathname);
};
const findParentItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return find(flattenedItems, (item) => {
return item.items && find(item.items, (i) => i.pathname === pathname);
});
};
const flattenItems = (items = []) => {
const flattenedItems = [];
const flatten = (itms, flattened) => {
each(itms, (i) => {
flattened.push(i);
if (i.items && i.items.length) {
flatten(i.items, flattened);
}
});
};
flatten(items, flattenedItems);
return flattenedItems;
};
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item.pathname);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item.pathname);
}
return path;
};
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
const { createFormData } = require('../utils/form-data');
const prepareRequest = (item = {}, collection = {}) => {
const request = item?.request;
@@ -237,7 +31,8 @@ const prepareRequest = (item = {}, collection = {}) => {
method: request.method,
url: request.url,
headers: headers,
pathParams: request?.params?.filter((param) => param.type === 'path')
pathParams: request?.params?.filter((param) => param.type === 'path'),
responseType: 'arraybuffer'
};
const collectionAuth = get(collection, 'root.request.auth');
@@ -302,16 +97,10 @@ const prepareRequest = (item = {}, collection = {}) => {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
let jsonBody;
try {
jsonBody = decomment(request?.body?.json);
axiosRequest.data = decomment(request?.body?.json);
} catch (error) {
jsonBody = request?.body?.json;
}
try {
axiosRequest.data = JSONbig.parse(jsonBody);
} catch (error) {
axiosRequest.data = jsonBody;
axiosRequest.data = request?.body?.json;
}
}
@@ -324,7 +113,7 @@ const prepareRequest = (item = {}, collection = {}) => {
if (request.body.mode === 'xml') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/xml';
axiosRequest.headers['content-type'] = 'application/xml';
}
axiosRequest.data = request.body.xml;
}
@@ -343,13 +132,11 @@ const prepareRequest = (item = {}, collection = {}) => {
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
}
if (request.body.mode === 'multipartForm') {
axiosRequest.headers['content-type'] = 'multipart/form-data';
const params = {};
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
axiosRequest.data = enabledParams;
}
if (request.body.mode === 'graphql') {

View File

@@ -19,8 +19,9 @@ const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const path = require('path');
const { createFormData } = require('../utils/common');
const { parseDataFromResponse } = require('../utils/common');
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
const { createFormData } = require('../utils/form-data');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const onConsoleLog = (type, args) => {
@@ -227,6 +228,9 @@ const runSingleRequest = async function (
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
@@ -237,6 +241,8 @@ const runSingleRequest = async function (
}
} catch (err) {
if (err?.response) {
const { data } = parseDataFromResponse(err?.response);
err.response.data = data;
response = err.response;
// Prevents the duration on leaking to the actual result

View File

@@ -58,7 +58,7 @@ const bruToJson = (bru) => {
body: _.get(json, 'body', {}),
vars: _.get(json, 'vars', []),
assertions: _.get(json, 'assertions', []),
script: _.get(json, 'script', ''),
script: _.get(json, 'script', {}),
tests: _.get(json, 'tests', '')
}
};

View File

@@ -0,0 +1,208 @@
const { get, each, find, compact } = require('lodash');
const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
};
const findItem = (items = [], pathname) => {
return find(items, (i) => i.pathname === pathname);
};
const findItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return findItem(flattenedItems, pathname);
};
const findParentItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return find(flattenedItems, (item) => {
return item.items && find(item.items, (i) => i.pathname === pathname);
});
};
const flattenItems = (items = []) => {
const flattenedItems = [];
const flatten = (itms, flattened) => {
each(itms, (i) => {
flattened.push(i);
if (i.items && i.items.length) {
flatten(i.items, flattened);
}
});
};
flatten(items, flattenedItems);
return flattenedItems;
};
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item.pathname);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item.pathname);
}
return path;
};
module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
getTreePathFromCollectionToItem
}

View File

@@ -2,6 +2,7 @@ const fs = require('fs');
const FormData = require('form-data');
const { forOwn } = require('lodash');
const path = require('path');
const iconv = require('iconv-lite');
const lpad = (str, width) => {
let paddedStr = str;
@@ -19,33 +20,34 @@ const rpad = (str, width) => {
return paddedStr;
};
const createFormData = (datas, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forOwn(datas, (value, key) => {
if (typeof value == 'string') {
form.append(key, value);
return;
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
const charsetValue = charsetMatch?.[1];
const dataBuffer = Buffer.from(response.data);
// Overwrite the original data for backwards compatibility
let data;
if (iconv.encodingExists(charsetValue)) {
data = iconv.decode(dataBuffer, charsetValue);
} else {
data = iconv.decode(dataBuffer, 'utf-8');
}
// Try to parse response to JSON, this can quietly fail
try {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch { }
const filePaths = value || [];
filePaths?.forEach?.((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
});
});
return form;
return { data, dataBuffer };
};
module.exports = {
lpad,
rpad,
createFormData
parseDataFromResponse
};

View File

@@ -0,0 +1,42 @@
const { forEach } = require('lodash');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
const createFormData = (data, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forEach(data, (datum) => {
const { name, type, value, contentType } = datum;
let options = {};
if (contentType) {
options.contentType = contentType;
}
if (type === 'text') {
if (Array.isArray(value)) {
value.forEach((val) => form.append(name, val, options));
} else {
form.append(name, value, options);
}
return;
}
if (type === 'file') {
const filePaths = value || [];
filePaths.forEach((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
options.filename = path.basename(trimmedFilePath);
form.append(name, fs.createReadStream(trimmedFilePath), options);
});
}
});
return form;
};
module.exports = {
createFormData
}

View File

@@ -6,14 +6,18 @@ describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
it('If request body is valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
const expected = { test: '{{someVar}}' };
const expected = `{
\"test\": \"{{someVar}}\"
}`;
const result = prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});
it('If request body is not valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
const expected = '{\n"test": {{someVar}} \n}';
const expected = `{
\"test\": {{someVar}}
}`;
const result = prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});

View File

@@ -389,6 +389,8 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
};
const unlink = (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher unlink: ${pathname}`);
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return unlinkEnvironmentFile(win, pathname, collectionUid);
}
@@ -506,6 +508,33 @@ class Watcher {
this.watchers[watchPath] = null;
}
}
getWatcherByItemPath(itemPath) {
const paths = Object.keys(this.watchers);
const watcherPath = paths?.find(collectionPath => {
const absCollectionPath = path.resolve(collectionPath);
const absItemPath = path.resolve(itemPath);
return absItemPath.startsWith(absCollectionPath);
});
return watcherPath ? this.watchers[watcherPath] : null;
}
unlinkItemPathInWatcher(itemPath) {
const watcher = this.getWatcherByItemPath(itemPath);
if (watcher) {
watcher.unwatch(itemPath);
}
}
addItemPathInWatcher(itemPath) {
const watcher = this.getWatcherByItemPath(itemPath);
if (watcher && !watcher?.has?.(itemPath)) {
watcher?.add?.(itemPath);
}
}
}
module.exports = Watcher;

View File

@@ -20,8 +20,10 @@ const {
normalizeWslPath,
normalizeAndResolvePath,
safeToRename,
sanitizeCollectionName,
isWindowsOS,
isValidFilename
isValidFilename,
hasSubDirectories,
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@@ -67,6 +69,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
'renderer:create-collection',
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
collectionName = sanitizeCollectionName(collectionName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath);
@@ -75,9 +79,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`collection: ${dirPath} already exists and is not empty`);
}
}
if (!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
if (!isValidPathname(path.basename(dirPath))) {
throw new Error(`collection: invalid pathname - ${dirPath}`);
}
if (!fs.existsSync(dirPath)) {
@@ -105,13 +108,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle(
'renderer:clone-collection',
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
collectionFolderName = sanitizeCollectionName(collectionFolderName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
if (!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
if (!isValidPathname(path.basename(dirPath))) {
throw new Error(`collection: invalid pathname - ${dirPath}`);
}
// create dir
@@ -150,6 +154,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
try {
newName = sanitizeCollectionName(newName);
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
const json = JSON.parse(content);
@@ -343,6 +348,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
// const parentDir = path.dirname(oldPath);
const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
// let parentDirUnwatched = false;
// let parentDirRewatched = false;
try {
// Normalize paths if they are WSL paths
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
@@ -365,15 +376,29 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
moveRequestUid(bruFile, newBruFilePath);
}
if (isWindowsOS() && !isWSLPath(oldPath)) {
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
// watcher.unlinkItemPathInWatcher(parentDir);
// parentDirUnwatched = true;
/**
* If it is windows OS
* And it is not WSL path (meaning its not linux running on windows using WSL)
* And it has sub directories
* Only then we need to use the temp dir approach to rename the folder
*
* Windows OS would sometimes throw error when renaming a folder with sub directories
* This is a alternative approach to avoid that error
*/
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
await fsExtra.copy(oldPath, tempDir);
await fsExtra.move(tempDir, newPath, { overwrite: true });
await fsExtra.remove(oldPath);
await fsExtra.move(tempDir, newPath, { overwrite: true });
await fsExtra.remove(tempDir);
} else {
await fs.renameSync(oldPath, newPath);
}
// watcher.addItemPathInWatcher(parentDir);
// parentDirRewatched = true;
return newPath;
}
@@ -397,12 +422,33 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return newPath;
} catch (error) {
// in case an error occurs during the rename file operations after unlinking the parent dir
// and the rewatch fails, we need to add it back to watcher
// if (parentDirUnwatched && !parentDirRewatched) {
// watcher.addItemPathInWatcher(parentDir);
// }
// in case the rename file operations fails, and we see that the temp dir exists
// and the old path does not exist, we need to restore the data from the temp dir to the old path
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {
try {
await fsExtra.copy(tempDir, oldPath);
await fsExtra.remove(tempDir);
} catch (err) {
console.error("Failed to restore data to the old path:", err);
}
}
}
return Promise.reject(error);
}
});
// new folder
ipcMain.handle('renderer:new-folder', async (event, pathname) => {
const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname));
pathname = path.join(path.dirname(pathname), resolvedFolderName);
try {
if (!fs.existsSync(pathname)) {
fs.mkdirSync(pathname);
@@ -461,7 +507,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try {
let collectionName = sanitizeDirectoryName(collection.name);
let collectionName = sanitizeCollectionName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)) {
@@ -477,6 +523,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.writeFileSync(filePath, content);
}
if (item.type === 'folder') {
item.name = sanitizeDirectoryName(item.name);
const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath);

View File

@@ -38,7 +38,7 @@ const {
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
const FormData = require('form-data');
const { createFormData } = prepareRequest;
const { createFormData } = require('../../utils/form-data');
const safeStringifyJSON = (data) => {
try {
@@ -365,11 +365,15 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
try {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
data = data.replace(/^\uFEFF/, '');
if (!disableParsingResponseJson) {
if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) {
data = JSON.parse(data);
}
} catch { }
} catch {
console.log('Failed to parse response data as JSON');
}
return { data, dataBuffer };
};
@@ -397,7 +401,7 @@ const registerNetworkIpc = (mainWindow) => {
) => {
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
const requestScript = get(request, 'script.req');
if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runRequestScript(
@@ -493,12 +497,7 @@ const registerNetworkIpc = (mainWindow) => {
}
// run post-response script
const responseScript = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.script.res'), get(request, 'script.res')
] : [
get(request, 'script.res'), get(collectionRoot, 'request.script.res')
]).join(os.EOL);
const responseScript = get(request, 'script.res');
let scriptResult;
if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
@@ -675,14 +674,7 @@ const registerNetworkIpc = (mainWindow) => {
});
}
// run tests
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript,
] : [
testScript, get(collectionRoot, 'request.tests')
]).join(os.EOL);
const testFile = get(request, 'tests');
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(
@@ -1147,14 +1139,7 @@ const registerNetworkIpc = (mainWindow) => {
});
}
// run tests
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript
] : [
testScript, get(collectionRoot, 'request.tests')
]).join(os.EOL);
const testFile = get(request, 'tests');
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(

View File

@@ -86,11 +86,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
} catch (err) {}
}
} else if (contentType === 'multipart/form-data') {
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
try {
forOwn(request?.data, (value, key) => {
request.data[key] = _interpolate(value);
});
request.data = request?.data?.map(d => ({
...d,
value: _interpolate(d?.value)
}));
} catch (err) {}
}
} else {

View File

@@ -1,194 +1,8 @@
const os = require('os');
const { get, each, filter, compact, forOwn } = require('lodash');
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
const crypto = require('node:crypto');
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
const { buildFormUrlEncodedPayload } = require('../../utils/common');
const mergeFolderLevelHeaders = (request, requestTreePath) => {
let folderHeaders = new Map();
for (let i of requestTreePath) {
if (i.type === 'folder') {
let headers = get(i, 'root.request.headers', []);
headers.forEach((header) => {
if (header.enabled) {
folderHeaders.set(header.name, header.value);
}
});
} else if (i.uid === request.uid) {
const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
headers.forEach((header) => {
if (header.enabled) {
folderHeaders.set(header.name, header.value);
}
});
}
}
let mergedFolderHeaders = Array.from(folderHeaders, ([name, value]) => ({ name, value, enabled: true }));
let requestHeaders = request.headers || [];
let requestHeadersMap = new Map();
for (let header of requestHeaders) {
if (header.enabled) {
requestHeadersMap.set(header.name, header.value);
}
}
mergedFolderHeaders.forEach((header) => {
requestHeadersMap.set(header.name, header.value);
});
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
};
const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
let folderCombinedPreReqScript = [];
let folderCombinedPostResScript = [];
let folderCombinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
folderCombinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
folderCombinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
folderCombinedTests.push(tests);
}
}
}
if (folderCombinedPreReqScript.length) {
request.script.req = compact([...folderCombinedPreReqScript, request?.script?.req || '']).join(os.EOL);
}
if (folderCombinedPostResScript.length) {
if (scriptFlow === 'sequential') {
request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
}
}
if (folderCombinedTests.length) {
if (scriptFlow === 'sequential') {
request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
}
}
};
const createFormData = (datas, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forOwn(datas, (value, key) => {
if (typeof value == 'string') {
form.append(key, value);
return;
}
const filePaths = value || [];
filePaths?.forEach?.((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
});
});
return form;
};
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth');
@@ -352,29 +166,25 @@ const prepareRequest = (item, collection) => {
const headers = {};
let contentTypeDefined = false;
let url = request.url;
// Collection level headers
each(get(collectionRoot, 'request.headers', []), (h) => {
if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
if (h.enabled && h.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
return false;
}
});
// scriptFlow is either "sandwich" or "sequential"
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) {
mergeFolderLevelHeaders(request, requestTreePath);
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
mergeHeaders(collection, request, requestTreePath);
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
}
// Request level headers
each(request.headers, (h) => {
each(get(request, 'headers', []), (h) => {
if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
@@ -414,7 +224,7 @@ const prepareRequest = (item, collection) => {
if (request.body.mode === 'xml') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/xml';
axiosRequest.headers['content-type'] = 'application/xml';
}
axiosRequest.data = request.body.xml;
}
@@ -435,11 +245,11 @@ const prepareRequest = (item, collection) => {
}
if (request.body.mode === 'multipartForm') {
axiosRequest.headers['content-type'] = 'multipart/form-data';
const params = {};
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'multipart/form-data';
}
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
axiosRequest.data = enabledParams;
}
if (request.body.mode === 'graphql') {
@@ -458,6 +268,10 @@ const prepareRequest = (item, collection) => {
axiosRequest.script = request.script;
}
if (request.tests) {
axiosRequest.tests = request.tests;
}
axiosRequest.vars = request.vars;
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
@@ -470,4 +284,3 @@ const prepareRequest = (item, collection) => {
module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders;
module.exports.createFormData = createFormData;

View File

@@ -1,5 +1,161 @@
const each = require('lodash/each');
const find = require('lodash/find');
const { get, each, find, compact } = require('lodash');
const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
if (header?.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
};
const flattenItems = (items = []) => {
const flattenedItems = [];
@@ -44,14 +200,12 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
path.unshift(item);
item = findParentItemInCollection(collection, item.uid);
}
return path;
};
module.exports = {
flattenItems,
findItem,
findItemInCollection,
findParentItemInCollection,
mergeHeaders,
mergeVars,
mergeScripts,
getTreePathFromCollectionToItem
};
}

View File

@@ -85,24 +85,6 @@ const flattenDataForDotNotation = (data) => {
return result;
};
/**
* @param {Array.<object>} params The request body Array
* @returns {object} Returns an obj with repeating key as a array of values
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
*/
const buildFormUrlEncodedPayload = (params) => {
return params.reduce((acc, p) => {
if (!acc[p.name]) {
acc[p.name] = p.value;
} else if (Array.isArray(acc[p.name])) {
acc[p.name].push(p.value);
} else {
acc[p.name] = [acc[p.name], p.value];
}
return acc;
}, {});
};
module.exports = {
uuid,
stringifyJson,
@@ -111,6 +93,5 @@ module.exports = {
safeParseJSON,
simpleHash,
generateUidBasedOnHash,
flattenDataForDotNotation,
buildFormUrlEncodedPayload
flattenDataForDotNotation
};

View File

@@ -38,6 +38,11 @@ const isDirectory = (dirPath) => {
}
};
const hasSubDirectories = (dir) => {
const files = fs.readdirSync(dir);
return files.some(file => fs.statSync(path.join(dir, file)).isDirectory());
};
const normalizeAndResolvePath = (pathname) => {
if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname);
@@ -156,8 +161,12 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
const sanitizeCollectionName = (name) => {
return name.trim();
}
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
};
const isWindowsOS = () => {
@@ -222,7 +231,9 @@ module.exports = {
searchForFiles,
searchForBruFiles,
sanitizeDirectoryName,
sanitizeCollectionName,
isWindowsOS,
safeToRename,
isValidFilename
isValidFilename,
hasSubDirectories
};

View File

@@ -0,0 +1,62 @@
const { forEach } = require('lodash');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
/**
* @param {Array.<object>} params The request body Array
* @returns {object} Returns an obj with repeating key as a array of values
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
*/
const buildFormUrlEncodedPayload = (params) => {
return params.reduce((acc, p) => {
if (!acc[p.name]) {
acc[p.name] = p.value;
} else if (Array.isArray(acc[p.name])) {
acc[p.name].push(p.value);
} else {
acc[p.name] = [acc[p.name], p.value];
}
return acc;
}, {});
};
const createFormData = (data, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forEach(data, (datum) => {
const { name, type, value, contentType } = datum;
let options = {};
if (contentType) {
options.contentType = contentType;
}
if (type === 'text') {
if (Array.isArray(value)) {
value.forEach((val) => form.append(name, val, options));
} else {
form.append(name, value, options);
}
return;
}
if (type === 'file') {
const filePaths = value || [];
filePaths.forEach((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
options.filename = path.basename(trimmedFilePath);
form.append(name, fs.createReadStream(trimmedFilePath), options);
});
}
});
return form;
};
module.exports = {
buildFormUrlEncodedPayload,
createFormData
};

View File

@@ -1,7 +1,7 @@
const { describe, it, expect } = require('@jest/globals');
const prepareRequest = require('../../src/ipc/network/prepare-request');
const { buildFormUrlEncodedPayload } = require('../../src/utils/common');
const { buildFormUrlEncodedPayload } = require('../../src/utils/form-data');
describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {

View File

@@ -20,7 +20,7 @@
"graphql": "^16.6.0",
"markdown-it": "^13.0.1",
"postcss": "8.4.47",
"react": "18.2.0",
"react": "19.0.0",
"react-dom": "18.2.0",
"rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0",

View File

@@ -58,6 +58,7 @@ chai.use(function (chai, utils) {
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
@@ -95,6 +96,7 @@ const parseAssertionOperator = (str = '') => {
'endsWith',
'between',
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
@@ -109,6 +111,7 @@ const parseAssertionOperator = (str = '') => {
const unaryOperators = [
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
@@ -147,6 +150,7 @@ const parseAssertionOperator = (str = '') => {
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
@@ -345,6 +349,9 @@ class AssertRuntime {
case 'isEmpty':
expect(lhs).to.be.empty;
break;
case 'isNotEmpty':
expect(lhs).to.not.be.empty;
break;
case 'isNull':
expect(lhs).to.be.null;
break;

View File

@@ -18,8 +18,8 @@ const JS_KEYWORDS = `
* ```js
* res.data.pets.map(pet => pet.name.toUpperCase())
*
* function(context) {
* const { res, pet } = context;
* function(__bruno__functionInnerContext) {
* const { res, pet } = __bruno__functionInnerContext;
* return res.data.pets.map(pet => pet.name.toUpperCase())
* }
* ```
@@ -45,9 +45,11 @@ const compileJsExpression = (expr) => {
globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('')
};
const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`;
// param name that is unlikely to show up as a var in an expression
const param = `__bruno__functionInnerContext`;
const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`;
return new Function('context', body);
return new Function(param, body);
};
const internalExpressionCache = new Map();

View File

@@ -5,7 +5,9 @@ describe('utils', () => {
describe('expression evaluation', () => {
const context = {
res: {
data: { pets: ['bruno', 'max'] }
data: { pets: ['bruno', 'max'] },
context: 'testContext',
__bruno__functionInnerContext: 0
}
};
@@ -45,32 +47,32 @@ describe('utils', () => {
it('should identify top level variables', () => {
const expr = 'res.data.pets[0].toUpperCase()';
evaluateJsExpression(expr, context);
expect(cache.get(expr).toString()).toContain('let { res } = context;');
expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');
});
it('should not duplicate variables', () => {
const expr = 'res.data.pets[0] + res.data.pets[1]';
evaluateJsExpression(expr, context);
expect(cache.get(expr).toString()).toContain('let { res } = context;');
expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');
});
it('should exclude js keywords like true false from vars', () => {
const expr = 'res.data.pets.length > 0 ? true : false';
evaluateJsExpression(expr, context);
expect(cache.get(expr).toString()).toContain('let { res } = context;');
expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');
});
it('should exclude numbers from vars', () => {
const expr = 'res.data.pets.length + 10';
evaluateJsExpression(expr, context);
expect(cache.get(expr).toString()).toContain('let { res } = context;');
expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');
});
it('should pick variables from complex expressions', () => {
const expr = 'res.data.pets.map(pet => pet.length)';
const result = evaluateJsExpression(expr, context);
expect(result).toEqual([5, 3]);
expect(cache.get(expr).toString()).toContain('let { res, pet } = context;');
expect(cache.get(expr).toString()).toContain('let { res, pet } = __bruno__functionInnerContext;');
});
it('should be ok picking extra vars from strings', () => {
@@ -78,7 +80,7 @@ describe('utils', () => {
const result = evaluateJsExpression(expr, context);
expect(result).toBe('hello bruno');
// extra var hello is harmless
expect(cache.get(expr).toString()).toContain('let { hello, res } = context;');
expect(cache.get(expr).toString()).toContain('let { hello, res } = __bruno__functionInnerContext;');
});
it('should evaluate expressions referencing globals', () => {
@@ -112,6 +114,20 @@ describe('utils', () => {
expect(result).toBe(startTime);
});
it('should allow "context" as a var name', () => {
const expr = 'res["context"].toUpperCase()';
evaluateJsExpression(expr, context);
expect(cache.get(expr).toString()).toContain('let { res, context } = __bruno__functionInnerContext;');
});
it('should throw an error when we use "__bruno__functionInnerContext" as a var name', () => {
const expr = 'res["__bruno__functionInnerContext"].toUpperCase()';
expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError);
expect(() => evaluateJsExpression(expr, context)).toThrow(
"Identifier '__bruno__functionInnerContext' has already been declared"
);
});
});
describe('response parser', () => {

View File

@@ -46,7 +46,7 @@ const grammar = ohm.grammar(`Bru {
pair = st* key st* ":" st* value st*
key = keychar*
value = multilinetextblock | valuechar*
// Dictionary for Assert Block
assertdictionary = st* "{" assertpairlist? tagend
assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*
@@ -160,16 +160,31 @@ const mapRequestParams = (pairList = [], type) => {
});
};
const multipartExtractContentType = (pair) => {
if (_.isString(pair.value)) {
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
if (match != null && match.length > 2) {
pair.value = match[1];
pair.contentType = match[2];
} else {
pair.contentType = '';
}
}
};
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
return pairs.map((pair) => {
pair.type = 'text';
multipartExtractContentType(pair);
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
pair.type = 'file';
pair.value = filestr.split('|');
}
return pair;
});
};

View File

@@ -280,15 +280,18 @@ ${indentString(body.sparql)}
multipartForms
.map((item) => {
const enabled = item.enabled ? '' : '~';
const contentType =
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
if (item.type === 'text') {
return `${enabled}${item.name}: ${getValueString(item.value)}`;
return `${enabled}${item.name}: ${getValueString(item.value)}${contentType}`;
}
if (item.type === 'file') {
let filepaths = item.value || [];
let filestr = filepaths.join('|');
const value = `@file(${filestr})`;
return `${enabled}${item.name}: ${value}`;
return `${enabled}${item.name}: ${value}${contentType}`;
}
})
.join('\n')

View File

@@ -117,18 +117,21 @@
],
"multipartForm": [
{
"contentType": "",
"name": "apikey",
"value": "secret",
"enabled": true,
"type": "text"
},
{
"contentType": "",
"name": "numbers",
"value": "+91998877665",
"enabled": true,
"type": "text"
},
{
"contentType": "",
"name": "message",
"value": "hello",
"enabled": false,

View File

@@ -68,6 +68,7 @@ const multipartFormSchema = Yup.object({
otherwise: Yup.string().nullable()
}),
description: Yup.string().nullable(),
contentType: Yup.string().nullable(),
enabled: Yup.boolean()
})
.noUnknown(true)

View File

@@ -1,6 +1,7 @@
headers {
check: again
token: {{collection_pre_var_token}}
collection-header: collection-header-value
}
auth {
@@ -17,6 +18,27 @@ vars:pre-request {
collection-var: collection-var-value
}
script:pre-request {
// used by `scripting/js/folder-collection script-tests`
const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');
if(shouldTestCollectionScripts) {
bru.setVar('collection-var-set-by-collection-script', 'collection-var-value-set-by-collection-script');
}
}
tests {
// used by `scripting/js/folder-collection script-tests`
const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');
const collectionVar = bru.getVar("collection-var-set-by-collection-script");
if (shouldTestCollectionScripts && collectionVar) {
test("collection level test - should get the var that was set by the collection script", function() {
expect(collectionVar).to.equal("collection-var-value-set-by-collection-script");
});
bru.setVar('collection-var-set-by-collection-script', null);
bru.setVar('should-test-collection-scripts', null);
}
}
docs {
# bruno-testbench 🐶

View File

@@ -1,42 +0,0 @@
meta {
name: echo bigint
type: http
seq: 6
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
headers {
foo: bar
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": 990531470713421825,
"decimal": 1.0,
"decimal2": 1.00,
"decimal3": 1.00200,
"decimal4": 0.00
}
}
assert {
res.status: eq 200
}
tests {
// todo: add tests once lossless json echo server is ready
}

View File

@@ -11,6 +11,7 @@ post {
}
body:multipart-form {
foo: {"bar":"baz"} @contentType(application/json--test)
form-data-key: {{form-data-key}}
form-data-stringified-object: {{form-data-stringified-object}}
file: @file(bruno.png)
@@ -19,6 +20,7 @@ body:multipart-form {
assert {
res.body: contains form-data-value
res.body: contains {"foo":123}
res.body: contains Content-Type: application/json--test
}
script:pre-request {

View File

@@ -0,0 +1,44 @@
meta {
name: echo numbers
type: http
seq: 1
}
post {
url: {{echo-host}}
body: json
auth: none
}
body:json {
{
"integer": 123,
"negativeInteger": -99,
"zero": 0,
"float": 2.718,
"negativeFloat": -1.618,
"largeDouble": 12345.678901234567,
"smallDouble": 9.876e-12,
"booleanTrue": true,
"booleanFalse": false
}
}
assert {
res.body.integer: eq 123
res.body.integer: isNumber
res.body.negativeInteger: eq -99
res.body.negativeInteger: isNumber
res.body.zero: eq 0
res.body.zero: isNumber
res.body.float: eq 2.718
res.body.float: isNumber
res.body.negativeFloat: eq -1.618
res.body.negativeFloat: isNumber
res.body.largeDouble: eq 12345.678901234567
res.body.largeDouble: isNumber
res.body.smallDouble: eq 9.876e-12
res.body.smallDouble: isNumber
res.body.booleanTrue: eq true
res.body.booleanFalse: eq false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

View File

@@ -0,0 +1,16 @@
meta {
name: folder-collection script-tests pre
type: http
seq: 4
}
post {
url: {{echo-host}}
body: none
auth: none
}
script:pre-request {
bru.setVar('should-test-collection-scripts', true);
bru.setVar('should-test-folder-scripts', true);
}

View File

@@ -0,0 +1,28 @@
meta {
name: folder-collection script-tests
type: http
seq: 5
}
post {
url: {{echo-host}}
body: none
auth: none
}
script:pre-request {
// do not delete - the collection/folder scripts/tests run during this request execution
}
tests {
const collectionHeader = req.getHeader("collection-header");
const folderHeader = req.getHeader("folder-header");
test("should get the header value set at collection level", function() {
expect(collectionHeader).to.equal("collection-header-value");
});
test("should get the header value set at folder level", function() {
expect(folderHeader).to.equal("folder-header-value");
});
}

View File

@@ -0,0 +1,28 @@
meta {
name: js
}
headers {
folder-header: folder-header-value
}
script:pre-request {
// used by `scripting/js/folder-collection script-tests`
const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');
if(shouldTestFolderScripts) {
bru.setVar('folder-var-set-by-folder-script', 'folder-var-value-set-by-folder-script');
}
}
tests {
// used by `scripting/js/folder-collection script-tests`
const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');
const folderVar = bru.getVar("folder-var-set-by-folder-script");
if (shouldTestFolderScripts && folderVar) {
test("folder level test - should get the var that was set by the folder script", function() {
expect(folderVar).to.equal("folder-var-value-set-by-folder-script");
});
bru.setVar('folder-var-set-by-folder-script', null);
bru.setVar('should-test-folder-scripts', null);
}
}

View File

@@ -1,23 +1,25 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const multer = require('multer');
const formDataParser = require('./multipart/form-data-parser');
const authRouter = require('./auth');
const echoRouter = require('./echo');
const xmlParser = require('./utils/xmlParser');
const multipartRouter = require('./multipart');
const app = new express();
const port = process.env.PORT || 8080;
const upload = multer();
app.use(cors());
app.use(xmlParser());
app.use(bodyParser.text());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
formDataParser.init(app, express);
app.use('/api/auth', authRouter);
app.use('/api/echo', echoRouter);
app.use('/api/multipart', multipartRouter);
app.get('/ping', function (req, res) {
return res.send('pong');
@@ -31,10 +33,6 @@ app.get('/query', function (req, res) {
return res.json(req.query);
});
app.post('/echo/multipartForm', upload.none(), function (req, res) {
return res.json(req.body);
});
app.get('/redirect-to-ping', function (req, res) {
return res.redirect('/ping');
});

View File

@@ -0,0 +1,58 @@
/**
* Instead of using multer for example to parse the multipart form data, we build our own parser
* so that we can verify the content type are set correctly by bruno (for example application/json for json content)
*/
const extractParam = function (param, str, delimiter, quote, endDelimiter) {
let regex = new RegExp(`${param}${delimiter}\\s*${quote}(.*?)${quote}${endDelimiter}`);
const found = str.match(regex);
if (found != null && found.length > 1) {
return found[1];
} else {
return null;
}
};
const init = function (app, express) {
app.use(express.raw({ type: 'multipart/form-data' }));
};
const parsePart = function (part) {
let result = {};
const name = extractParam('name', part, '=', '"', '');
if (name) {
result.name = name;
}
const filename = extractParam('filename', part, '=', '"', '');
if (filename) {
result.filename = filename;
}
const contentType = extractParam('Content-Type', part, ':', '', ';');
if (contentType) {
result.contentType = contentType;
}
if (!filename) {
result.value = part.substring(part.indexOf('value=') + 'value='.length);
}
if (contentType === 'application/json') {
result.value = JSON.parse(result.value);
}
return result;
};
const parse = function (req) {
const BOUNDARY = 'boundary=';
const contentType = req.headers['content-type'];
const boundary = '--' + contentType.substring(contentType.indexOf(BOUNDARY) + BOUNDARY.length);
const rawBody = req.body.toString();
let parts = rawBody.split(boundary).filter((part) => part.length > 0);
parts = parts.map((part) => part.trim('\r\n'));
parts = parts.filter((part) => part != '--');
parts = parts.map((part) => part.replace('\r\n\r\n', ';value='));
parts = parts.map((part) => part.replace('\r\n', ';'));
parts = parts.map((part) => parsePart(part));
return parts;
};
module.exports.parse = parse;
module.exports.init = init;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const formDataParser = require('./form-data-parser');
router.post('/mixed-content-types', (req, res) => {
const parts = formDataParser.parse(req);
return res.json(parts);
});
module.exports = router;

View File

@@ -10,11 +10,11 @@ rm -rf packages/bruno-electron/web
mkdir packages/bruno-electron/web
# Copy build
cp -r packages/bruno-app/out/* packages/bruno-electron/web
cp -r packages/bruno-app/dist/* packages/bruno-electron/web
# Change paths in next
sed -i'' -e 's@/_next/@_next/@g' packages/bruno-electron/web/**.html
sed -i'' -e 's@/static/@static/@g' packages/bruno-electron/web/**.html
# Remove sourcemaps
find packages/bruno-electron/web -name '*.map' -type f -delete

95
scripts/setup.js Normal file
View File

@@ -0,0 +1,95 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const icons = {
clean: '🧹',
delete: '🗑️',
install: '📦',
build: '🔨',
success: '✅',
error: '❌',
working: '⚡'
};
const execCommand = (command, description) => {
try {
console.log(`\n${icons.working} ${description}...`);
execSync(command, { stdio: 'inherit' });
console.log(`${icons.success} ${description} completed`);
} catch (error) {
console.error(`${icons.error} ${description} failed`);
throw error;
}
};
const glob = function (startPath, pattern) {
let results = [];
// Ensure start path exists
if (!fs.existsSync(startPath)) {
return results;
}
const files = fs.readdirSync(startPath);
for (const file of files) {
const filename = path.join(startPath, file);
const stat = fs.lstatSync(filename);
// If directory, recurse into it
if (stat.isDirectory()) {
// Skip node_modules recursion to avoid unnecessary deep scanning
if (file === 'node_modules') {
if (file === pattern) {
results.push(filename);
}
continue;
}
results = results.concat(glob(filename, pattern));
}
// If file matches pattern, add to results
if (file === pattern) {
results.push(filename);
}
}
return results;
};
async function setup() {
try {
// Clean up node_modules (if exists)
console.log(`\n${icons.clean} Cleaning up node_modules directories...`);
const nodeModulesPaths = glob('.', 'node_modules');
for (const dir of nodeModulesPaths) {
console.log(`${icons.delete} Removing ${dir}`);
fs.rmSync(dir, { recursive: true, force: true });
}
// Install dependencies
execCommand('npm i --legacy-peer-deps', 'Installing dependencies');
// Build packages
execCommand('npm run build:graphql-docs', 'Building graphql-docs');
execCommand('npm run build:bruno-query', 'Building bruno-query');
execCommand('npm run build:bruno-common', 'Building bruno-common');
// Bundle JS sandbox libraries
execCommand(
'npm run sandbox:bundle-libraries --workspace=packages/bruno-js',
'Bundling JS sandbox libraries'
);
console.log(`\n${icons.success} Setup completed successfully!\n`);
} catch (error) {
console.error(`\n${icons.error} Setup failed:`);
console.error(error);
process.exit(1);
}
}
setup().catch(error => {
console.error(error);
process.exit(1);
});