Compare commits

...

9 Commits

Author SHA1 Message Date
Anoop M D
a10e6ee858 release: v1.28.0 2024-09-04 17:56:22 +05:30
Anoop M D
46017a6c50 chore: removed collection vars test as cli support is not yet ready 2024-09-04 11:20:03 +05:30
Anoop M D
cb395e7649 Collection Variables Support (#2963) (#3018)
Support for Collection Variables
---------

Co-authored-by: lohit <lohit.jiddimani@gmail.com>
Co-authored-by: lohit <lohxt.space@gmail.com>
2024-09-03 21:18:38 +05:30
j-lebek
5931f0bb4e feat: Store cookies received in Runner execution (#2951) 2024-09-03 21:12:54 +05:30
Anoop M D
2a93a6fa65 feat: content indicator for request docs 2024-09-03 20:16:50 +05:30
lohit
8a233fb489 fix: openapi collection import to not add protocol by default (#3011)
* Update package.json

* Update package.json

* Update package.json

* Update package.json

* revert jest command

* fix: openapi importwith server url

* fix: updates
2024-09-03 17:32:29 +05:30
lohit
b102898709 fix: multipart timeline (#3008)
* Update package.json

* Update package.json

* Update package.json

* Update package.json

* fix: multipart timeline

* revert jest command
2024-09-03 17:29:51 +05:30
Anoop M D
c5c343c543 feat: update proxy implementation in preferences (#2977) 2024-08-30 15:22:06 +05:30
lohit
c1ec95dc29 fix: option to toggle on/off system proxy env variables (#2724)
fix: option to toggle on/off system proxy env variables
2024-08-30 11:44:29 +05:30
41 changed files with 911 additions and 474 deletions

53
package-lock.json generated
View File

@@ -50,6 +50,7 @@
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -670,6 +671,7 @@
},
"node_modules/@babel/compat-data": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -677,6 +679,7 @@
},
"node_modules/@babel/core": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -740,6 +743,7 @@
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.25.2",
@@ -754,6 +758,7 @@
},
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -761,6 +766,7 @@
},
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
"version": "3.1.1",
"dev": true,
"license": "ISC"
},
"node_modules/@babel/helper-create-class-features-plugin": {
@@ -839,6 +845,7 @@
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.25.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.24.7",
@@ -905,6 +912,7 @@
},
"node_modules/@babel/helper-simple-access": {
"version": "7.24.7",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.24.7",
@@ -942,6 +950,7 @@
},
"node_modules/@babel/helper-validator-option": {
"version": "7.24.8",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -962,6 +971,7 @@
},
"node_modules/@babel/helpers": {
"version": "7.25.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.0",
@@ -3644,37 +3654,6 @@
"node": ">=12"
}
},
"node_modules/@n8n/vm2": {
"version": "3.9.25",
"resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.25.tgz",
"integrity": "sha512-qoGLFzyHBW7HKpwXkl05QKsIh3GkDw6lOiTOWYlUDnOIQ1b7EgM+O5EMjrMGy7r+kz52+Q7o6GLxBIcxVI8rEg==",
"license": "MIT",
"peer": true,
"dependencies": {
"acorn": "^8.7.0",
"acorn-walk": "^8.2.0"
},
"bin": {
"vm2": "bin/vm2"
},
"engines": {
"node": ">=18.10",
"pnpm": ">=9.6"
}
},
"node_modules/@n8n/vm2/node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@next/env": {
"version": "12.3.3",
"license": "MIT"
@@ -4751,6 +4730,7 @@
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -4759,6 +4739,7 @@
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -4767,6 +4748,7 @@
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/@types/minimatch": {
@@ -6240,6 +6222,7 @@
},
"node_modules/browserslist": {
"version": "4.23.3",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -7238,6 +7221,7 @@
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -8477,6 +8461,7 @@
},
"node_modules/electron-to-chromium": {
"version": "1.5.11",
"dev": true,
"license": "ISC"
},
"node_modules/electron-util": {
@@ -9438,6 +9423,7 @@
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -13186,6 +13172,7 @@
},
"node_modules/node-releases": {
"version": "2.0.18",
"dev": true,
"license": "MIT"
},
"node_modules/node-vault": {
@@ -16201,6 +16188,7 @@
},
"node_modules/semver": {
"version": "6.3.1",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -17663,6 +17651,7 @@
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -18633,7 +18622,7 @@
},
"packages/bruno-electron": {
"name": "bruno",
"version": "v1.26.1",
"version": "v1.27.0",
"dependencies": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",

View File

@@ -336,4 +336,4 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
);
};
export default ProxySettings;
export default ProxySettings;

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,162 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
import {
addCollectionVar,
deleteCollectionVar,
updateCollectionVar
} from 'providers/ReduxStore/slices/collections/index';
const VarsTable = ({ collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addVar = () => {
dispatch(
addCollectionVar({
collectionUid: collection.uid,
type: varType
})
);
};
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
const value = e.target.value;
if (variableNameRegex.test(value) === false) {
toast.error(
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
);
return;
}
_var.name = value;
break;
}
case 'value': {
_var.value = e.target.value;
break;
}
case 'enabled': {
_var.enabled = e.target.checked;
break;
}
}
dispatch(
updateCollectionVar({
type: varType,
var: _var,
collectionUid: collection.uid
})
);
};
const handleRemoveVar = (_var) => {
dispatch(
deleteCollectionVar({
type: varType,
varUid: _var.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{varType === 'request' ? (
<td>
<div className="flex items-center">
<span>Value</span>
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
? vars.map((_var) => {
return (
<tr key={_var.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={_var.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, _var, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleVarChange(
{
target: {
value: newValue
}
},
_var,
'value'
)
}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={_var.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
+ Add
</button>
</StyledWrapper>
);
};
export default VarsTable;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Vars;

View File

@@ -16,6 +16,7 @@ import Docs from './Docs';
import Presets from './Presets';
import Info from './Info';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
@@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
case 'headers': {
return <Headers collection={collection} />;
}
case 'vars': {
return <Vars collection={collection} />;
}
case 'auth': {
return <Auth collection={collection} />;
}
@@ -123,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
Vars
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
</div>

View File

@@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
)
}
collection={collection}
item={folder}
/>
</td>
<td>

View File

@@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
)
}
collection={collection}
item={folder}
/>
</td>
<td>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
width: 100px;
}
.textbox {
@@ -20,6 +20,12 @@ const StyledWrapper = styled.div`
outline: none !important;
}
}
.system-proxy-settings {
label {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`;
export default StyledWrapper;

View File

@@ -11,14 +11,17 @@ import { useState } from 'react';
const ProxySettings = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
const dispatch = useDispatch();
console.log(preferences);
const proxySchema = Yup.object({
enabled: Yup.boolean(),
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: true,
is: 'on',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
@@ -31,7 +34,7 @@ const ProxySettings = ({ close }) => {
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: true,
is: 'on',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
@@ -54,7 +57,7 @@ const ProxySettings = ({ close }) => {
const formik = useFormik({
initialValues: {
enabled: preferences.proxy.enabled || false,
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || 0,
@@ -94,7 +97,7 @@ const ProxySettings = ({ close }) => {
useEffect(() => {
formik.setValues({
enabled: preferences.proxy.enabled || false,
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || '',
@@ -109,188 +112,256 @@ const ProxySettings = ({ close }) => {
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
Mode
</label>
<div className="flex items-center">
<label className="flex items-center">
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
name="mode"
value="false"
checked={formik.values.mode === 'off'}
onChange={(e) => {
formik.setFieldValue('mode', 'off');
}}
className="mr-1 cursor-pointer"
/>
HTTP
Off
</label>
<label className="flex items-center ml-4">
<label className="flex items-center ml-4 cursor-pointer">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
name="mode"
value="true"
checked={formik.values.mode === 'on'}
onChange={(e) => {
formik.setFieldValue('mode', 'on');
}}
className="mr-1 cursor-pointer"
/>
HTTPS
On
</label>
<label className="flex items-center ml-4">
<label className="flex items-center ml-4 cursor-pointer">
<input
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
name="mode"
value="system"
checked={formik.values.mode === 'system'}
onChange={formik.handleChange}
className="mr-1"
className="mr-1 cursor-pointer"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
System Proxy
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
{formik?.values?.mode === 'system' ? (
<div className="mb-3 pt-1 text-muted system-proxy-settings">
<small>
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
Please refer to your OS documentation to change these values.
</small>
<div className="flex flex-col justify-start items-start pt-2">
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="http_proxy">
http_proxy
</label>
<div className="opacity-80">{http_proxy || '-'}</div>
</div>
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="https_proxy">
https_proxy
</label>
<div className="opacity-80">{https_proxy || '-'}</div>
</div>
<div className="mb-1 flex items-center">
<label className="settings-label" htmlFor="no_proxy">
no_proxy
</label>
<div className="opacity-80">{no_proxy || '-'}</div>
</div>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
) : null}
{formik?.values?.mode === 'on' ? (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTP
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTPS
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
className="outline-none w-[10.5rem] bg-transparent"
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
<input
id="auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
className="outline-none w-[10.5rem] bg-transparent"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
</>
) : null}
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save

View File

@@ -191,6 +191,7 @@ const AssertionRow = ({
}
onRun={handleRun}
collection={collection}
item={item}
/>
) : (
<input type="text" className="cursor-default" disabled />

View File

@@ -97,6 +97,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const script = getPropertyFromDraftOrRequest('request.script');
const assertions = getPropertyFromDraftOrRequest('request.assertions');
const tests = getPropertyFromDraftOrRequest('request.tests');
const docs = getPropertyFromDraftOrRequest('request.docs');
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
@@ -139,10 +140,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
{tests && <ContentIndicator />}
{tests && tests.length > 0 && <ContentIndicator />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
{docs && docs.length > 0 && <ContentIndicator />}
</div>
{focusedTab.requestPaneTab === 'body' ? (
<div className="flex flex-grow justify-end items-center">

View File

@@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>

View File

@@ -17,6 +17,8 @@ const Timeline = ({ request, response }) => {
});
});
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
return (
<StyledWrapper className="pb-4 w-full">
<div>
@@ -31,10 +33,10 @@ const Timeline = ({ request, response }) => {
);
})}
{request.data ? (
{requestData ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
</pre>
) : null}
</div>

View File

@@ -132,7 +132,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.28.0</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,10 @@
import { useEffect } from 'react';
import { showPreferences, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
import {
showPreferences,
updateCookies,
updatePreferences,
updateSystemProxyEnvVariables
} from 'providers/ReduxStore/slices/app';
import {
brunoConfigUpdateEvent,
collectionAddDirectoryEvent,
@@ -136,6 +141,10 @@ const useIpcEvents = () => {
dispatch(updatePreferences(val));
});
const removeSystemProxyEnvUpdatesListener = ipcRenderer.on('main:load-system-proxy-env', (val) => {
dispatch(updateSystemProxyEnvVariables(val));
});
const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {
dispatch(updateCookies(val));
});
@@ -155,6 +164,7 @@ const useIpcEvents = () => {
removeShowPreferencesListener();
removePreferencesUpdatesListener();
removeCookieUpdateListener();
removeSystemProxyEnvUpdatesListener();
};
}, [isElectron]);
};

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.27.0'
version: '1.28.0'
}
});
};

View File

@@ -27,7 +27,8 @@ const initialState = {
}
},
cookies: [],
taskQueue: []
taskQueue: [],
systemProxyEnvVariables: {}
};
export const appSlice = createSlice({
@@ -72,6 +73,9 @@ export const appSlice = createSlice({
},
removeAllTasksFromQueue: (state) => {
state.taskQueue = [];
},
updateSystemProxyEnvVariables: (state, action) => {
state.systemProxyEnvVariables = action.payload;
}
}
});
@@ -89,7 +93,8 @@ export const {
updateCookies,
insertTaskIntoQueue,
removeTaskFromQueue,
removeAllTasksFromQueue
removeAllTasksFromQueue,
updateSystemProxyEnvVariables
} = appSlice.actions;
export const savePreferences = (preferences) => (dispatch, getState) => {

View File

@@ -1302,6 +1302,71 @@ export const collectionsSlice = createSlice({
set(collection, 'root.request.headers', headers);
}
},
addCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (type === 'request') {
const vars = get(collection, 'root.request.vars.req', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
const vars = get(collection, 'root.request.vars.res', []);
vars.push({
uid: uuid(),
name: '',
value: '',
enabled: true
});
set(collection, 'root.request.vars.res', vars);
}
}
},
updateCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
if (_var) {
_var.name = action.payload.var.name;
_var.value = action.payload.var.value;
_var.description = action.payload.var.description;
_var.enabled = action.payload.var.enabled;
}
set(collection, 'root.request.vars.res', vars);
}
},
deleteCollectionVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
if (type === 'request') {
let vars = get(collection, 'root.request.vars.req', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(collection, 'root.request.vars.res', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(collection, 'root.request.vars.res', vars);
}
}
},
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
@@ -1694,6 +1759,9 @@ export const {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
addCollectionVar,
updateCollectionVar,
deleteCollectionVar,
updateCollectionAuthMode,
updateCollectionAuth,
updateCollectionRequestScript,

View File

@@ -787,24 +787,25 @@ export const getTotalRequestCountInCollection = (collection) => {
};
export const getAllVariables = (collection, item) => {
const environmentVariables = getEnvironmentVariables(collection);
let requestVariables = {};
if (item?.request) {
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
}
const envVariables = getEnvironmentVariables(collection);
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
const pathParams = getPathParams(item);
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
return {
...environmentVariables,
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...collection.runtimeVariables,
...runtimeVariables,
pathParams: {
...pathParams
},
process: {
env: {
...collection.processEnvVariables
...processEnvVariables
}
}
};
@@ -831,14 +832,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
const mergeFolderLevelVars = (request, requestTreePath = []) => {
const mergeVars = (collection, requestTreePath = []) => {
let collectionVariables = {};
let folderVariables = {};
let requestVariables = {};
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
collectionVariables[_var.name] = _var.value;
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
requestVariables[_var.name] = _var.value;
folderVariables[_var.name] = _var.value;
}
});
} else {
@@ -850,6 +859,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
});
}
}
return requestVariables;
return {
collectionVariables,
folderVariables,
requestVariables
};
};

View File

@@ -31,9 +31,8 @@ const readFile = (files) => {
};
const ensureUrl = (url) => {
let protUrl = url.startsWith('http') ? url : `http://${url}`;
// replace any double or triple slashes
return protUrl.replace(/([^:]\/)\/+/g, '$1');
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
return url.replace(/(^\w+:|^)\/{2,}/, '$1/');
};
const buildEmptyJsonBody = (bodySchema) => {
@@ -67,7 +66,7 @@ const transformOpenapiRequestItem = (request) => {
name: operationName,
type: 'http-request',
request: {
url: ensureUrl(request.global.server + '/' + path),
url: ensureUrl(request.global.server + path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
@@ -306,7 +305,7 @@ const getDefaultUrl = (serverObject) => {
url = url.replace(`{${variableName}}`, sub);
});
}
return url;
return url.endsWith('/') ? url : `${url}/`;
};
const getSecurity = (apiSpec) => {

View File

@@ -60,20 +60,6 @@ const runSingleRequest = async function (
request.data = form;
}
// run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVariables,
runtimeVariables,
collectionPath,
processEnvVars
);
}
// run pre request script
const requestScriptFile = compact([
get(collectionRoot, 'request.script.req'),
@@ -276,7 +262,7 @@ const runSingleRequest = async function (
console.log(
chalk.green(stripExtension(filename)) +
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
);
// run post-response vars

View File

@@ -1,5 +1,5 @@
{
"version": "v1.27.0",
"version": "v1.28.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@@ -49,7 +49,9 @@ const checkConnection = (host, port) =>
*/
function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */
const instance = axios.create();
const instance = axios.create({
proxy: false
});
instance.interceptors.request.use(async (config) => {
const url = URL.parse(config.url);

View File

@@ -88,6 +88,22 @@ const getJsSandboxRuntime = (collection) => {
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const saveCookies = (url, headers) => {
if (preferencesUtil.shouldStoreCookies()) {
let setCookieHeaders = [];
if (headers['set-cookie']) {
setCookieHeaders = Array.isArray(headers['set-cookie'])
? headers['set-cookie']
: [headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, url);
}
}
}
}
}
const configureRequest = async (
collectionUid,
request,
@@ -165,51 +181,92 @@ const configureRequest = async (
}
}
// proxy configuration
let proxyConfig = get(brunoConfig, 'proxy', {});
let proxyEnabled = get(proxyConfig, 'enabled', 'global');
if (proxyEnabled === 'global') {
/**
* Proxy configuration
*
* Preferences proxyMode has three possible values: on, off, system
* Collection proxyMode has three possible values: true, false, global
*
* When collection proxyMode is true, it overrides the app-level proxy settings
* When collection proxyMode is false, it ignores the app-level proxy settings
* When collection proxyMode is global, it uses the app-level proxy settings
*
* Below logic calculates the proxyMode and proxyConfig to be used for the request
*/
let proxyMode = 'off';
let proxyConfig = {};
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global');
if (collectionProxyEnabled === true) {
proxyConfig = collectionProxyConfig;
proxyMode = 'on';
} else if (collectionProxyEnabled === 'global') {
proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyEnabled = get(proxyConfig, 'enabled', false);
proxyMode = get(proxyConfig, 'mode', 'off');
}
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (proxyEnabled === true && shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
request.httpsAgent = new SocksProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new SocksProxyAgent(proxyUri);
} else {
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
}
if (socksEnabled) {
request.httpsAgent = new SocksProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new SocksProxyAgent(proxyUri);
} else {
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
if (shouldUseSystemProxy) {
try {
if (http_proxy?.length) {
new URL(http_proxy);
request.httpAgent = new HttpProxyAgent(http_proxy);
}
} catch (error) {
throw new Error('Invalid system http_proxy');
}
try {
if (https_proxy?.length) {
new URL(https_proxy);
request.httpsAgent = new PatchedHttpsProxyAgent(
https_proxy,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
}
} catch (error) {
throw new Error('Invalid system https_proxy');
}
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
const axiosInstance = makeAxiosInstance();
if (request.oauth2) {
@@ -290,10 +347,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
if(!disableParsingResponseJson) {
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {}
} catch { }
return { data, dataBuffer };
};
@@ -319,20 +376,6 @@ const registerNetworkIpc = (mainWindow) => {
processEnvVars,
scriptingConfig
) => {
// run pre-request vars
const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVars,
runtimeVariables,
collectionPath,
processEnvVars
);
}
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
@@ -549,17 +592,7 @@ const registerNetworkIpc = (mainWindow) => {
// save cookies
if (preferencesUtil.shouldStoreCookies()) {
let setCookieHeaders = [];
if (response.headers['set-cookie']) {
setCookieHeaders = Array.isArray(response.headers['set-cookie'])
? response.headers['set-cookie']
: [response.headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, request.url);
}
}
}
saveCookies(request.url, response.headers);
}
// send domain cookies to renderer
@@ -975,6 +1008,16 @@ const registerNetworkIpc = (mainWindow) => {
response.data = data;
response.responseTime = response.headers.get('request-duration');
// save cookies
if (preferencesUtil.shouldStoreCookies()) {
saveCookies(request.url, response.headers);
}
// send domain cookies to renderer
const domainsWithCookies = await getDomainsWithCookies();
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
mainWindow.webContents.send('main:run-folder-event', {
type: 'response-received',
responseReceived: {
@@ -1160,7 +1203,7 @@ const registerNetworkIpc = (mainWindow) => {
try {
const disposition = contentDispositionParser.parse(contentDisposition);
return disposition && disposition.parameters['filename'];
} catch (error) {}
} catch (error) { }
};
const getFileNameFromUrlPath = () => {

View File

@@ -12,15 +12,17 @@ const getContentType = (headers = {}) => {
return contentType;
};
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
envVariables = cloneDeep(envVariables);
// envVars can inturn have values as {{process.env.VAR_NAME}}
// so we need to interpolate envVars first with processEnvVars
forOwn(envVars, (value, key) => {
envVars[key] = interpolate(value, {
forOwn(envVariables, (value, key) => {
envVariables[key] = interpolate(value, {
process: {
env: {
...processEnvVars
@@ -36,7 +38,9 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
// runtimeVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
process: {

View File

@@ -44,73 +44,75 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeFolderLevelVars = (request, requestTreePath) => {
let folderReqVars = new Map();
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) {
folderReqVars.set(_var.name, _var.value);
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else if (i.uid === request.uid) {
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderReqVars.set(_var.name, _var.value);
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
let requestReqVars = request?.vars?.req || [];
let requestReqVarsMap = new Map();
for (let _var of requestReqVars) {
if (_var.enabled) {
requestReqVarsMap.set(_var.name, _var.value);
}
}
mergedFolderReqVars.forEach((_var) => {
requestReqVarsMap.set(_var.name, _var.value);
});
request.vars.req = Array.from(requestReqVarsMap, ([name, 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 folderResVars = new Map();
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) {
folderResVars.set(_var.name, _var.value);
resVars.set(_var.name, _var.value);
}
});
} else if (i.uid === request.uid) {
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
folderResVars.set(_var.name, _var.value);
resVars.set(_var.name, _var.value);
}
});
}
}
let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
let requestResVars = request?.vars?.res || [];
let requestResVarsMap = new Map();
for (let _var of requestResVars) {
if (_var.enabled) {
requestResVarsMap.set(_var.name, _var.value);
}
}
mergedFolderResVars.forEach((_var) => {
requestResVarsMap.set(_var.name, _var.value);
});
request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
@@ -314,7 +316,7 @@ const prepareRequest = (item, collection) => {
if (requestTreePath && requestTreePath.length > 0) {
mergeFolderLevelHeaders(request, requestTreePath);
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
mergeFolderLevelVars(request, requestTreePath);
mergeVars(collection, request, requestTreePath);
}
each(request.headers, (h) => {
@@ -401,6 +403,9 @@ const prepareRequest = (item, collection) => {
}
axiosRequest.vars = request.vars;
axiosRequest.collectionVariables = request.collectionVariables;
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.assertions = request.assertions;
return axiosRequest;

View File

@@ -1,5 +1,5 @@
const { ipcMain } = require('electron');
const { getPreferences, savePreferences } = require('../store/preferences');
const { getPreferences, savePreferences, preferencesUtil } = require('../store/preferences');
const { isDirectory } = require('../utils/filesystem');
const { openCollection } = require('../app/collections');
``;
@@ -9,6 +9,10 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
const preferences = getPreferences();
mainWindow.webContents.send('main:load-preferences', preferences);
const systemProxyVars = preferencesUtil.getSystemProxyEnvVariables();
const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {};
mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy });
// reload last opened collections
const lastOpened = lastOpenedCollections.getAll();

View File

@@ -1,6 +1,6 @@
const Yup = require('yup');
const Store = require('electron-store');
const { get } = require('lodash');
const { get, merge } = require('lodash');
/**
* The preferences are stored in the electron store 'preferences.json'.
@@ -27,7 +27,7 @@ const defaultPreferences = {
codeFontSize: 14
},
proxy: {
enabled: false,
mode: 'off',
protocol: 'http',
hostname: '',
port: null,
@@ -59,7 +59,7 @@ const preferencesSchema = Yup.object().shape({
codeFontSize: Yup.number().min(1).max(32).nullable()
}),
proxy: Yup.object({
enabled: Yup.boolean(),
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(1).max(65535).nullable(),
@@ -81,10 +81,22 @@ class PreferencesStore {
}
getPreferences() {
return {
...defaultPreferences,
...this.store.get('preferences')
};
let preferences = this.store.get('preferences', {});
// This to support the old preferences format
// In the old format, we had a proxy.enabled flag
// In the new format, this maps to proxy.mode = 'on'
if (preferences?.proxy?.enabled) {
preferences.proxy.mode = 'on';
}
// Delete the proxy.enabled property if it exists, regardless of its value
// This is a part of migration to the new preferences format
if (preferences?.proxy && 'enabled' in preferences.proxy) {
delete preferences.proxy.enabled;
}
return merge({}, defaultPreferences, preferences);
}
savePreferences(newPreferences) {
@@ -136,6 +148,14 @@ const preferencesUtil = {
},
shouldSendCookies: () => {
return get(getPreferences(), 'request.sendCookies', true);
},
getSystemProxyEnvVariables: () => {
const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
return {
http_proxy: http_proxy || HTTP_PROXY,
https_proxy: https_proxy || HTTPS_PROXY,
no_proxy: no_proxy || NO_PROXY
};
}
};

View File

@@ -4,10 +4,12 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;
class Bru {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables) {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables) {
this.envVariables = envVariables || {};
this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
this.collectionVariables = collectionVariables || {};
this.folderVariables = folderVariables || {};
this.requestVariables = requestVariables || {};
this.collectionPath = collectionPath;
}
@@ -18,7 +20,9 @@ class Bru {
}
const combinedVars = {
...this.collectionVariables,
...this.envVariables,
...this.folderVariables,
...this.requestVariables,
...this.runtimeVariables,
process: {
@@ -71,7 +75,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
' Names must only contain alpha-numeric characters, "-", "_", "."'
' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -82,7 +86,7 @@ class Bru {
if (variableNameRegex.test(key) === false) {
throw new Error(
`Variable name: "${key}" contains invalid characters!` +
' Names must only contain alpha-numeric characters, "-", "_", "."'
' Names must only contain alpha-numeric characters, "-", "_", "."'
);
}
@@ -93,6 +97,14 @@ class Bru {
delete this.runtimeVariables[key];
}
getCollectionVar(key) {
return this._interpolate(this.collectionVariables[key]);
}
getFolderVar(key) {
return this._interpolate(this.folderVariables[key]);
}
getRequestVar(key) {
return this._interpolate(this.requestVariables[key]);
}

View File

@@ -2,14 +2,16 @@ const { interpolate } = require('@usebruno/common');
const interpolateString = (
str,
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} }
) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const combinedVars = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
process: {

View File

@@ -192,6 +192,8 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
}
const interpolationContext = {
collectionVariables: context.bru.collectionVariables,
folderVariables: context.bru.folderVariables,
requestVariables: context.bru.requestVariables,
runtimeVariables: context.bru.runtimeVariables,
envVariables: context.bru.envVariables,
@@ -238,13 +240,23 @@ class AssertRuntime {
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
if (!enabledAssertions.length) {
return [];
}
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
const bru = new Bru(
envVariables,
runtimeVariables,
processEnvVars,
undefined,
collectionVariables,
folderVariables,
requestVariables
);
const req = new BrunoRequest(request);
const res = createResponseParser(response);
@@ -255,7 +267,9 @@ class AssertRuntime {
};
const context = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
...processEnvVars,

View File

@@ -47,8 +47,10 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
@@ -162,8 +164,10 @@ class ScriptRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);

View File

@@ -48,8 +48,10 @@ class TestRuntime {
processEnvVars,
scriptingConfig
) {
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);

View File

@@ -1,22 +1,10 @@
const _ = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { evaluateJsExpression, createResponseParser } = require('../utils');
const { executeQuickJsVm } = require('../sandbox/quickjs');
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
@@ -35,35 +23,6 @@ class VarsRuntime {
this.mode = props?.mode || 'developer';
}
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
if (!request?.requestVariables) {
request.requestVariables = {};
}
const enabledVars = _.filter(vars, (v) => v.enabled);
if (!enabledVars.length) {
return;
}
const bru = new Bru(envVariables, runtimeVariables, processEnvVars);
const req = new BrunoRequest(request);
const bruContext = {
bru,
req
};
const context = {
...envVariables,
...runtimeVariables,
...bruContext
};
_.each(enabledVars, (v) => {
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
request?.requestVariables && (request.requestVariables[v.name] = value);
});
}
runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledVars = _.filter(vars, (v) => v.enabled);

View File

@@ -69,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
getRequestVar.dispose();
let getFolderVar = vm.newFunction('getFolderVar', function (key) {
return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getFolderVar', getFolderVar);
getFolderVar.dispose();
let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
getCollectionVar.dispose();
const sleep = vm.newFunction('sleep', (timer) => {
const t = vm.getString(timer);
const promise = vm.newPromise();

View File

@@ -1,5 +1,6 @@
headers {
check: again
token: {{collection_pre_var_token}}
}
auth {
@@ -10,6 +11,11 @@ auth:bearer {
token: {{bearer_auth_token}}
}
vars:pre-request {
collection_pre_var: collection_pre_var_value
collection_pre_var_token: {{request_pre_var_token}}
}
docs {
# bruno-testbench 🐶

View File

@@ -25,16 +25,6 @@ body:json {
}
}
vars:pre-request {
boolean: false
undefined: undefined
null: null
string: foo
number_1: 1
number_2: 0
number_3: -1
}
assert {
req.body.boolean: isBoolean false
req.body.number_1: isNumber 1
@@ -51,35 +41,4 @@ assert {
req.body.number_3: eq -1
req.body.number_2: isNumber
req.body.number_3: isNumber
boolean: eq false
undefined: eq undefined
null: eq null
string: eq foo
number_1: eq 1
number_2: eq 0
number_3: eq -1
}
tests {
test("boolean pre var", function() {
expect(bru.getRequestVar('boolean')).to.eql(false);
});
test("number pre var", function() {
expect(bru.getRequestVar('number_1')).to.eql(1);
expect(bru.getRequestVar('number_2')).to.eql(0);
expect(bru.getRequestVar('number_3')).to.eql(-1);
});
test("null pre var", function() {
expect(bru.getRequestVar('null')).to.eql(null);
});
test("undefined pre var", function() {
expect(bru.getRequestVar('undefined')).to.eql(undefined);
});
test("string pre var", function() {
expect(bru.getRequestVar('string')).to.eql('foo');
});
}

View File

@@ -1,39 +0,0 @@
meta {
name: vars asserts
type: http
seq: 5
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
body:json {
{
"boolean": false,
"number": 1,
"string": "bruno",
"array": [1, 2, 3, 4, 5],
"object": {
"hello": "bruno"
},
"null": null
}
}
vars:pre-request {
vars_asserts__request_var: vars_asserts__request_var__value
}
assert {
vars_asserts__request_var: eq vars_asserts__request_var__value
vars_asserts__runtime_var: eq vars_asserts__runtime_var__value
vars_asserts__env_var: eq vars_asserts__env_var__value
}
script:pre-request {
bru.setVar('vars_asserts__runtime_var', 'vars_asserts__runtime_var__value');
bru.setEnvVar('vars_asserts__env_var', 'vars_asserts__env_var__value');
}

View File

@@ -0,0 +1,8 @@
meta {
name: string interpolation
}
vars:pre-request {
folder_pre_var: folder_pre_var_value
folder_pre_var_2: {{env.var1}}
}