+
{clientCert.domain}
+
+
+ {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
+
onRemove(clientCert)} className="remove-certificate ml-2">
diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
index d449d12d3..18a1aca1d 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
@@ -46,9 +46,10 @@ const Docs = ({ collection }) => {
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
-
+
)}
);
diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
index 718a38bd5..9ae6e1e07 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
@@ -13,6 +13,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
+import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
@@ -117,6 +118,7 @@ const Headers = ({ collection }) => {
)
}
collection={collection}
+ autocomplete={MimeTypes}
/>
diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
index 734bd90ef..e16884e16 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
@@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
id="request-url"
type="text"
name="requestUrl"
+ placeholder='Request URL'
className="block textbox"
autoComplete="off"
autoCorrect="off"
diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
index 3df200e88..105a92642 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
-import Tooltip from 'components/Tooltip';
+import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
@@ -104,7 +104,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
Config
-
@@ -114,7 +114,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
`}
- tooltipId="request-var"
+ infotipId="request-var"
/>
@@ -336,4 +336,4 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
);
};
-export default ProxySettings;
+export default ProxySettings;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/CollectionSettings/Script/index.js b/packages/bruno-app/src/components/CollectionSettings/Script/index.js
index 84af056f6..6fe979cbf 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Script/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Script/index.js
@@ -52,6 +52,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
@@ -64,6 +65,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
diff --git a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js
index c23294c74..d87a1dea4 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js
@@ -36,6 +36,7 @@ const Tests = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js
new file mode 100644
index 000000000..44b01b464
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js
@@ -0,0 +1,9 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ div.title {
+ color: var(--color-tab-inactive);
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/StyledWrapper.js
new file mode 100644
index 000000000..efacc8288
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
new file mode 100644
index 000000000..950076b60
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
@@ -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 (
+
+
+
+ + Add
+
+
+ );
+};
+export default VarsTable;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/index.js
new file mode 100644
index 000000000..fae3ed613
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/index.js
@@ -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 (
+
+
+
+
+
+ Save
+
+
+
+ );
+};
+
+export default Vars;
diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js
index 6cc42a09d..d7698e26c 100644
--- a/packages/bruno-app/src/components/CollectionSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/index.js
@@ -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
;
}
+ case 'vars': {
+ return
;
+ }
case 'auth': {
return
;
}
@@ -95,6 +99,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': {
return (
{
setTab('headers')}>
Headers
+ setTab('vars')}>
+ Vars
+
setTab('auth')}>
Auth
diff --git a/packages/bruno-app/src/components/Documentation/StyledWrapper.js b/packages/bruno-app/src/components/Documentation/StyledWrapper.js
index f0ffee808..f159d94dc 100644
--- a/packages/bruno-app/src/components/Documentation/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Documentation/StyledWrapper.js
@@ -1,14 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
- div.CodeMirror {
- /* todo: find a better way */
- height: calc(100vh - 240px);
-
- .CodeMirror-scroll {
- padding-bottom: 0px;
- }
- }
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
diff --git a/packages/bruno-app/src/components/Documentation/index.js b/packages/bruno-app/src/components/Documentation/index.js
index d4b790965..0af0d7588 100644
--- a/packages/bruno-app/src/components/Documentation/index.js
+++ b/packages/bruno-app/src/components/Documentation/index.js
@@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
}
return (
-
-
+
+
{isEditing ? 'Preview' : 'Edit'}
@@ -47,13 +47,14 @@ const Documentation = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
) : (
-
+
)}
);
diff --git a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js
index 6ad94e289..7af8b9081 100644
--- a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js
@@ -40,10 +40,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.dropdown.iconColor};
}
- &:hover {
+ &:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
+ &:disabled {
+ cursor: not-allowed;
+ color: gray;
+ }
+
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
}
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index e6947bd3a..3427955a2 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -1,11 +1,11 @@
import React, { useEffect, useRef } from 'react';
-import Portal from 'components/Portal';
-import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
+import Portal from 'components/Portal';
+import Modal from 'components/Modal';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
toast.success('Environment created in collection');
onClose();
})
- .catch(() => toast.error('An error occurred while created the environment'));
+ .catch(() => toast.error('An error occurred while creating the environment'));
}
});
@@ -55,19 +55,21 @@ const CreateEnvironment = ({ collection, onClose }) => {
Environment Name
-
+
+
+
{formik.touched.name && formik.errors.name ? (
{formik.errors.name}
) : null}
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
index 1f36d05ea..45a43a6a9 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
-import { maskInputValue } from 'utils/collections';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { variableNameRegex } from 'utils/common/regex';
@@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
- Enabled
+ Enabled
Name
Value
- Secret
+ Secret
@@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
-
- {variable.secret ? (
- {maskInputValue(variable.value)}
- ) : (
+
+
formik.setFieldValue(`${index}.value`, newValue, true)}
/>
- )}
+
-
+
{
+const ImportEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
importPostmanEnvironment()
- .then((environment) => {
- dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
- .then(() => {
- toast.success('Environment imported successfully');
- onClose();
- })
- .catch(() => toast.error('An error occurred while importing the environment'));
+ .then((environments) => {
+ environments
+ .filter((env) =>
+ env.name && env.name !== 'undefined'
+ ? true
+ : () => {
+ toast.error('Failed to import environment: env has no name');
+ return false;
+ }
+ )
+ .map((environment) => {
+ dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
+ .then(() => {
+ toast.success('Environment imported successfully');
+ })
+ .catch(() => toast.error('An error occurred while importing the environment'));
+ });
+ })
+ .then(() => {
+ onClose();
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
@@ -26,11 +40,14 @@ const ImportEnvironment = ({ onClose, collection }) => {
return (
-
-
- Postman Environment
-
-
+
+
+ Import your Postman environments
+
);
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
index 0a3f7e25b..3a17e2ecd 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js
@@ -4,45 +4,61 @@ import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
import ImportEnvironment from './ImportEnvironment';
+import { IconFileAlert } from '@tabler/icons';
+
+export const SharedButton = ({ children, className, onClick }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DefaultTab = ({ setTab }) => {
+ return (
+
+
+
No environments found
+
+ Get started by using the following buttons :
+
+
+ setTab('create')}>
+ Create Environment
+
+
+ Or
+
+ setTab('import')}>
+ Import Environment
+
+
+
+ );
+};
const EnvironmentSettings = ({ collection, onClose }) => {
const [isModified, setIsModified] = useState(false);
const { environments } = collection;
- const [openCreateModal, setOpenCreateModal] = useState(false);
- const [openImportModal, setOpenImportModal] = useState(false);
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
-
+ const [tab, setTab] = useState('default');
if (!environments || !environments.length) {
return (
-
- {openCreateModal && setOpenCreateModal(false)} />}
- {openImportModal && setOpenImportModal(false)} />}
-
-
No environments found!
-
setOpenCreateModal(true)}
- >
- Create Environment
-
-
-
Or
-
-
setOpenImportModal(true)}
- >
- Import Environment
-
-
+
+ {tab === 'create' ? (
+ setTab('default')} />
+ ) : tab === 'import' ? (
+ setTab('default')} />
+ ) : (
+ <>>
+ )}
+
);
diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js
index a7b67264d..797771bbb 100644
--- a/packages/bruno-app/src/components/FilePickerEditor/index.js
+++ b/packages/bruno-app/src/components/FilePickerEditor/index.js
@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
};
const clear = () => {
- onChange('');
+ onChange([]);
};
const renderButtonText = (filenames) => {
diff --git a/packages/bruno-app/src/components/FolderSettings/Headers/index.js b/packages/bruno-app/src/components/FolderSettings/Headers/index.js
index 550a835c2..0f6e05f1f 100644
--- a/packages/bruno-app/src/components/FolderSettings/Headers/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Headers/index.js
@@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
)
}
collection={collection}
+ item={folder}
/>
diff --git a/packages/bruno-app/src/components/FolderSettings/Script/index.js b/packages/bruno-app/src/components/FolderSettings/Script/index.js
index 6c51c062d..628fa5cb5 100644
--- a/packages/bruno-app/src/components/FolderSettings/Script/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Script/index.js
@@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
Pre and post-request scripts that will run before and after any request inside this folder is sent.
-
-
Pre Request
+
+
Pre Request
{
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
-
-
Post Response
+
+
Post Response
{
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
diff --git a/packages/bruno-app/src/components/FolderSettings/Tests/index.js b/packages/bruno-app/src/components/FolderSettings/Tests/index.js
index b163c6b1e..8854b06cd 100644
--- a/packages/bruno-app/src/components/FolderSettings/Tests/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Tests/index.js
@@ -37,6 +37,7 @@ const Tests = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
index dcd84d73c..d0a77de44 100644
--- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
-import Tooltip from 'components/Tooltip';
+import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -82,14 +82,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
Value
-
+
) : (
Expr
-
+
)}
@@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
)
}
collection={collection}
+ item={folder}
/>
diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js
index 6dcd9cfd2..966c36b36 100644
--- a/packages/bruno-app/src/components/FolderSettings/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/index.js
@@ -12,15 +12,15 @@ const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
let tab = 'headers';
const { folderLevelSettingsSelectedTab } = collection;
- if (folderLevelSettingsSelectedTab?.[folder.uid]) {
- tab = folderLevelSettingsSelectedTab[folder.uid];
+ if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
+ tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
- collectionUid: collection.uid,
- folderUid: folder.uid,
+ collectionUid: collection?.uid,
+ folderUid: folder?.uid,
tab
})
);
@@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
};
return (
-
+
setTab('headers')}>
diff --git a/packages/bruno-app/src/components/Icons/Dot/index.js b/packages/bruno-app/src/components/Icons/Dot/index.js
new file mode 100644
index 000000000..e889bea48
--- /dev/null
+++ b/packages/bruno-app/src/components/Icons/Dot/index.js
@@ -0,0 +1,16 @@
+import React from 'react';
+
+const DotIcon = ({ width }) => {
+ return (
+
+
+
+
+ );
+};
+
+export default DotIcon;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Tooltip/index.js b/packages/bruno-app/src/components/InfoTip/index.js
similarity index 79%
rename from packages/bruno-app/src/components/Tooltip/index.js
rename to packages/bruno-app/src/components/InfoTip/index.js
index d5ab5c41d..97eb63d4d 100644
--- a/packages/bruno-app/src/components/Tooltip/index.js
+++ b/packages/bruno-app/src/components/InfoTip/index.js
@@ -1,26 +1,25 @@
import React from 'react';
-import { Tooltip as ReactTooltip } from 'react-tooltip';
+import { Tooltip as ReactInfoTip } from 'react-tooltip';
-const Tooltip = ({ text, tooltipId }) => {
+const InfoTip = ({ text, infotipId }) => {
return (
<>
-
+
>
);
};
-export default Tooltip;
+export default InfoTip;
diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
index 65cb9c23b..85be8f137 100644
--- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
+++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
@@ -2,7 +2,6 @@ import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div`
background: transparent;
- height: inherit;
.markdown-body {
background: transparent;
overflow-y: auto;
@@ -70,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
pre {
background: ${(props) => props.theme.sidebar.bg};
+ color: ${(props) => props.theme.text};
}
table {
diff --git a/packages/bruno-app/src/components/MarkDown/index.jsx b/packages/bruno-app/src/components/MarkDown/index.jsx
index 80f28cacf..3c778c5a6 100644
--- a/packages/bruno-app/src/components/MarkDown/index.jsx
+++ b/packages/bruno-app/src/components/MarkDown/index.jsx
@@ -1,15 +1,35 @@
import MarkdownIt from 'markdown-it';
+import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
import StyledWrapper from './StyledWrapper';
-import * as React from 'react';
+import React from 'react';
-const md = new MarkdownIt();
+const Markdown = ({ collectionPath, onDoubleClick, content }) => {
+ const markdownItOptions = {
+ replaceLink: function (link, env) {
+ return link.replace(/^\./, collectionPath);
+ }
+ };
+
+ const handleOnClick = (event) => {
+ const target = event.target;
+ if (target.tagName === 'A') {
+ event.preventDefault();
+ const href = target.getAttribute('href');
+ if (href) {
+ window.open(href, '_blank');
+ return;
+ }
+ }
+ };
-const Markdown = ({ onDoubleClick, content }) => {
const handleOnDoubleClick = (event) => {
- if (event?.detail === 2) {
+ if (event.detail === 2) {
onDoubleClick();
}
};
+
+ const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);
+
const htmlFromMarkdown = md.render(content || '');
return (
@@ -17,7 +37,8 @@ const Markdown = ({ onDoubleClick, content }) => {
);
diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js
index 49fcccf02..b83549aac 100644
--- a/packages/bruno-app/src/components/Modal/index.js
+++ b/packages/bruno-app/src/components/Modal/index.js
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper';
-const ModalHeader = ({ title, handleCancel, customHeader }) => (
+const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
{customHeader ? customHeader : <>{title ?
{title}
: null}>}
- {handleCancel ? (
+ {handleCancel && !hideClose ? (
handleCancel() : null}>
×
@@ -63,6 +63,7 @@ const Modal = ({
confirmDisabled,
hideCancel,
hideFooter,
+ hideClose,
disableCloseOnOutsideClick,
disableEscapeKey,
onClick,
@@ -100,7 +101,12 @@ const Modal = ({
return (
onClick(e) : null}>
-
closeModal({ type: 'icon' })} customHeader={customHeader} />
+ closeModal({ type: 'icon' })}
+ customHeader={customHeader}
+ />
{children}
{
@@ -104,10 +106,10 @@ class MultiLineEditor extends Component {
// event loop.
this.ignoreChangeEvent = true;
- let variables = getAllVariables(this.props.collection);
+ let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
- this.addOverlay();
+ this.addOverlay(variables);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
@@ -126,10 +128,8 @@ class MultiLineEditor extends Component {
this.editor.getWrapperElement().remove();
}
- addOverlay = () => {
- let variables = getAllVariables(this.props.collection);
+ addOverlay = (variables) => {
this.variables = variables;
-
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables');
};
diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js
index 15a055c76..245538541 100644
--- a/packages/bruno-app/src/components/Notifications/index.js
+++ b/packages/bruno-app/src/components/Notifications/index.js
@@ -10,6 +10,8 @@ import {
} from 'providers/ReduxStore/slices/notifications';
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
+import ToolHint from 'components/ToolHint';
+import { useTheme } from 'providers/Theme';
const PAGE_SIZE = 5;
@@ -20,6 +22,7 @@ const Notifications = () => {
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
+ const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -85,21 +88,22 @@ const Notifications = () => {
return (
{
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
>
- 0 ? 'bell' : ''}`}
- />
- {unreadNotifications.length > 0 && (
- {unreadNotifications.length}
- )}
+
+ 0 ? 'bell' : ''}`}
+ />
+ {unreadNotifications.length > 0 && (
+ {unreadNotifications.length}
+ )}
+
{showNotificationsModal && (
@@ -129,9 +133,8 @@ const Notifications = () => {
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
{notification?.title}
@@ -141,9 +144,8 @@ const Notifications = () => {
{'Prev'}
@@ -159,9 +161,8 @@ const Notifications = () => {
{'Next'}
diff --git a/packages/bruno-app/src/components/Preferences/Font/index.js b/packages/bruno-app/src/components/Preferences/Font/index.js
index 2f27fea8b..ef6ac9f2f 100644
--- a/packages/bruno-app/src/components/Preferences/Font/index.js
+++ b/packages/bruno-app/src/components/Preferences/Font/index.js
@@ -9,17 +9,25 @@ const Font = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
+ const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '14'));
- const handleInputChange = (event) => {
+ const handleCodeFontChange = (event) => {
setCodeFont(event.target.value);
};
+ const handleCodeFontSizeChange = (event) => {
+ // Restrict to min/max value
+ const clampedSize = Math.max(1, Math.min(event.target.value, 32));
+ setCodeFontSize(clampedSize);
+ };
+
const handleSave = () => {
dispatch(
savePreferences({
...preferences,
font: {
- codeFont
+ codeFont,
+ codeFontSize
}
})
).then(() => {
@@ -29,17 +37,33 @@ const Font = ({ close }) => {
return (
- Code Editor Font
-
+
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
index 42d06266d..ed5724c7c 100644
--- a/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
index 849421661..3c27786ef 100644
--- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
@@ -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 (
- Global Proxy Settings
@@ -184,6 +189,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleRegionChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
/>
@@ -196,6 +202,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleProfileNameChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
index 845dae273..8582a53cd 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js
@@ -55,6 +55,7 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
/>
@@ -67,6 +68,8 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
index 77198d311..bef4d062a 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js
@@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
onChange={(val) => handleTokenChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
index e43f18c46..e91ed8d1f 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js
@@ -55,6 +55,7 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
/>
@@ -67,6 +68,8 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={true}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
index 3c813b14b..2bb5dcc35 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js
@@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -92,6 +92,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
index 67bc277aa..a100ce8e5 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js
@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
index 7edb8bb25..a43c8f0ad 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js
@@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -55,6 +55,8 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
index 164dcaab4..f2cd88ae3 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js
@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
index 1e64d4faa..4ec8c1faa 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js
@@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
{inputsConfig.map((input) => {
- const { key, label } = input;
+ const { key, label, isSecret } = input;
return (
{label}
@@ -57,6 +57,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
+ item={item}
+ isSecret={isSecret}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
index 6366bb5e7..32f2c999c 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js
@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'password',
- label: 'Password'
+ label: 'Password',
+ isSecret: true
},
{
key: 'clientId',
@@ -17,7 +18,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
- label: 'Client Secret'
+ label: 'Client Secret',
+ isSecret: true
},
{
key: 'scope',
diff --git a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js
index a358e2ed3..22de4735b 100644
--- a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js
@@ -110,6 +110,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
allowNewlines={true}
onRun={handleRun}
collection={collection}
+ item={item}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
index 5bdd9c5e7..187a91a68 100644
--- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
@@ -31,6 +31,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
: get(item, 'request.body.graphql.variables');
const { displayedTheme } = useTheme();
const [schema, setSchema] = useState(null);
+ const preferences = useSelector((state) => state.app.preferences);
useEffect(() => {
onSchemaLoad(schema);
@@ -71,6 +72,8 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
+ font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
/>
);
}
@@ -151,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
- {getTabPanel(focusedTab.requestPaneTab)}
+ {getTabPanel(focusedTab.requestPaneTab)}
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
index a7978ebd7..91fea0134 100644
--- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
@@ -6,6 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
+import { format, applyEdits } from 'jsonc-parser';
+import { IconWand } from '@tabler/icons';
+import toast from 'react-hot-toast';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
@@ -13,6 +16,25 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
+ const onPrettify = () => {
+ if (!variables) return;
+ try {
+ const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
+ const prettyVariables = applyEdits(variables, edits);
+ dispatch(
+ updateRequestGraphqlVariables({
+ variables: prettyVariables,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ toast.success('Variables prettified');
+ } catch (error) {
+ console.error(error);
+ toast.error('Error occurred while prettifying GraphQL variables');
+ }
+ };
+
const onEdit = (value) => {
dispatch(
updateRequestGraphqlVariables({
@@ -27,12 +49,20 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
-
+
+
+
+
props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
+
+ .content-indicator {
+ color: ${(props) => props.theme.text}
+ }
}
}
`;
diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
index df90082c6..d2032f7f4 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
@@ -7,7 +7,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Auth from 'components/RequestPane/Auth';
-import AuthMode from 'components/RequestPane/Auth/AuthMode';
+import DotIcon from 'components/Icons/Dot';
import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
@@ -16,6 +16,12 @@ import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
+const ContentIndicator = () => {
+ return
+
+
+};
+
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -82,12 +88,18 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
- // get the length of active params, headers, asserts and vars
- const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
- const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
- const assertions = item.draft ? get(item, 'draft.request.assertions', []) : get(item, 'request.assertions', []);
- const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
- const responseVars = item.draft ? get(item, 'draft.request.vars.res', []) : get(item, 'request.vars.res', []);
+ // get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
+ const getPropertyFromDraftOrRequest = (propertyKey) =>
+ item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
+ const params = getPropertyFromDraftOrRequest('request.params');
+ const body = getPropertyFromDraftOrRequest('request.body');
+ const headers = getPropertyFromDraftOrRequest('request.headers');
+ 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');
const activeParamsLength = params.filter((param) => param.enabled).length;
const activeHeadersLength = headers.filter((header) => header.enabled).length;
@@ -105,10 +117,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('body')}>
Body
+ {body.mode !== 'none' && }
selectTab('headers')}>
Headers
- {activeHeadersLength > 0 && {activeHeadersLength} }
+ {activeHeadersLength > 0 && {activeHeadersLength} }
selectTab('auth')}>
Auth
@@ -119,6 +132,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('script')}>
Script
+ {(script.req || script.res) && }
selectTab('assert')}>
Assert
@@ -126,9 +140,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('tests')}>
Tests
+ {tests && tests.length > 0 && }
selectTab('docs')}>
Docs
+ {docs && docs.length > 0 && }
{focusedTab.requestPaneTab === 'body' ? (
@@ -137,7 +153,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
) : null}
diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
index 4ddd64218..af23a645e 100644
--- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
@@ -24,7 +24,8 @@ const MultipartFormParams = ({ item, collection }) => {
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
- type: 'text'
+ type: 'text',
+ value: ''
})
);
};
@@ -34,7 +35,8 @@ const MultipartFormParams = ({ item, collection }) => {
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
- type: 'file'
+ type: 'file',
+ value: []
})
);
};
@@ -144,6 +146,7 @@ const MultipartFormParams = ({ item, collection }) => {
onRun={handleRun}
allowNewlines={true}
collection={collection}
+ item={item}
/>
)}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
index 99d5ed3b9..439c7a574 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js
@@ -4,8 +4,9 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
- /* todo: find a better way */
- height: calc(100vh - 220px);
+ font-family: ${(props) => (props.font ? props.font : 'default')};
+ font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
+ flex: 1 1 0;
}
textarea.cm-editor {
diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
index 598af0212..2da307ee9 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
@@ -209,8 +209,10 @@ export default class QueryEditor extends React.Component {
return (
<>
{
this._node = node;
}}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
index 5c3e1d537..b460c1b4f 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js
@@ -22,14 +22,12 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
+ }
+ }
- &:nth-child(1) {
- width: 30%;
- }
-
- &:nth-child(3) {
- width: 70px;
- }
+ td {
+ &:nth-child(1) {
+ padding: 0 0 0 8px;
}
}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
index 162d57a43..a1099f4fd 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
@@ -1,20 +1,23 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
-import has from 'lodash/has';
+import InfoTip from 'components/InfoTip';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
addQueryParam,
+ updateQueryParam,
deleteQueryParam,
- updatePathParam,
- updateQueryParam
+ moveQueryParam,
+ updatePathParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
+import Table from 'components/Table/index';
+import ReorderTable from 'components/ReorderTable';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -100,79 +103,94 @@ const QueryParams = ({ item, collection }) => {
);
};
+ const handleParamDrag = ({ updateReorderedItem }) => {
+ dispatch(
+ moveQueryParam({
+ collectionUid: collection.uid,
+ itemUid: item.uid,
+ updateReorderedItem
+ })
+ );
+ };
+
return (
-
+
Query
-
-
-
- Name
- Value
-
-
-
-
+
+
+
{queryParams && queryParams.length
- ? queryParams.map((param, index) => {
- return (
-
-
+ ? queryParams.map((param, index) => (
+
+
+ handleQueryParamChange(e, param, 'name')}
+ />
+
+
+ handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
+ onRun={handleRun}
+ collection={collection}
+ variablesAutocomplete={true}
+ />
+
+
+
handleQueryParamChange(e, param, 'name')}
+ type="checkbox"
+ checked={param.enabled}
+ tabIndex="-1"
+ className="mr-3 mousetrap"
+ onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
/>
-
-
-
- handleQueryParamChange(
- {
- target: {
- value: newValue
- }
- },
- param,
- 'value'
- )
- }
- onRun={handleRun}
- collection={collection}
- />
-
-
-
- handleQueryParamChange(e, param, 'enabled')}
- />
- handleRemoveQueryParam(param)}>
-
-
-
-
-
- );
- })
+ handleRemoveQueryParam(param)}>
+
+
+
+
+
+ ))
: null}
-
-
+
+
+
+ Add Param
-
Path
+
+ Path
+
+ Path variables are automatically added whenever the
+ :name
+ template is used in the URL. For example:
+
+ https://example.com/v1/users/:id
+
+
+ `}
+ infotipId="path-param-InfoTip"
+ />
+
@@ -214,6 +232,7 @@ const QueryParams = ({ item, collection }) => {
}
onRun={handleRun}
collection={collection}
+ item={item}
/>
@@ -222,6 +241,11 @@ const QueryParams = ({ item, collection }) => {
: null}
+ {!(pathParams && pathParams.length) ?
+
+
+
+ : null}
);
diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js
index 2308dec4f..cca562025 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js
@@ -33,18 +33,18 @@ const Wrapper = styled.div`
top: 1px;
}
- .tooltip {
+ .infotip {
position: relative;
display: inline-block;
cursor: pointer;
}
- .tooltip:hover .tooltiptext {
+ .infotip:hover .infotiptext {
visibility: visible;
opacity: 1;
}
- .tooltiptext {
+ .infotiptext {
visibility: hidden;
width: auto;
background-color: ${(props) => props.theme.requestTabs.active.bg};
@@ -62,7 +62,7 @@ const Wrapper = styled.div`
white-space: nowrap;
}
- .tooltiptext::after {
+ .infotiptext::after {
content: '';
position: absolute;
top: 100%;
diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
index 88fe4ee01..bcbf55c91 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js
@@ -69,11 +69,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
collection={collection}
+ highlightPathParams={true}
item={item}
/>
{
e.stopPropagation();
if (!item.draft) return;
@@ -86,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
size={22}
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
/>
-
+
Save ({saveShortcut})
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
index 83ebd8140..42da81d61 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js
@@ -1,10 +1,6 @@
import styled from 'styled-components';
const Wrapper = styled.div`
- div.CodeMirror {
- /* todo: find a better way */
- height: calc(100vh - 220px);
- }
`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
index b776351d7..5fde52ea0 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
@@ -50,6 +50,7 @@ const RequestBody = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}
diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
index 445505c07..9aa3b621f 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
@@ -9,6 +9,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
+import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
@@ -115,8 +116,10 @@ const RequestHeaders = ({ item, collection }) => {
)
}
onRun={handleRun}
+ autocomplete={MimeTypes}
allowNewlines={true}
collection={collection}
+ item={item}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Script/index.js b/packages/bruno-app/src/components/RequestPane/Script/index.js
index 935b52ede..ec4f4df95 100644
--- a/packages/bruno-app/src/components/RequestPane/Script/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Script/index.js
@@ -40,26 +40,28 @@ const Script = ({ item, collection }) => {
return (
-
-
Pre Request
+
-
-
Post Response
+
+
Post Response
{
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
index 01cf0f340..84f040c6e 100644
--- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
-import Tooltip from 'components/Tooltip';
+import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -83,14 +83,14 @@ const VarsTable = ({ item, collection, vars, varType }) => {
Value
-
+
) : (
Expr
-
+
)}
@@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
}
onRun={handleRun}
collection={collection}
+ item={item}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/index.js b/packages/bruno-app/src/components/RequestPane/Vars/index.js
index 500ebb25b..eb292e9c2 100644
--- a/packages/bruno-app/src/components/RequestPane/Vars/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Vars/index.js
@@ -9,11 +9,11 @@ const Vars = ({ item, collection }) => {
return (
-
+
-
+
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
index db0e45e41..cb62ac8a0 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js
@@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
return (
-
+
Request no longer exists.
This can happen when the .bru file associated with this request was deleted on your filesystem.
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index 2fd253f4b..51d3194be 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
+import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300;
@@ -137,6 +138,10 @@ const RequestTabPanel = () => {
return
;
}
+ if (focusedTab.type === 'security-settings') {
+ return
;
+ }
+
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return
;
@@ -158,10 +163,9 @@ const RequestTabPanel = () => {
{item.type === 'graphql-request' ? (
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/StyledWrapper.js
index ec278887d..39cd89e4c 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/StyledWrapper.js
@@ -2,4 +2,4 @@ import styled from 'styled-components';
const StyledWrapper = styled.div``;
-export default StyledWrapper;
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
index ce86b820c..26ec31545 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
@@ -4,7 +4,9 @@ import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
+import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
+import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
const CollectionToolBar = ({ collection }) => {
const dispatch = useDispatch();
@@ -48,13 +50,22 @@ const CollectionToolBar = ({ collection }) => {
-
+
-
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
index f8d7a992a..8745cf079 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
+import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
@@ -12,6 +12,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
>
);
}
+ case 'security-settings': {
+ return (
+ <>
+
+ Security
+ >
+ )
+ }
case 'folder-settings': {
return (
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
index 680782169..64d6eebb5 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
@@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
+import Dropdown from 'components/Dropdown';
+import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
+import NewRequest from 'components/Sidebar/NewRequest/index';
-const RequestTab = ({ tab, collection, folderUid }) => {
+const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
+ const dropdownTippyRef = useRef();
+ const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
+
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
@@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
+ const handleRightClick = (_event) => {
+ const menuDropdown = dropdownTippyRef.current;
+ if (!menuDropdown) {
+ return;
+ }
+
+ if (menuDropdown.state.isShown) {
+ menuDropdown.hide();
+ } else {
+ menuDropdown.show();
+ }
+ };
+
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
@@ -43,45 +62,11 @@ const RequestTab = ({ tab, collection, folderUid }) => {
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
-
- let color = '';
- method = method.toLocaleLowerCase();
-
- switch (method) {
- case 'get': {
- color = theme.request.methods.get;
- break;
- }
- case 'post': {
- color = theme.request.methods.post;
- break;
- }
- case 'put': {
- color = theme.request.methods.put;
- break;
- }
- case 'delete': {
- color = theme.request.methods.delete;
- break;
- }
- case 'patch': {
- color = theme.request.methods.patch;
- break;
- }
- case 'options': {
- color = theme.request.methods.options;
- break;
- }
- case 'head': {
- color = theme.request.methods.head;
- break;
- }
- }
-
- return color;
+ return theme.request.methods[method.toLocaleLowerCase()];
};
+
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
- if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
+ if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
{tab.type === 'folder-settings' ? (
@@ -143,6 +128,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
)}
{
if (!item.draft) return handleMouseUp(e);
@@ -159,6 +145,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
{item.name}
+
{
);
};
+function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
+ const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
+ const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
+
+ const totalTabs = collectionRequestTabs.length || 0;
+ const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
+ const currentTabItem = findItemInCollection(collection, currentTabUid);
+
+ const hasLeftTabs = tabIndex !== 0;
+ const hasRightTabs = totalTabs > tabIndex + 1;
+ const hasOtherTabs = totalTabs > 1;
+
+ async function handleCloseTab(event, tabUid) {
+ event.stopPropagation();
+ dropdownTippyRef.current.hide();
+
+ if (!tabUid) {
+ return;
+ }
+
+ try {
+ const item = findItemInCollection(collection, tabUid);
+ // silently save unsaved changes before closing the tab
+ if (item.draft) {
+ await dispatch(saveRequest(item.uid, collection.uid, true));
+ }
+
+ dispatch(closeTabs({ tabUids: [tabUid] }));
+ } catch (err) {}
+ }
+
+ function handleCloseOtherTabs(event) {
+ dropdownTippyRef.current.hide();
+
+ const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
+ otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseTabsToTheLeft(event) {
+ dropdownTippyRef.current.hide();
+
+ const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
+ leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseTabsToTheRight(event) {
+ dropdownTippyRef.current.hide();
+
+ const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
+ rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ function handleCloseSavedTabs(event) {
+ event.stopPropagation();
+
+ const savedTabs = collection.items.filter((item) => !item.draft);
+ const savedTabIds = savedTabs.map((item) => item.uid) || [];
+ dispatch(closeTabs({ tabUids: savedTabIds }));
+ }
+
+ function handleCloseAllTabs(event) {
+ collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
+ }
+
+ return (
+
+ {showAddNewRequestModal && (
+ setShowAddNewRequestModal(false)} />
+ )}
+
+ {showCloneRequestModal && (
+ setShowCloneRequestModal(false)}
+ />
+ )}
+
+ } placement="bottom-start">
+ {
+ dropdownTippyRef.current.hide();
+ setShowAddNewRequestModal(true);
+ }}
+ >
+ New Request
+
+ {
+ dropdownTippyRef.current.hide();
+ setShowCloneRequestModal(true);
+ }}
+ >
+ Clone Request
+
+ handleCloseTab(e, currentTabUid)}>
+ Close
+
+
+ Close Others
+
+
+ Close to the Left
+
+
+ Close to the Right
+
+
+ Close Saved
+
+
+ Close All
+
+
+
+ );
+}
+
export default RequestTab;
diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
index ec76ec5b5..93829cca9 100644
--- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
@@ -7,13 +7,14 @@ const Wrapper = styled.div`
padding: 0;
margin: 0;
display: flex;
- position: relative;
overflow: scroll;
&::-webkit-scrollbar {
display: none;
}
+ scrollbar-width: none;
+
li {
display: inline-flex;
max-width: 150px;
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index fbafb55cf..d0cd0b459 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -110,7 +110,14 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
-
+
);
})
diff --git a/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js
index a341acdc2..045a9dcc3 100644
--- a/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js
@@ -3,6 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
height: 100%;
+ width: calc(100% - 0.75rem);
z-index: 1;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};
diff --git a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js
index b203053fb..91fb02d78 100644
--- a/packages/bruno-app/src/components/ResponsePane/Overlay/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/Overlay/index.js
@@ -13,7 +13,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
};
return (
-
+
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
index a07acc95f..bd52c410a 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
@@ -2,7 +2,7 @@ import { IconFilter, IconX } from '@tabler/icons';
import React, { useMemo } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
-import { Tooltip as ReactTooltip } from 'react-tooltip';
+import { Tooltip as ReactInfotip } from 'react-tooltip';
const QueryResultFilter = ({ filter, onChange, mode }) => {
const inputRef = useRef(null);
@@ -19,7 +19,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
};
- const tooltipText = useMemo(() => {
+ const infotipText = useMemo(() => {
if (mode.includes('json')) {
return 'Filter with JSONPath';
}
@@ -46,10 +46,10 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
return (
- {tooltipText && !isExpanded &&
}
+ {infotipText && !isExpanded &&
}
{
autoCapitalize="off"
spellCheck="false"
className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
- isExpanded ? 'w-full opacity-100' : 'w-[0] opacity-0'
+ isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`}
onChange={onChange}
/>
-
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
index 13b280320..86d79c4bc 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
@@ -81,6 +81,7 @@ const QueryResultPreview = ({
{
});
});
- let requestData = safeStringifyJSON(request.data);
+ let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
return (
@@ -35,7 +35,8 @@ const Timeline = ({ request, response }) => {
{requestData ? (
- {'>'} data {requestData}
+ {'>'} data{' '}
+ {requestData}
) : null}
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index 02edc106e..f0df42e3e 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -97,6 +97,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
});
};
+ const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
+
return (
@@ -105,7 +107,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
selectTab('headers')}>
Headers
- {response.headers?.length > 0 && {response.headers.length} }
+ {responseHeadersCount > 0 && {responseHeadersCount} }
selectTab('timeline')}>
Timeline
diff --git a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
index 0178b90d7..38dd7511e 100644
--- a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js
@@ -1,6 +1,19 @@
import styled from 'styled-components';
const Wrapper = styled.div`
+ .textbox {
+ border: 1px solid #ccc;
+ padding: 0.2rem 0.5rem;
+ box-shadow: none;
+ border-radius: 0px;
+ outline: none;
+ box-shadow: none;
+ transition: border-color ease-in-out 0.1s;
+ border-radius: 3px;
+ background-color: ${(props) => props.theme.modal.input.bg};
+ border: 1px solid ${(props) => props.theme.modal.input.border};
+ }
+
.item-path {
.link {
color: ${(props) => props.theme.textLink};
diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx
index e415aeb3c..4b0b68cba 100644
--- a/packages/bruno-app/src/components/RunnerResults/index.jsx
+++ b/packages/bruno-app/src/components/RunnerResults/index.jsx
@@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null);
+ const [delay, setDelay] = useState(null);
// ref for the runner output body
const runnerBodyRef = useRef();
@@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
.filter(Boolean);
const runCollection = () => {
- dispatch(runCollectionFolder(collection.uid, null, true));
+ dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
};
const runAgain = () => {
- dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
+ dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
};
const resetRunner = () => {
@@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
You have {totalRequestsInCollection} requests in this collection.
+
+ Delay (in ms)
+ setDelay(e.target.value)}
+ />
+
+
Run Collection
@@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
{item.status !== 'error' && item.status !== 'completed' ? (
- ) : (
+ ) : item.responseReceived?.status ? (
setSelectedItem(item)}>
- ({get(item.responseReceived, 'status')}
- {get(item.responseReceived, 'statusText')} )
+ ({item.responseReceived?.status}
+ {item.responseReceived?.statusText} )
+
+ ) : (
+ setSelectedItem(item)}>
+ (request failed)
)}
diff --git a/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/StyledWrapper.js b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/StyledWrapper.js
new file mode 100644
index 000000000..cad253fd9
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/StyledWrapper.js
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .safe-mode {
+ padding: 0.15rem 0.3rem;
+ color: ${(props) => props.theme.colors.text.green};
+ border: solid 1px ${(props) => props.theme.colors.text.green} !important;
+ }
+ .developer-mode {
+ padding: 0.15rem 0.3rem;
+ color: ${(props) => props.theme.colors.text.yellow};
+ border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js
new file mode 100644
index 000000000..c4ab2dbf2
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js
@@ -0,0 +1,45 @@
+import { useDispatch } from 'react-redux';
+import { IconShieldLock } from '@tabler/icons';
+import { addTab } from 'providers/ReduxStore/slices/tabs';
+import { uuid } from 'utils/common/index';
+import JsSandboxModeModal from '../JsSandboxModeModal';
+import StyledWrapper from './StyledWrapper';
+
+const JsSandboxMode = ({ collection }) => {
+ const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
+ const dispatch = useDispatch();
+
+ const viewSecuritySettings = () => {
+ dispatch(
+ addTab({
+ uid: uuid(),
+ collectionUid: collection.uid,
+ type: 'security-settings'
+ })
+ );
+ };
+
+ return (
+
+ {jsSandboxMode === 'safe' && (
+
+ Safe Mode
+
+ )}
+ {jsSandboxMode === 'developer' && (
+
+ Developer Mode
+
+ )}
+ {!jsSandboxMode ? : null}
+
+ );
+};
+
+export default JsSandboxMode;
diff --git a/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/StyledWrapper.js b/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/StyledWrapper.js
new file mode 100644
index 000000000..ecaab4ff1
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/StyledWrapper.js
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ max-width: 800px;
+
+ span.beta-tag {
+ display: flex;
+ align-items: center;
+ padding: 0.1rem 0.25rem;
+ font-size: 0.75rem;
+ border-radius: 0.25rem;
+ color: ${(props) => props.theme.colors.text.green};
+ border: solid 1px ${(props) => props.theme.colors.text.green} !important;
+ }
+
+ span.developer-mode-warning {
+ font-weight: 400;
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/index.js b/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/index.js
new file mode 100644
index 000000000..52a988ea7
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/JsSandboxModeModal/index.js
@@ -0,0 +1,98 @@
+import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
+import { useDispatch } from 'react-redux';
+import toast from 'react-hot-toast';
+import { useState } from 'react';
+import Portal from 'components/Portal';
+import Modal from 'components/Modal';
+import StyledWrapper from './StyledWrapper';
+
+const JsSandboxModeModal = ({ collection }) => {
+ const dispatch = useDispatch();
+ const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
+
+ const handleChange = (e) => {
+ setJsSandboxMode(e.target.value);
+ };
+
+ const handleSave = () => {
+ dispatch(
+ saveCollectionSecurityConfig(collection?.uid, {
+ jsSandboxMode: jsSandboxMode
+ })
+ )
+ .then(() => {
+ toast.success('Sandbox mode updated successfully');
+ })
+ .catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
+ };
+
+ return (
+
+
+
+
+ The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
+
+
+
+ Please choose the security level for the JavaScript code execution.
+
+
+
+
+
+
+ Safe Mode
+
+ BETA
+
+
+ JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
+
+
+
+
+
+ Developer Mode
+ (use only if you trust the collections authors)
+
+
+
+ JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
+
+
+ * SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
+
+
+
+
+
+ );
+};
+
+export default JsSandboxModeModal;
diff --git a/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js b/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js
new file mode 100644
index 000000000..ecaab4ff1
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ max-width: 800px;
+
+ span.beta-tag {
+ display: flex;
+ align-items: center;
+ padding: 0.1rem 0.25rem;
+ font-size: 0.75rem;
+ border-radius: 0.25rem;
+ color: ${(props) => props.theme.colors.text.green};
+ border: solid 1px ${(props) => props.theme.colors.text.green} !important;
+ }
+
+ span.developer-mode-warning {
+ font-weight: 400;
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/SecuritySettings/index.js b/packages/bruno-app/src/components/SecuritySettings/index.js
new file mode 100644
index 000000000..7761760f6
--- /dev/null
+++ b/packages/bruno-app/src/components/SecuritySettings/index.js
@@ -0,0 +1,86 @@
+import { useState } from 'react';
+import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
+import toast from 'react-hot-toast';
+import StyledWrapper from './StyledWrapper';
+import { useDispatch } from 'react-redux';
+
+const SecuritySettings = ({ collection }) => {
+ const dispatch = useDispatch();
+ const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
+
+ const handleChange = (e) => {
+ setJsSandboxMode(e.target.value);
+ };
+
+ const handleSave = () => {
+ dispatch(
+ saveCollectionSecurityConfig(collection?.uid, {
+ jsSandboxMode: jsSandboxMode
+ })
+ )
+ .then(() => {
+ toast.success('Sandbox mode updated successfully');
+ })
+ .catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
+ };
+
+ return (
+
+ JavaScript Sandbox
+
+
+ The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
+
+
+
+
+
+
+
+ Safe Mode
+
+ BETA
+
+
+ JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
+
+
+
+
+
+ Developer Mode
+ (use only if you trust the collections authors)
+
+
+
+ JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
+
+
+
+ Save
+
+
+ * SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
+
+
+
+ );
+};
+
+export default SecuritySettings;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
index cd9857a15..1dccf6adb 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
-import Tooltip from 'components/Tooltip';
+import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CloneCollection = ({ onClose, collection }) => {
@@ -44,7 +44,7 @@ const CloneCollection = ({ onClose, collection }) => {
toast.success('Collection created');
onClose();
})
- .catch(() => toast.error('An error occurred while creating the collection'));
+ .catch((e) => toast.error('An error occurred while creating the collection - ' + e));
}
});
@@ -126,9 +126,9 @@ const CloneCollection = ({ onClose, collection }) => {
Folder Name
-
{
id="collection-item-name"
type="text"
name="name"
+ placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
index 418658f03..ff06f4f31 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js
@@ -2,6 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
+ height: 100%;
.copy-to-clipboard {
position: absolute;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
index 102d57065..78977cabb 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js
@@ -53,6 +53,7 @@ const CodeView = ({ language, item }) => {
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
+ fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
mode={lang}
/>
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
index a62469910..2b19d461b 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
@@ -73,7 +73,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
const interpolatedUrl = interpolateUrl({
url: requestUrl,
envVars,
- collectionVariables: collection.collectionVariables,
+ runtimeVariables: collection.runtimeVariables,
processEnvVars: collection.processEnvVariables
});
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/index.js
index 87315dfea..e41309871 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/index.js
@@ -23,7 +23,9 @@ const RequestMethod = ({ item }) => {
return (
- {item.request.method}
+
+ {item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
+
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
index 14d7432fa..8d61203e1 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
@@ -65,7 +65,7 @@ const Wrapper = styled.div`
div.dropdown-item.delete-item {
color: ${(props) => props.theme.colors.danger};
&:hover {
- background-color: ${(props) => props.theme.colors.bg.danger};
+ background-color: ${(props) => props.theme.colors.bg.danger} !important;
color: white;
}
}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
index 9fce06eec..6e5947e58 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
@@ -189,16 +189,28 @@ const CollectionItem = ({ item, collection, searchText }) => {
toast.error('URL is required');
}
};
+
const viewFolderSettings = () => {
- dispatch(
- addTab({
- uid: uuid(),
- collectionUid: collection.uid,
- folderUid: item.uid,
- type: 'folder-settings'
- })
- );
+ if (isItemAFolder(item)) {
+ if (itemIsOpenedInTabs(item, tabs)) {
+ dispatch(
+ focusTab({
+ uid: item.uid
+ })
+ );
+ return;
+ }
+ dispatch(
+ addTab({
+ uid: item.uid,
+ collectionUid: collection.uid,
+ type: 'folder-settings'
+ })
+ );
+ return;
+ }
};
+
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js
index e5a657ef9..91018594f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js
@@ -91,13 +91,13 @@ const Collections = () => {
setSearchText(e.target.value.toLowerCase())}
/>
@@ -115,7 +115,7 @@ const Collections = () => {
)}
-
+
{collections && collections.length
? collections.map((c) => {
return (
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 168c922cd..996c314df 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
-import Tooltip from 'components/Tooltip';
+import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CreateCollection = ({ onClose }) => {
@@ -37,7 +37,7 @@ const CreateCollection = ({ onClose }) => {
toast.success('Collection created');
onClose();
})
- .catch(() => toast.error('An error occurred while creating the collection'));
+ .catch((e) => toast.error('An error occurred while creating the collection - ' + e));
}
});
@@ -119,9 +119,9 @@ const CreateCollection = ({ onClose }) => {
Folder Name
-
{
{children}
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
index 4211e8ff1..7dd827298 100644
--- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
+++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js
@@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
collectionLocation: Yup.string()
.min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less')
- .required('name is required')
+ .required('Location is required')
}),
onSubmit: (values) => {
handleSubmit(values.collectionLocation);
@@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
- formik.setFieldValue('collectionLocation', dirPath);
+ if (typeof dirPath === 'string' && dirPath.length > 0) {
+ formik.setFieldValue('collectionLocation', dirPath);
+ }
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -160,7 +162,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
type="text"
name="collectionLocation"
readOnly={true}
- className="block textbox mt-2 w-full"
+ className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index 8d8125e94..50e7be277 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
- body: request.body
+ body: request.body,
+ auth: request.auth
})
)
.then(() => onClose())
@@ -161,7 +162,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
return (
-
@@ -137,7 +140,7 @@ const Sidebar = () => {
-
+
);
};
diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js
index 3b901fc25..31d0875fd 100644
--- a/packages/bruno-app/src/components/SingleLineEditor/index.js
+++ b/packages/bruno-app/src/components/SingleLineEditor/index.js
@@ -1,8 +1,9 @@
import React, { Component } from 'react';
import isEqual from 'lodash/isEqual';
import { getAllVariables } from 'utils/collections';
-import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
+import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
+import { IconEye, IconEyeOff } from '@tabler/icons';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
this.cachedValue = props.value || '';
this.editorRef = React.createRef();
this.variables = {};
+
+ this.state = {
+ maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
+ };
}
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
+ const runHandler = () => {
+ if (this.props.onRun) {
+ this.props.onRun();
+ }
+ };
+ const saveHandler = () => {
+ if (this.props.onSave) {
+ this.props.onSave();
+ }
+ };
+ const noopHandler = () => {};
+
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
@@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
scrollbarStyle: null,
tabindex: 0,
extraKeys: {
- Enter: () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Ctrl-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Cmd-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
+ Enter: runHandler,
+ 'Ctrl-Enter': runHandler,
+ 'Cmd-Enter': runHandler,
'Alt-Enter': () => {
if (this.props.allowNewlines) {
this.editor.setValue(this.editor.getValue() + '\n');
@@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
this.props.onRun();
}
},
- 'Shift-Enter': () => {
- if (this.props.onRun) {
- this.props.onRun();
- }
- },
- 'Cmd-S': () => {
- if (this.props.onSave) {
- this.props.onSave();
- }
- },
- 'Ctrl-S': () => {
- if (this.props.onSave) {
- this.props.onSave();
- }
- },
- 'Cmd-F': () => {},
- 'Ctrl-F': () => {},
+ 'Shift-Enter': runHandler,
+ 'Cmd-S': saveHandler,
+ 'Ctrl-S': saveHandler,
+ 'Cmd-F': noopHandler,
+ 'Ctrl-F': noopHandler,
// Tabbing disabled to make tabindex work
Tab: false,
'Shift-Tab': false
@@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay(variables);
+ this._enableMaskedEditor(this.props.isSecret);
+ this.setState({ maskInput: this.props.isSecret });
}
+ /** Enable or disable masking the rendered content of the editor */
+ _enableMaskedEditor = (enabled) => {
+ if (typeof enabled !== 'boolean') return;
+
+ console.log('Enabling masked editor: ' + enabled);
+ if (enabled == true) {
+ if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
+ this.maskedEditor.enable();
+ } else {
+ this.maskedEditor?.disable();
+ this.maskedEditor = null;
+ }
+ };
+
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();
@@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
this.cachedValue = String(this.props.value);
this.editor.setValue(String(this.props.value) || '');
}
+ if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
+ // If the secret flag has changed, update the editor to reflect the change
+ this._enableMaskedEditor(this.props.isSecret);
+ // also set the maskInput flag to the new value
+ this.setState({ maskInput: this.props.isSecret });
+ }
this.ignoreChangeEvent = false;
}
@@ -131,12 +146,39 @@ class SingleLineEditor extends Component {
addOverlay = (variables) => {
this.variables = variables;
- defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
- this.editor.setOption('mode', 'combinedmode');
+ defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
+ this.editor.setOption('mode', 'brunovariables');
+ };
+
+ toggleVisibleSecret = () => {
+ const isVisible = !this.state.maskInput;
+ this.setState({ maskInput: isVisible });
+ this._enableMaskedEditor(isVisible);
+ };
+
+ /**
+ * @brief Eye icon to show/hide the secret value
+ * @returns ReactComponent The eye icon
+ */
+ secretEye = (isSecret) => {
+ return isSecret === true ? (
+
this.toggleVisibleSecret()}>
+ {this.state.maskInput === true ? (
+
+ ) : (
+
+ )}
+
+ ) : null;
};
render() {
- return
;
+ return (
+
+
+ {this.secretEye(this.props.isSecret)}
+
+ );
}
}
export default SingleLineEditor;
diff --git a/packages/bruno-app/src/components/Table/StyledWrapper.js b/packages/bruno-app/src/components/Table/StyledWrapper.js
new file mode 100644
index 000000000..e35f11b3a
--- /dev/null
+++ b/packages/bruno-app/src/components/Table/StyledWrapper.js
@@ -0,0 +1,63 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ table {
+ width: 100%;
+ display: grid;
+ overflow-y: hidden;
+ overflow-x: auto;
+
+ // for icon hover
+ position: inherit;
+ left: -4px;
+ padding-left: 4px;
+ padding-right: 4px;
+
+ grid-template-columns: ${({ columns }) =>
+ columns?.[0]?.width
+ ? columns.map((col) => `${col?.width}`).join(' ')
+ : columns.map((col) => `${100 / columns.length}%`).join(' ')};
+ }
+
+ table thead,
+ table tbody,
+ table tr {
+ display: contents;
+ }
+
+ table th {
+ position: relative;
+ }
+
+ table tr td {
+ padding: 0.5rem;
+ text-align: left;
+ border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ }
+
+ tr {
+ transition: transform 0.2s ease-in-out;
+ }
+
+ tr.dragging {
+ opacity: 0.5;
+ }
+
+ tr.hovered {
+ transform: translateY(10px); /* Adjust the value as needed for the animation effect */
+ }
+
+ table tr th {
+ padding: 0.5rem;
+ text-align: left;
+ border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+
+ &:nth-child(1) {
+ border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Table/index.js b/packages/bruno-app/src/components/Table/index.js
new file mode 100644
index 000000000..80bfb19f3
--- /dev/null
+++ b/packages/bruno-app/src/components/Table/index.js
@@ -0,0 +1,110 @@
+import { useState, useRef, useEffect, useCallback } from 'react';
+import StyledWrapper from './StyledWrapper';
+
+const Table = ({ minColumnWidth = 1, headers = [], children }) => {
+ const [activeColumnIndex, setActiveColumnIndex] = useState(null);
+ const tableRef = useRef(null);
+
+ const columns = headers?.map((item) => ({
+ ...item,
+ ref: useRef()
+ }));
+
+ const updateDivHeights = () => {
+ if (tableRef.current) {
+ const height = tableRef.current.offsetHeight;
+ columns.forEach((col) => {
+ if (col.ref.current) {
+ col.ref.current.querySelector('.resizer').style.height = `${height}px`;
+ }
+ });
+ }
+ };
+
+ useEffect(() => {
+ updateDivHeights();
+ window.addEventListener('resize', updateDivHeights);
+
+ return () => {
+ window.removeEventListener('resize', updateDivHeights);
+ };
+ }, [columns]);
+
+ useEffect(() => {
+ if (tableRef.current) {
+ const observer = new MutationObserver(updateDivHeights);
+ observer.observe(tableRef.current, { childList: true, subtree: true });
+
+ return () => {
+ observer.disconnect();
+ };
+ }
+ }, [columns]);
+
+ const handleMouseDown = (index) => (e) => {
+ setActiveColumnIndex(index);
+ };
+
+ const handleMouseMove = useCallback(
+ (e) => {
+ const gridColumns = columns.map((col, i) => {
+ if (i === activeColumnIndex) {
+ const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
+
+ if (width >= minColumnWidth) {
+ return `${width}px`;
+ }
+ }
+ return `${col.ref.current.offsetWidth}px`;
+ });
+
+ tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
+ },
+ [activeColumnIndex, columns, minColumnWidth]
+ );
+
+ const handleMouseUp = useCallback(() => {
+ setActiveColumnIndex(null);
+ removeListeners();
+ }, [removeListeners]);
+
+ const removeListeners = useCallback(() => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', removeListeners);
+ }, [handleMouseMove]);
+
+ useEffect(() => {
+ if (activeColumnIndex !== null) {
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+ }
+ return () => {
+ removeListeners();
+ };
+ }, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
+
+ return (
+
+
+
+
+
+ {columns.map(({ ref, name }, i) => (
+
+ {name}
+
+
+ ))}
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Table;
diff --git a/packages/bruno-app/src/components/ToolHint/StyledWrapper.js b/packages/bruno-app/src/components/ToolHint/StyledWrapper.js
new file mode 100644
index 000000000..8cbe85f38
--- /dev/null
+++ b/packages/bruno-app/src/components/ToolHint/StyledWrapper.js
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ background-color: ${(props) => props.theme.sidebar.badge};
+ color: ${(props) => props.theme.text};
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/ToolHint/index.js b/packages/bruno-app/src/components/ToolHint/index.js
new file mode 100644
index 000000000..5ca7af233
--- /dev/null
+++ b/packages/bruno-app/src/components/ToolHint/index.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { Tooltip as ReactToolHint } from 'react-tooltip';
+import StyledWrapper from './StyledWrapper';
+import { useTheme } from 'providers/Theme';
+
+const ToolHint = ({
+ text,
+ toolhintId,
+ children,
+ tooltipStyle = {},
+ place = 'top',
+ offset,
+ theme = null
+}) => {
+ const { theme: contextTheme } = useTheme();
+ const appliedTheme = theme || contextTheme;
+
+ const toolhintBackgroundColor = appliedTheme?.sidebar.badge.bg || 'black';
+ const toolhintTextColor = appliedTheme?.text || 'white';
+
+ const combinedToolhintStyle = {
+ ...tooltipStyle,
+ fontSize: '0.75rem',
+ padding: '0.25rem 0.5rem',
+ backgroundColor: toolhintBackgroundColor,
+ color: toolhintTextColor
+ };
+
+ return (
+ <>
+
{children}
+
+
+
+ >
+ );
+};
+
+export default ToolHint;
diff --git a/packages/bruno-app/src/components/VariablesEditor/index.js b/packages/bruno-app/src/components/VariablesEditor/index.js
index 980a8c5c3..08f415113 100644
--- a/packages/bruno-app/src/components/VariablesEditor/index.js
+++ b/packages/bruno-app/src/components/VariablesEditor/index.js
@@ -62,10 +62,10 @@ const EnvVariables = ({ collection, theme }) => {
);
};
-const CollectionVariables = ({ collection, theme }) => {
- const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
+const RuntimeVariables = ({ collection, theme }) => {
+ const runtimeVariablesFound = Object.keys(collection.runtimeVariables).length > 0;
- const collectionVariableArray = Object.entries(collection.collectionVariables).map(([name, value]) => ({
+ const runtimeVariableArray = Object.entries(collection.runtimeVariables).map(([name, value]) => ({
name,
value,
secret: false
@@ -73,11 +73,11 @@ const CollectionVariables = ({ collection, theme }) => {
return (
<>
-
Collection Variables
- {collectionVariablesFound ? (
-
+
Runtime Variables
+ {runtimeVariablesFound ? (
+
) : (
-
No collection variables found
+
No runtime variables found
)}
>
);
@@ -90,13 +90,13 @@ const VariablesEditor = ({ collection }) => {
return (
-
+
- Note: As of today, collection variables can only be set via the API -{' '}
- getVar() and setVar() .
- In the next release, we will add a UI to set and modify collection variables.
+ Note: As of today, runtime variables can only be set via the API - getVar() {' '}
+ and setVar() .
+ In the next release, we will add a UI to set and modify runtime variables.
);
diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js
index 385a71486..0ec4c1245 100644
--- a/packages/bruno-app/src/components/Welcome/index.js
+++ b/packages/bruno-app/src/components/Welcome/index.js
@@ -1,6 +1,7 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
+import { useTranslation } from 'react-i18next';
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
@@ -12,6 +13,7 @@ import StyledWrapper from './StyledWrapper';
const Welcome = () => {
const dispatch = useDispatch();
+ const { t } = useTranslation();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@@ -20,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
- (err) => console.log(err) && toast.error('An error occurred while opening the collection')
+ (err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))
);
};
@@ -38,12 +40,12 @@ const Welcome = () => {
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
- toast.success('Collection imported successfully');
+ toast.success(t('WELCOME.COLLECTION_IMPORT_SUCCESS'));
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
- toast.error('An error occurred while importing the collection. Check the logs for more information.');
+ toast.error(t('WELCOME.COLLECTION_IMPORT_ERROR'));
});
};
@@ -66,46 +68,45 @@ const Welcome = () => {
bruno
-
Opensource IDE for exploring and testing APIs
+
{t('WELCOME.ABOUT_BRUNO')}
-
Collections
+
{t('COMMON.COLLECTIONS')}
setCreateCollectionModalOpen(true)}>
- Create Collection
+ {t('WELCOME.CREATE_COLLECTION')}
- Open Collection
+ {t('WELCOME.OPEN_COLLECTION')}
setImportCollectionModalOpen(true)}>
- Import Collection
+ {t('WELCOME.IMPORT_COLLECTION')}
-
-
Links
+
{t('WELCOME.LINKS')}
diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js
index c2b167813..7839a55ac 100644
--- a/packages/bruno-app/src/globalStyles.js
+++ b/packages/bruno-app/src/globalStyles.js
@@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
}
}
+ input::placeholder {
+ color: ${(props) => props.theme.input.placeholder.color};
+ opacity: ${(props) => props.theme.input.placeholder.opacity};
+ }
+
@keyframes fade-in {
from {
opacity: 0;
@@ -168,7 +173,6 @@ const GlobalStyle = createGlobalStyle`
// (macos scrollbar styling is the ideal style reference)
@media not all and (pointer: coarse) {
* {
- scrollbar-width: thin;
scrollbar-color: ${(props) => props.theme.scrollbar.color};
}
diff --git a/packages/bruno-app/src/i18n/index.js b/packages/bruno-app/src/i18n/index.js
new file mode 100644
index 000000000..26e89695f
--- /dev/null
+++ b/packages/bruno-app/src/i18n/index.js
@@ -0,0 +1,24 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import translationEn from './translation/en.json';
+
+const resources = {
+ en: {
+ translation: translationEn,
+ },
+};
+
+i18n
+ .use(initReactI18next) // passes i18n down to react-i18next
+ .init({
+ resources,
+ lng: 'en', // Use "en" as the default language. "cimode" can be used to debug / show translation placeholder
+
+ ns: 'translation', // Use translation as the default Namespace that will be loaded by default
+
+ interpolation: {
+ escapeValue: false // react already safes from xss
+ }
+ });
+
+export default i18n;
diff --git a/packages/bruno-app/src/i18n/translation/en.json b/packages/bruno-app/src/i18n/translation/en.json
new file mode 100644
index 000000000..7dda41e42
--- /dev/null
+++ b/packages/bruno-app/src/i18n/translation/en.json
@@ -0,0 +1,20 @@
+{
+ "COMMON": {
+ "COLLECTIONS": "Collections",
+ "DOCUMENTATION": "Documentation",
+ "REPORT_ISSUES": "Report Issues",
+ "GITHUB": "GitHub",
+ "DISCORD": "Discord",
+ "TWITTER": "Twitter"
+ },
+ "WELCOME": {
+ "ABOUT_BRUNO": "Opensource IDE for exploring and testing APIs",
+ "LINKS": "Links",
+ "CREATE_COLLECTION": "Create Collection",
+ "OPEN_COLLECTION": "Open Collection",
+ "IMPORT_COLLECTION": "Import Collection",
+ "COLLECTION_IMPORT_SUCCESS": "Collection imported successfully",
+ "COLLECTION_IMPORT_ERROR": "An error occurred while importing the collection. Check the logs for more information.",
+ "COLLECTION_OPEN_ERROR": "An error occurred while opening the collection"
+ }
+}
diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js
index cf8b3683e..0f3f553d6 100644
--- a/packages/bruno-app/src/pages/_app.js
+++ b/packages/bruno-app/src/pages/_app.js
@@ -14,6 +14,15 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
+import '@fontsource/inter/100.css';
+import '@fontsource/inter/200.css';
+import '@fontsource/inter/300.css';
+import '@fontsource/inter/400.css';
+import '@fontsource/inter/500.css';
+import '@fontsource/inter/600.css';
+import '@fontsource/inter/700.css';
+import '@fontsource/inter/800.css';
+import '@fontsource/inter/900.css';
function SafeHydrate({ children }) {
return
{typeof window === 'undefined' ? null : children}
;
diff --git a/packages/bruno-app/src/pages/_document.js b/packages/bruno-app/src/pages/_document.js
index 131fc50dd..2152707dd 100644
--- a/packages/bruno-app/src/pages/_document.js
+++ b/packages/bruno-app/src/pages/_document.js
@@ -30,12 +30,7 @@ export default class MyDocument extends Document {
render() {
return (
-
-
-
+
diff --git a/packages/bruno-app/src/pages/index.js b/packages/bruno-app/src/pages/index.js
index de969b5a5..08a43dbd4 100644
--- a/packages/bruno-app/src/pages/index.js
+++ b/packages/bruno-app/src/pages/index.js
@@ -1,6 +1,7 @@
import Head from 'next/head';
import Bruno from './Bruno';
import GlobalStyle from '../globalStyles';
+import '../i18n';
export default function Home() {
return (
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js
index c54d53867..7664ae03e 100644
--- a/packages/bruno-app/src/providers/App/index.js
+++ b/packages/bruno-app/src/providers/App/index.js
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+import { get } from 'lodash';
import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
import ConfirmAppClose from './ConfirmAppClose';
@@ -18,6 +19,13 @@ export const AppProvider = (props) => {
dispatch(refreshScreenWidth());
}, []);
+ useEffect(() => {
+ const platform = get(navigator, 'platform', '');
+ if(platform && platform.toLowerCase().indexOf('mac') > -1) {
+ document.body.classList.add('os-mac');
+ }
+ }, []);
+
useEffect(() => {
const handleResize = () => {
dispatch(refreshScreenWidth());
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 467a8582c..f4a04030f 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -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]);
};
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index ff5c7ba5b..c82a342b2 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.20.0'
+ version: '1.28.0'
}
});
};
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index 8b0503b1c..1b28b891b 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -9,7 +9,7 @@ import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
-import { closeTabs } from 'providers/ReduxStore/slices/tabs';
+import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
export const HotkeysContext = React.createContext();
@@ -154,6 +154,65 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
+ // Switch to the previous tab
+ useEffect(() => {
+ Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
+ dispatch(
+ switchTab({
+ direction: 'pageup'
+ })
+ );
+
+ return false; // this stops the event bubbling
+ });
+
+ return () => {
+ Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
+ };
+ }, [dispatch]);
+
+ // Switch to the next tab
+ useEffect(() => {
+ Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
+ dispatch(
+ switchTab({
+ direction: 'pagedown'
+ })
+ );
+
+ return false; // this stops the event bubbling
+ });
+
+ return () => {
+ Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
+ };
+ }, [dispatch]);
+
+ // Close all tabs
+ useEffect(() => {
+ Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
+ const activeTab = find(tabs, (t) => t.uid === activeTabUid);
+ if (activeTab) {
+ const collection = findCollectionByUid(collections, activeTab.collectionUid);
+
+ if (collection) {
+ const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
+ dispatch(
+ closeTabs({
+ tabUids: tabUids
+ })
+ );
+ }
+ }
+
+ return false; // this stops the event bubbling
+ });
+
+ return () => {
+ Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
+ };
+ }, [activeTabUid, tabs, collections, dispatch]);
+
return (
{showSaveRequestModal && (
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
index 3cd276880..f5034b5d5 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
@@ -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) => {
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 483456a91..054b4fbd4 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -33,13 +33,14 @@ import {
requestCancelled,
resetRunResults,
responseReceived,
- updateLastAction
+ updateLastAction,
+ setCollectionSecurityConfig
} from './index';
import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
-import { parseQueryParams, splitOnFirst } from 'utils/url/index';
+import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
@@ -192,10 +193,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
- const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
- const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
-
- _sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
+ _sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@@ -224,7 +222,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
- sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.collectionVariables)
+ sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
.then((response) => {
return dispatch(
responseReceived({
@@ -284,7 +282,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
};
-export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
+export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -314,8 +312,9 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
folder,
collectionCopy,
environment,
- collectionCopy.collectionVariables,
- recursive
+ collectionCopy.runtimeVariables,
+ recursive,
+ delay
)
.then(resolve)
.catch((err) => {
@@ -375,6 +374,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
});
};
+// rename item
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -700,7 +700,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
- const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
+ const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => {
const state = getState();
@@ -710,11 +710,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
const parts = splitOnFirst(requestUrl, '?');
- const params = parseQueryParams(parts[1]);
- each(params, (urlParam) => {
+ const queryParams = parseQueryParams(parts[1]);
+ each(queryParams, (urlParam) => {
urlParam.enabled = true;
+ urlParam.type = 'query';
});
+ const pathParams = parsePathParams(requestUrl);
+ each(pathParams, (pathParm) => {
+ pathParams.enabled = true;
+ pathParm.type = 'path';
+ });
+
+ const params = [...queryParams, ...pathParams];
+
const item = {
uid: uuid(),
type: requestType,
@@ -732,6 +741,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
sparql: null,
multipartForm: null,
formUrlEncoded: null
+ },
+ auth: auth ?? {
+ mode: 'none'
}
}
};
@@ -1036,16 +1048,18 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
name: brunoConfig.name,
pathname: pathname,
items: [],
- collectionVariables: {},
+ runtimeVariables: {},
brunoConfig: brunoConfig
};
return new Promise((resolve, reject) => {
- collectionSchema
- .validate(collection)
- .then(() => dispatch(_createCollection(collection)))
- .then(resolve)
- .catch(reject);
+ ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
+ collectionSchema
+ .validate(collection)
+ .then(() => dispatch(_createCollection({ ...collection, securityConfig })))
+ .then(resolve)
+ .catch(reject);
+ });
});
};
@@ -1110,3 +1124,19 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject);
});
};
+
+export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+
+ ipcRenderer
+ .invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)
+ .then(async () => {
+ await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));
+ resolve();
+ })
+ .catch(reject);
+ });
+};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 7493c947b..7ba2ac34c 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -33,7 +33,6 @@ export const collectionsSlice = createSlice({
const collection = action.payload;
collection.settingsSelectedTab = 'headers';
-
collection.folderLevelSettingsSelectedTab = {};
// TODO: move this to use the nextAction approach
@@ -51,6 +50,12 @@ export const collectionsSlice = createSlice({
state.collections.push(collection);
}
},
+ setCollectionSecurityConfig: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ if (collection) {
+ collection.securityConfig = action.payload.securityConfig;
+ }
+ },
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -200,7 +205,7 @@ export const collectionsSlice = createSlice({
}
},
scriptEnvironmentUpdateEvent: (state, action) => {
- const { collectionUid, envVariables, collectionVariables } = action.payload;
+ const { collectionUid, envVariables, runtimeVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
@@ -230,7 +235,7 @@ export const collectionsSlice = createSlice({
});
}
- collection.collectionVariables = collectionVariables;
+ collection.runtimeVariables = runtimeVariables;
}
},
processEnvUpdateEvent: (state, action) => {
@@ -498,6 +503,39 @@ export const collectionsSlice = createSlice({
}
}
},
+
+ moveQueryParam: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ // Ensure item.draft is a deep clone of item if not already present
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ // Extract payload data
+ const { updateReorderedItem } = action.payload;
+ const params = item.draft.request.params;
+
+ item.draft.request.params = updateReorderedItem.map((uid) => {
+ return params.find((param) => param.uid === uid);
+ });
+
+ // update request url
+ const parts = splitOnFirst(item.draft.request.url, '?');
+ const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
+ if (query && query.length) {
+ item.draft.request.url = parts[0] + '?' + query;
+ } else {
+ item.draft.request.url = parts[0];
+ }
+ }
+ }
+ },
+
updateQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -717,7 +755,7 @@ export const collectionsSlice = createSlice({
uid: uuid(),
type: action.payload.type,
name: '',
- value: '',
+ value: action.payload.value,
description: '',
contentType: '',
enabled: true
@@ -1181,7 +1219,6 @@ export const collectionsSlice = createSlice({
uid: uuid(),
name: '',
value: '',
- type: 'request',
enabled: true
});
set(folder, 'root.request.vars.req', vars);
@@ -1191,7 +1228,6 @@ export const collectionsSlice = createSlice({
uid: uuid(),
name: '',
value: '',
- type: 'response',
enabled: true
});
set(folder, 'root.request.vars.res', vars);
@@ -1301,6 +1337,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;
@@ -1314,7 +1415,7 @@ export const collectionsSlice = createSlice({
}
if (isFolderRoot) {
- const folderPath = path.dirname(file.meta.pathname);
+ const folderPath = getDirectoryName(file.meta.pathname);
const folderItem = findItemInCollectionByPathname(collection, folderPath);
if (folderItem) {
folderItem.root = file.data;
@@ -1570,29 +1671,29 @@ export const collectionsSlice = createSlice({
}
if (type === 'request-sent') {
- const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.status = 'running';
item.requestSent = action.payload.requestSent;
}
if (type === 'response-received') {
- const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.status = 'completed';
item.responseReceived = action.payload.responseReceived;
}
if (type === 'test-results') {
- const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.testResults = action.payload.testResults;
}
if (type === 'assertion-results') {
- const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.assertionResults = action.payload.assertionResults;
}
if (type === 'error') {
- const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.error = action.payload.error;
item.responseReceived = action.payload.responseReceived;
item.status = 'error';
@@ -1626,6 +1727,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
+ setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
removeCollection,
@@ -1653,6 +1755,7 @@ export const {
requestUrlChanged,
updateAuth,
addQueryParam,
+ moveQueryParam,
updateQueryParam,
deleteQueryParam,
updatePathParam,
@@ -1692,6 +1795,9 @@ export const {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
+ addCollectionVar,
+ updateCollectionVar,
+ deleteCollectionVar,
updateCollectionAuthMode,
updateCollectionAuth,
updateCollectionRequestScript,
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
index b64a71fad..935be6075 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
@@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
return;
}
- if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
+ if (
+ ['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
+ ) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;
@@ -39,13 +41,33 @@ export const tabsSlice = createSlice({
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response',
type: action.payload.type || 'request',
- ...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
+ ...(action.payload.uid ? { folderUid: action.payload.uid } : {})
});
state.activeTabUid = action.payload.uid;
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
},
+ switchTab: (state, action) => {
+ if (!state.tabs || !state.tabs.length) {
+ state.activeTabUid = null;
+ return;
+ }
+
+ const direction = action.payload.direction;
+
+ const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);
+
+ let toBeActivatedTabIndex = 0;
+
+ if (direction == 'pageup') {
+ toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;
+ } else if (direction == 'pagedown') {
+ toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;
+ }
+
+ state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;
+ },
updateRequestPaneTabWidth: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
@@ -109,6 +131,7 @@ export const tabsSlice = createSlice({
export const {
addTab,
focusTab,
+ switchTab,
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,
diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css
index 29e9196ea..8396c48b5 100644
--- a/packages/bruno-app/src/styles/globals.css
+++ b/packages/bruno-app/src/styles/globals.css
@@ -58,6 +58,15 @@ body::-webkit-scrollbar-thumb,
border-radius: 5rem;
}
+/*
+ * Mac-specific scrollbar styling
+ * This ensures that scrollbars are only visible when the user starts to scroll,
+ * providing a cleaner and more minimalistic appearance.
+ */
+body.os-mac * {
+ scrollbar-width: thin;
+}
+
/*
* todo: this will be supported in the future to be changed via applying a theme
* making all the checkboxes and radios bigger
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index bb1001f31..9e8e923aa 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -20,7 +20,11 @@ const darkTheme = {
input: {
bg: 'rgb(65, 65, 65)',
border: 'rgb(65, 65, 65)',
- focusBorder: 'rgb(65, 65, 65)'
+ focusBorder: 'rgb(65, 65, 65)',
+ placeholder: {
+ color: '#a2a2a2',
+ opacity: 0.75
+ }
},
variables: {
@@ -154,7 +158,7 @@ const darkTheme = {
modal: {
title: {
color: '#ccc',
- bg: 'rgb(48, 48, 49)',
+ bg: 'rgb(38, 38, 39)',
iconColor: '#ccc'
},
body: {
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index a130f2513..a25583136 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -20,7 +20,11 @@ const lightTheme = {
input: {
bg: 'white',
border: '#ccc',
- focusBorder: '#8b8b8b'
+ focusBorder: '#8b8b8b',
+ placeholder: {
+ color: '#a2a2a2',
+ opacity: 0.8
+ }
},
menubar: {
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index 0e3476256..fa0738503 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -2,10 +2,16 @@ const createContentType = (mode) => {
switch (mode) {
case 'json':
return 'application/json';
+ case 'text':
+ return 'text/plain';
case 'xml':
return 'application/xml';
+ case 'sparql':
+ return 'application/sparql-query';
case 'formUrlEncoded':
return 'application/x-www-form-urlencoded';
+ case 'graphql':
+ return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
default:
@@ -13,13 +19,19 @@ const createContentType = (mode) => {
}
};
-const createHeaders = (headers) => {
- return headers
+const createHeaders = (request, headers) => {
+ const enabledHeaders = headers
.filter((header) => header.enabled)
.map((header) => ({
name: header.name,
value: header.value
}));
+
+ const contentType = createContentType(request.body?.mode);
+ if (contentType !== '') {
+ enabledHeaders.push({ name: 'content-type', value: contentType });
+ }
+ return enabledHeaders;
};
const createQuery = (queryParams = []) => {
@@ -38,7 +50,11 @@ const createPostData = (body) => {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
- .map((param) => ({ name: param.name, value: param.value }))
+ .map((param) => ({
+ name: param.name,
+ value: param.value,
+ ...(param.type === 'file' && { fileName: param.value })
+ }))
};
} else {
return {
@@ -54,7 +70,7 @@ export const buildHarRequest = ({ request, headers }) => {
url: encodeURI(request.url),
httpVersion: 'HTTP/1.1',
cookies: [],
- headers: createHeaders(headers),
+ headers: createHeaders(request, headers),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,
diff --git a/packages/bruno-app/src/utils/codemirror/autocompleteConstants.js b/packages/bruno-app/src/utils/codemirror/autocompleteConstants.js
new file mode 100644
index 000000000..f0c8a9aa2
--- /dev/null
+++ b/packages/bruno-app/src/utils/codemirror/autocompleteConstants.js
@@ -0,0 +1,56 @@
+export const MimeTypes = [
+ 'application/atom+xml',
+ 'application/ecmascript',
+ 'application/json',
+ 'application/vnd.api+json',
+ 'application/javascript',
+ 'application/octet-stream',
+ 'application/ogg',
+ 'application/pdf',
+ 'application/postscript',
+ 'application/rdf+xml',
+ 'application/rss+xml',
+ 'application/soap+xml',
+ 'application/font-woff',
+ 'application/x-yaml',
+ 'application/xhtml+xml',
+ 'application/xml',
+ 'application/xml-dtd',
+ 'application/xop+xml',
+ 'application/zip',
+ 'application/gzip',
+ 'application/graphql',
+ 'application/x-www-form-urlencoded',
+ 'audio/basic',
+ 'audio/L24',
+ 'audio/mp4',
+ 'audio/mpeg',
+ 'audio/ogg',
+ 'audio/vorbis',
+ 'audio/vnd.rn-realaudio',
+ 'audio/vnd.wave',
+ 'audio/webm',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/pjpeg',
+ 'image/png',
+ 'image/svg+xml',
+ 'image/tiff',
+ 'message/http',
+ 'message/imdn+xml',
+ 'message/partial',
+ 'message/rfc822',
+ 'multipart/mixed',
+ 'multipart/alternative',
+ 'multipart/related',
+ 'multipart/form-data',
+ 'multipart/signed',
+ 'multipart/encrypted',
+ 'text/cmd',
+ 'text/css',
+ 'text/csv',
+ 'text/html',
+ 'text/plain',
+ 'text/vcard',
+ 'text/xml'
+];
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
index 1632aa43a..eb6a0cc7a 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
@@ -31,8 +31,8 @@ if (!SERVER_RENDERED) {
if (str.startsWith('{{')) {
variableName = str.replace('{{', '').replace('}}', '').trim();
variableValue = interpolate(get(options.variables, variableName), options.variables);
- } else if (str.startsWith(':')) {
- variableName = str.replace(':', '').trim();
+ } else if (str.startsWith('/:')) {
+ variableName = str.replace('/:', '').trim();
variableValue =
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
}
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 6145ee200..d872a6685 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import path from 'path';
+import slash from 'utils/common/slash';
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if (!str || !str.length || !isString(str)) {
@@ -98,7 +99,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
};
export const findItemByPathname = (items = [], pathname) => {
- return find(items, (i) => i.pathname === pathname);
+ return find(items, (i) => slash(i.pathname) === slash(pathname));
};
export const findItemInCollectionByPathname = (collection, pathname) => {
@@ -380,6 +381,55 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
}
}
+ if (si.type == 'folder' && si?.root) {
+ di.root = {
+ request: {}
+ };
+
+ let { request, meta } = si?.root || {};
+ let { headers, script = {}, vars = {}, tests } = request || {};
+
+ // folder level headers
+ if (headers?.length) {
+ di.root.request.headers = headers;
+ }
+ // folder level script
+ if (Object.keys(script)?.length) {
+ di.root.request.script = {};
+ if (script?.req?.length) {
+ di.root.request.script.req = script?.req;
+ }
+ if (script?.res?.length) {
+ di.root.request.script.res = script?.res;
+ }
+ }
+ // folder level vars
+ if (Object.keys(vars)?.length) {
+ di.root.request.vars = {};
+ if (vars?.req?.length) {
+ di.root.request.vars.req = vars?.req;
+ }
+ if (vars?.res?.length) {
+ di.root.request.vars.res = vars?.res;
+ }
+ }
+ // folder level tests
+ if (tests?.length) {
+ di.root.request.tests = tests;
+ }
+
+ if (meta?.name) {
+ di.root.meta = {};
+ di.root.meta.name = meta?.name;
+ }
+ if (!Object.keys(di.root.request)?.length) {
+ delete di.root.request;
+ }
+ if (!Object.keys(di.root)?.length) {
+ delete di.root;
+ }
+ }
+
if (si.type === 'js') {
di.fileContent = si.raw;
}
@@ -403,6 +453,60 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
collectionToSave.environments = collection.environments || [];
+ collectionToSave.root = {
+ request: {}
+ };
+
+ let { request, docs, meta } = collection?.root || {};
+ let { auth, headers, script = {}, vars = {}, tests } = request || {};
+
+ // collection level auth
+ if (auth?.mode) {
+ collectionToSave.root.request.auth = auth;
+ }
+ // collection level headers
+ if (headers?.length) {
+ collectionToSave.root.request.headers = headers;
+ }
+ // collection level script
+ if (Object.keys(script)?.length) {
+ collectionToSave.root.request.script = {};
+ if (script?.req?.length) {
+ collectionToSave.root.request.script.req = script?.req;
+ }
+ if (script?.res?.length) {
+ collectionToSave.root.request.script.res = script?.res;
+ }
+ }
+ // collection level vars
+ if (Object.keys(vars)?.length) {
+ collectionToSave.root.request.vars = {};
+ if (vars?.req?.length) {
+ collectionToSave.root.request.vars.req = vars?.req;
+ }
+ if (vars?.res?.length) {
+ collectionToSave.root.request.vars.res = vars?.res;
+ }
+ }
+ // collection level tests
+ if (tests?.length) {
+ collectionToSave.root.request.tests = tests;
+ }
+ // collection level docs
+ if (docs?.length) {
+ collectionToSave.root.docs = docs;
+ }
+ if (meta?.name) {
+ collectionToSave.root.meta = {};
+ collectionToSave.root.meta.name = meta?.name;
+ }
+ if (!Object.keys(collectionToSave.root.request)?.length) {
+ delete collectionToSave.root.request;
+ }
+ if (!Object.keys(collectionToSave.root)?.length) {
+ delete collectionToSave.root;
+ }
+
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
// delete proxy password if present
@@ -683,18 +787,25 @@ export const getTotalRequestCountInCollection = (collection) => {
};
export const getAllVariables = (collection, item) => {
- const environmentVariables = getEnvironmentVariables(collection);
+ 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,
- ...collection.collectionVariables,
+ ...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
+ ...requestVariables,
+ ...runtimeVariables,
pathParams: {
...pathParams
},
process: {
env: {
- ...collection.processEnvVariables
+ ...processEnvVariables
}
}
};
@@ -710,3 +821,47 @@ export const maskInputValue = (value) => {
.map(() => '*')
.join('');
};
+
+const getTreePathFromCollectionToItem = (collection, _item) => {
+ let path = [];
+ let item = findItemInCollection(collection, _item?.uid);
+ while (item) {
+ path.unshift(item);
+ item = findParentItemInCollection(collection, item?.uid);
+ }
+ return path;
+};
+
+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) {
+ folderVariables[_var.name] = _var.value;
+ }
+ });
+ } else {
+ let vars = get(i, 'request.vars.req', []);
+ vars.forEach((_var) => {
+ if (_var.enabled) {
+ requestVariables[_var.name] = _var.value;
+ }
+ });
+ }
+ }
+ return {
+ collectionVariables,
+ folderVariables,
+ requestVariables
+ };
+};
diff --git a/packages/bruno-app/src/utils/collections/search.js b/packages/bruno-app/src/utils/collections/search.js
index b420687b7..9c2f187e5 100644
--- a/packages/bruno-app/src/utils/collections/search.js
+++ b/packages/bruno-app/src/utils/collections/search.js
@@ -3,7 +3,7 @@ import filter from 'lodash/filter';
import find from 'lodash/find';
export const doesRequestMatchSearchText = (request, searchText = '') => {
- return request.name.toLowerCase().includes(searchText.toLowerCase());
+ return request?.name?.toLowerCase().includes(searchText.toLowerCase());
};
export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {
diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js
index ecd197428..cbb1a2b3a 100644
--- a/packages/bruno-app/src/utils/common/codemirror.js
+++ b/packages/bruno-app/src/utils/common/codemirror.js
@@ -12,8 +12,67 @@ const pathFoundInVariables = (path, obj) => {
return value !== undefined;
};
-export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
- CodeMirror.defineMode('combinedmode', function (config, parserConfig) {
+/**
+ * Changes the render behaviour for a given CodeMirror editor.
+ * Replaces all **rendered** characters, not the actual value, with the provided character.
+ */
+export class MaskedEditor {
+ /**
+ * @param {import('codemirror').Editor} editor CodeMirror editor instance
+ * @param {string} maskChar Target character being applied to all content
+ */
+ constructor(editor, maskChar) {
+ this.editor = editor;
+ this.maskChar = maskChar;
+ this.enabled = false;
+ }
+
+ /**
+ * Set and apply new masking character
+ */
+ enable = () => {
+ this.enabled = true;
+ this.editor.setValue(this.editor.getValue());
+ this.editor.on('inputRead', this.maskContent);
+ this.update();
+ };
+
+ /** Disables masking of the editor field. */
+ disable = () => {
+ this.enabled = false;
+ this.editor.off('inputRead', this.maskContent);
+ this.editor.setValue(this.editor.getValue());
+ };
+
+ /** Updates the rendered content if enabled. */
+ update = () => {
+ if (this.enabled) this.maskContent();
+ };
+
+ /** Replaces all rendered characters, with the provided character. */
+ maskContent = () => {
+ const content = this.editor.getValue();
+ this.editor.operation(() => {
+ // Clear previous masked text
+ this.editor.getAllMarks().forEach((mark) => mark.clear());
+ // Apply new masked text
+ for (let i = 0; i < content.length; i++) {
+ if (content[i] !== '\n') {
+ const maskedNode = document.createTextNode(this.maskChar);
+ this.editor.markText(
+ { line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
+ { line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
+ { replacedWith: maskedNode, handleMouseEvents: true }
+ );
+ }
+ }
+ });
+ };
+}
+
+export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
+ CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
+ const { pathParams = {}, ...variables } = _variables || {};
const variablesOverlay = {
token: function (stream) {
if (stream.match('{{', true)) {
@@ -37,13 +96,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
const urlPathParamsOverlay = {
token: function (stream) {
- if (stream.match(':', true)) {
+ if (stream.match('/:', true)) {
let ch;
let word = '';
while ((ch = stream.next()) != null) {
if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {
stream.backUp(1);
- const found = pathFoundInVariables(word, variables?.pathParams);
+ const found = pathFoundInVariables(word, pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
@@ -53,21 +112,24 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
// If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.
if (word) {
- const found = pathFoundInVariables(word, variables?.pathParams);
+ const found = pathFoundInVariables(word, pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
}
}
- stream.skipTo(':') || stream.skipToEnd();
+ stream.skipTo('/:') || stream.skipToEnd();
return null;
}
};
- return CodeMirror.overlayMode(
- CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay),
- urlPathParamsOverlay
- );
+ let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
+
+ if (highlightPathParams) {
+ return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
+ } else {
+ return baseMode;
+ }
});
};
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index f31dd228f..05f1bad2f 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -149,7 +149,9 @@ export const relativeDate = (dateString) => {
};
export const humanizeDate = (dateString) => {
- const date = new Date(dateString);
+ // See this discussion for why .split is necessary
+ // https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off
+ const date = new Date(dateString.split('-'));
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
diff --git a/packages/bruno-app/src/utils/common/index.spec.js b/packages/bruno-app/src/utils/common/index.spec.js
index 3484fac9c..c4055c5d1 100644
--- a/packages/bruno-app/src/utils/common/index.spec.js
+++ b/packages/bruno-app/src/utils/common/index.spec.js
@@ -1,6 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
-import { normalizeFileName, startsWith } from './index';
+import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
describe('common utils', () => {
describe('normalizeFileName', () => {
@@ -49,4 +49,50 @@ describe('common utils', () => {
expect(startsWith('foo', 'foo')).toBe(true);
});
});
+
+ describe('humanizeDate', () => {
+ it('should return a date string in the en-US locale', () => {
+ expect(humanizeDate('2024-03-17')).toBe('March 17, 2024');
+ });
+
+ it('should return invalid date if the date is invalid', () => {
+ expect(humanizeDate('9999-99-99')).toBe('Invalid Date');
+ });
+ });
+
+ describe('relativeDate', () => {
+ it('should return few seconds ago', () => {
+ expect(relativeDate(new Date())).toBe('Few seconds ago');
+ });
+
+ it('should return minutes ago', () => {
+ let date = new Date();
+ date.setMinutes(date.getMinutes() - 30);
+ expect(relativeDate(date)).toBe('30 minutes ago');
+ });
+
+ it('should return hours ago', () => {
+ let date = new Date();
+ date.setHours(date.getHours() - 10);
+ expect(relativeDate(date)).toBe('10 hours ago');
+ });
+
+ it('should return days ago', () => {
+ let date = new Date();
+ date.setDate(date.getDate() - 5);
+ expect(relativeDate(date)).toBe('5 days ago');
+ });
+
+ it('should return weeks ago', () => {
+ let date = new Date();
+ date.setDate(date.getDate() - 8);
+ expect(relativeDate(date)).toBe('1 week ago');
+ });
+
+ it('should return months ago', () => {
+ let date = new Date();
+ date.setDate(date.getDate() - 60);
+ expect(relativeDate(date)).toBe('2 months ago');
+ });
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js
index 82eb0be95..e76f4014a 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.js
@@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => {
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
}
- requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
+ requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
@@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
- const splitAuth = request.auth.split(':');
- const user = splitAuth[0] || '';
- const password = splitAuth[1] || '';
-
- requestJson.auth = {
- user: repr(user),
- password: repr(password)
- };
+ if(request.auth.mode === 'basic'){
+ requestJson.auth = {
+ mode: 'basic',
+ basic: {
+ username: repr(request.auth.basic?.username),
+ password: repr(request.auth.basic?.password)
+ }
+ }
+ }
}
return Object.keys(requestJson).length ? requestJson : {};
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
index 2704bd4c5..2d9785154 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
@@ -75,4 +75,15 @@ describe('curlToJson', () => {
}
});
});
+
+ it('should return and parse a simple curl command with a trailing slash', () => {
+ const curlCommand = 'curl https://www.usebruno.com/';
+ const result = curlToJson(curlCommand);
+
+ expect(result).toEqual({
+ url: 'https://www.usebruno.com/',
+ raw_url: 'https://www.usebruno.com/',
+ method: 'get'
+ });
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js
index 97bfbd966..e16dc68a5 100644
--- a/packages/bruno-app/src/utils/curl/index.js
+++ b/packages/bruno-app/src/utils/curl/index.js
@@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
url: request.url,
method: request.method,
body,
- headers: headers
+ headers: headers,
+ auth: request.auth
};
} catch (error) {
console.error(error);
diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js
index 77426da83..79db23672 100644
--- a/packages/bruno-app/src/utils/curl/parse-curl.js
+++ b/packages/bruno-app/src/utils/curl/parse-curl.js
@@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => {
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
alias: {
H: 'header',
- A: 'user-agent'
+ A: 'user-agent',
+ u: 'user'
}
});
@@ -72,11 +73,10 @@ const parseCurlCommand = (curlCommand) => {
parsedArguments.header.forEach((header) => {
if (header.indexOf('Cookie') !== -1) {
cookieString = header;
- } else {
- const components = header.split(/:(.*)/);
- if (components[1]) {
- headers[components[0]] = components[1].trim();
- }
+ }
+ const components = header.split(/:(.*)/);
+ if (components[1]) {
+ headers[components[0]] = components[1].trim();
}
});
}
@@ -119,15 +119,16 @@ const parseCurlCommand = (curlCommand) => {
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
}
let method;
- if (parsedArguments.X === 'POST') {
+ let parsedMethodArgument = parsedArguments.X || parsedArguments.request || parsedArguments.T;
+ if (parsedMethodArgument === 'POST') {
method = 'post';
- } else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
+ } else if (parsedMethodArgument === 'PUT') {
method = 'put';
- } else if (parsedArguments.X === 'PATCH') {
+ } else if (parsedMethodArgument === 'PATCH') {
method = 'patch';
- } else if (parsedArguments.X === 'DELETE') {
+ } else if (parsedMethodArgument === 'DELETE') {
method = 'delete';
- } else if (parsedArguments.X === 'OPTIONS') {
+ } else if (parsedMethodArgument === 'OPTIONS') {
method = 'options';
} else if (
(parsedArguments.d ||
@@ -187,10 +188,21 @@ const parseCurlCommand = (curlCommand) => {
}
urlObject.search = null; // Clean out the search/query portion.
+
+ let urlWithoutQuery = URL.format(urlObject);
+ let urlHost = urlObject?.host;
+ if (!url?.includes(`${urlHost}/`)) {
+ if (urlWithoutQuery && urlHost) {
+ const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost);
+ urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1);
+ }
+ }
+
const request = {
- url: url,
- urlWithoutQuery: URL.format(urlObject)
+ url,
+ urlWithoutQuery
};
+
if (compressed) {
request.compressed = true;
}
@@ -226,12 +238,19 @@ const parseCurlCommand = (curlCommand) => {
request.data = parsedArguments['data-urlencode'];
}
- if (parsedArguments.u) {
- request.auth = parsedArguments.u;
- }
- if (parsedArguments.user) {
- request.auth = parsedArguments.user;
+ if (parsedArguments.user && typeof parsedArguments.user === 'string') {
+ const basicAuth = parsedArguments.user.split(':')
+ const username = basicAuth[0] || ''
+ const password = basicAuth[1] || ''
+ request.auth = {
+ mode: 'basic',
+ basic: {
+ username,
+ password
+ }
+ }
}
+
if (Array.isArray(request.data)) {
request.dataArray = request.data;
request.data = request.data.join('&');
diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js
index 719391f0a..7260942f4 100644
--- a/packages/bruno-app/src/utils/exporters/postman-collection.js
+++ b/packages/bruno-app/src/utils/exporters/postman-collection.js
@@ -12,6 +12,7 @@ export const exportCollection = (collection) => {
const generateInfoSection = () => {
return {
name: collection.name,
+ description: collection.root?.docs,
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
};
};
@@ -137,6 +138,11 @@ export const exportCollection = (collection) => {
}
}
};
+ case 'graphql':
+ return {
+ mode: 'graphql',
+ graphql: body.graphql
+ };
}
};
@@ -201,6 +207,8 @@ export const exportCollection = (collection) => {
const requestObject = {
method: itemRequest.method,
header: generateHeaders(itemRequest.headers),
+ auth: generateAuth(itemRequest.auth),
+ description: itemRequest.docs,
url: {
raw: itemRequest.url,
host: generateHost(itemRequest.url),
diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js
index 01fb66c01..eb2944cbf 100644
--- a/packages/bruno-app/src/utils/importers/openapi-collection.js
+++ b/packages/bruno-app/src/utils/importers/openapi-collection.js
@@ -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) => {
@@ -41,9 +40,12 @@ const buildEmptyJsonBody = (bodySchema) => {
each(bodySchema.properties || {}, (prop, name) => {
if (prop.type === 'object') {
_jsonBody[name] = buildEmptyJsonBody(prop);
- // handle arrays
} else if (prop.type === 'array') {
- _jsonBody[name] = [];
+ if (prop.items && prop.items.type === 'object') {
+ _jsonBody[name] = [buildEmptyJsonBody(prop.items)];
+ } else {
+ _jsonBody[name] = [];
+ }
} else {
_jsonBody[name] = '';
}
@@ -59,12 +61,15 @@ const transformOpenapiRequestItem = (request) => {
operationName = `${request.method} ${request.path}`;
}
+ // replace OpenAPI links in path by Bruno variables
+ let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
+
const brunoRequestItem = {
uid: uuid(),
name: operationName,
type: 'http-request',
request: {
- url: ensureUrl(request.global.server + '/' + request.path),
+ url: ensureUrl(request.global.server + path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
@@ -81,6 +86,9 @@ const transformOpenapiRequestItem = (request) => {
xml: null,
formUrlEncoded: [],
multipartForm: []
+ },
+ script: {
+ res: null
}
}
};
@@ -159,6 +167,9 @@ const transformOpenapiRequestItem = (request) => {
let _jsonBody = buildEmptyJsonBody(bodySchema);
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
}
+ if (bodySchema && bodySchema.type === 'array') {
+ brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
+ }
} else if (mimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
if (bodySchema && bodySchema.type === 'object') {
@@ -195,10 +206,30 @@ const transformOpenapiRequestItem = (request) => {
}
}
+ // build the extraction scripts from responses that have links
+ // https://swagger.io/docs/specification/links/
+ let script = [];
+ each(_operationObject.responses || [], (response, responseStatus) => {
+ if (Object.hasOwn(response, 'links')) {
+ // only extract if the status code matches the response
+ script.push(`if (res.status === ${responseStatus}) {`);
+ each(response.links, (link) => {
+ each(link.parameters || [], (expression, parameter) => {
+ let value = openAPIRuntimeExpressionToScript(expression);
+ script.push(` bru.setVar('${link.operationId}_${parameter}', ${value});`);
+ });
+ });
+ script.push(`}`);
+ }
+ });
+ if (script.length > 0) {
+ brunoRequestItem.request.script.res = script.join('\n');
+ }
+
return brunoRequestItem;
};
-const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
+const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
@@ -222,7 +253,7 @@ const resolveRefs = (spec, components = spec.components, visitedItems = new Set(
let ref = components;
for (const key of refKeys) {
- if (ref[key]) {
+ if (ref && ref[key]) {
ref = ref[key];
} else {
// Handle invalid references gracefully?
@@ -280,7 +311,7 @@ const getDefaultUrl = (serverObject) => {
url = url.replace(`{${variableName}}`, sub);
});
}
- return url;
+ return url.endsWith('/') ? url : `${url}/`;
};
const getSecurity = (apiSpec) => {
@@ -305,6 +336,18 @@ const getSecurity = (apiSpec) => {
};
};
+const openAPIRuntimeExpressionToScript = (expression) => {
+ // see https://swagger.io/docs/specification/links/#runtime-expressions
+ if (expression === '$response.body') {
+ return 'res.body';
+ } else if (expression.startsWith('$response.body#')) {
+ let pointer = expression.substring(15);
+ // could use https://www.npmjs.com/package/json-pointer for better support
+ return `res.body${pointer.replace('/', '.')}`;
+ }
+ return expression;
+};
+
const parseOpenApiCollection = (data) => {
const brunoCollection = {
name: '',
@@ -348,7 +391,7 @@ const parseOpenApiCollection = (data) => {
.map(([method, operationObject]) => {
return {
method: method,
- path: path,
+ path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
operationObject: operationObject,
global: {
server: baseUrl,
diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js
index 0683a3bbc..3f10aea9c 100644
--- a/packages/bruno-app/src/utils/importers/postman-collection.js
+++ b/packages/bruno-app/src/utils/importers/postman-collection.js
@@ -113,7 +113,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
xml: null,
formUrlEncoded: [],
multipartForm: []
- }
+ },
+ docs: i.request.description
}
};
/* struct of translation log
diff --git a/packages/bruno-app/src/utils/importers/postman-environment.js b/packages/bruno-app/src/utils/importers/postman-environment.js
index 61c62311c..38f818f71 100644
--- a/packages/bruno-app/src/utils/importers/postman-environment.js
+++ b/packages/bruno-app/src/utils/importers/postman-environment.js
@@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
const importEnvironment = () => {
return new Promise((resolve, reject) => {
- fileDialog({ accept: 'application/json' })
- .then(readFile)
- .then(parsePostmanEnvironment)
- .then((environment) => resolve(environment))
+ fileDialog({ multiple: true, accept: 'application/json' })
+ .then((files) => {
+ return Promise.all(
+ Object.values(files ?? {}).map((file) =>
+ readFile([file])
+ .then(parsePostmanEnvironment)
+ .catch((err) => {
+ console.error(`Error processing file: ${file.name || 'undefined'}`, err);
+ throw err;
+ })
+ )
+ );
+ })
+ .then((environments) => resolve(environments))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import Environment failed'));
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index e76a7debd..18a2b8a1c 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -1,9 +1,9 @@
import { safeStringifyJSON } from 'utils/common';
-export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
+export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
- sendHttpRequest(item, collection, environment, collectionVariables)
+ sendHttpRequest(item, collection, environment, runtimeVariables)
.then((response) => {
resolve({
state: 'success',
@@ -22,22 +22,22 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
});
};
-const sendHttpRequest = async (item, collection, environment, collectionVariables) => {
+const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
- .invoke('send-http-request', item, collection, environment, collectionVariables)
+ .invoke('send-http-request', item, collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
});
};
-export const sendCollectionOauth2Request = async (collection, environment, collectionVariables) => {
+export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
- .invoke('send-collection-oauth2-request', collection, environment, collectionVariables)
+ .invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
});
diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js
index 0b9f2201a..f9557b3c4 100644
--- a/packages/bruno-app/src/utils/url/index.js
+++ b/packages/bruno-app/src/utils/url/index.js
@@ -18,16 +18,17 @@ const hasLength = (str) => {
};
export const parseQueryParams = (query) => {
- if (!query || !query.length) {
+ try {
+ if (!query || !query.length) {
+ return [];
+ }
+
+ return Array.from(new URLSearchParams(query.split('#')[0]).entries())
+ .map(([name, value]) => ({ name, value }));
+ } catch (error) {
+ console.error('Error parsing query params:', error);
return [];
}
-
- let params = query.split('&').map((param) => {
- let [name, value = ''] = param.split('=');
- return { name, value };
- });
-
- return filter(params, (p) => hasLength(p.name));
};
export const parsePathParams = (url) => {
@@ -44,7 +45,8 @@ export const parsePathParams = (url) => {
try {
uri = new URL(uri);
} catch (e) {
- throw e;
+ // URL is non-parsable, is it incomplete? Ignore.
+ return [];
}
let paths = uri.pathname.split('/');
@@ -107,14 +109,14 @@ export const isValidUrl = (url) => {
}
};
-export const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
+export const interpolateUrl = ({ url, envVars, runtimeVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return;
}
return interpolate(url, {
...envVars,
- ...collectionVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js
index 2fd3f0815..0645befee 100644
--- a/packages/bruno-app/src/utils/url/index.spec.js
+++ b/packages/bruno-app/src/utils/url/index.spec.js
@@ -49,6 +49,23 @@ describe('Url Utils - parseQueryParams', () => {
{ name: 'b', value: '2' }
]);
});
+
+ it('should parse query with "=" character - case 9', () => {
+ const params = parseQueryParams('a=1&b={color=red,size=large}&c=3');
+ expect(params).toEqual([
+ { name: 'a', value: '1' },
+ { name: 'b', value: '{color=red,size=large}' },
+ { name: 'c', value: '3' }
+ ]);
+ });
+
+ it('should parse query with fragment - case 10', () => {
+ const params = parseQueryParams('a=1&b=2#I-AM-FRAGMENT');
+ expect(params).toEqual([
+ { name: 'a', value: '1' },
+ { name: 'b', value: '2' }
+ ]);
+ });
});
describe('Url Utils - parsePathParams', () => {
@@ -129,10 +146,10 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value';
const envVars = { host: 'https://example.com', foo: 'foo_value' };
- const collectionVariables = { bar: 'bar_value' };
+ const runtimeVariables = { bar: 'bar_value' };
const processEnvVars = { baz: 'baz_value' };
- const result = interpolateUrl({ url, envVars, collectionVariables, processEnvVars });
+ const result = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
expect(result).toEqual(expectedUrl);
});
@@ -153,10 +170,10 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value';
const envVars = { host: 'https://example.com', foo: 'foo_value' };
- const collectionVariables = { bar: 'bar_value' };
+ const runtimeVariables = { bar: 'bar_value' };
const processEnvVars = { baz: 'baz_value' };
- const intermediateResult = interpolateUrl({ url, envVars, collectionVariables, processEnvVars });
+ const intermediateResult = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
const result = interpolateUrlPathParams(intermediateResult, params);
expect(result).toEqual(expectedUrl);
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index c8e3b72ce..67c39de79 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -14,7 +14,7 @@
"url": "git+https://github.com/usebruno/bruno.git"
},
"scripts": {
- "test": "jest"
+ "test": "node --experimental-vm-modules $(npx which jest)"
},
"files": [
"src",
@@ -40,7 +40,6 @@
"inquirer": "^9.1.4",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
- "mustache": "^4.2.0",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"vm2": "^3.9.13",
diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md
index 5cc98f3c0..ec82267fb 100644
--- a/packages/bruno-cli/readme.md
+++ b/packages/bruno-cli/readme.md
@@ -64,13 +64,13 @@ Bruno cli returns the following exit status codes:
- `1` -- an assertion, test, or request in the executed collection failed
- `2` -- the specified output directory does not exist
- `3` -- the request chain seems to loop endlessly
-- `4` -- bru was called outside of a colection root directory
+- `4` -- bru was called outside of a collection root directory
- `5` -- the specified input file does not exist
- `6` -- the specified environment does not exist
- `7` -- the environment override was not a string or object
- `8` -- an environment override is malformed
- `9` -- an invalid output format was requested
-- `255` -- another error occured
+- `255` -- another error occurred
## Demo
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index e51daa552..53871bc57 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -190,6 +190,10 @@ const getFolderRoot = (dir) => {
return collectionBruToJson(content);
};
+const getJsSandboxRuntime = (sandbox) => {
+ return sandbox === 'safe' ? 'quickjs' : 'vm2';
+};
+
const builder = async (yargs) => {
yargs
.option('r', {
@@ -215,6 +219,11 @@ const builder = async (yargs) => {
describe: 'Overwrite a single environment variable, multiple usages possible',
type: 'string'
})
+ .option('sandbox', {
+ describe: 'Javscript sandbox to use; available sandboxes are "developer" (default) or "safe"',
+ default: 'developer',
+ type: 'string'
+ })
.option('output', {
alias: 'o',
describe: 'Path to write file results to',
@@ -232,7 +241,7 @@ const builder = async (yargs) => {
})
.option('tests-only', {
type: 'boolean',
- description: 'Only run requests that have a test'
+ description: 'Only run requests that have a test or active assertion'
})
.option('bail', {
type: 'boolean',
@@ -282,6 +291,7 @@ const handler = async function (argv) {
r: recursive,
output: outputPath,
format,
+ sandbox,
testsOnly,
bail
} = argv;
@@ -312,7 +322,7 @@ const handler = async function (argv) {
recursive = true;
}
- const collectionVariables = {};
+ const runtimeVariables = {};
let envVars = {};
if (env) {
@@ -417,7 +427,7 @@ const handler = async function (argv) {
if (!recursive) {
console.log(chalk.yellow('Running Folder \n'));
const files = fs.readdirSync(filename);
- const bruFiles = files.filter((file) => file.endsWith('.bru'));
+ const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru'));
for (const bruFile of bruFiles) {
const bruFilepath = path.join(filename, bruFile);
@@ -451,6 +461,7 @@ const handler = async function (argv) {
}
}
+ const runtime = getJsSandboxRuntime(sandbox);
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
@@ -462,11 +473,12 @@ const handler = async function (argv) {
bruFilepath,
bruJson,
collectionPath,
- collectionVariables,
+ runtimeVariables,
envVars,
processEnvVars,
brunoConfig,
- collectionRoot
+ collectionRoot,
+ runtime
);
results.push({
diff --git a/packages/bruno-cli/src/runner/interpolate-string.js b/packages/bruno-cli/src/runner/interpolate-string.js
index 052041670..e210be339 100644
--- a/packages/bruno-cli/src/runner/interpolate-string.js
+++ b/packages/bruno-cli/src/runner/interpolate-string.js
@@ -1,13 +1,13 @@
const { forOwn, cloneDeep } = require('lodash');
const { interpolate } = require('@usebruno/common');
-const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
+const interpolateString = (str, { envVars, runtimeVariables, processEnvVars }) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
processEnvVars = processEnvVars || {};
- collectionVariables = collectionVariables || {};
+ runtimeVariables = runtimeVariables || {};
// we clone envVars because we don't want to modify the original object
envVars = envVars ? cloneDeep(envVars) : {};
@@ -24,10 +24,10 @@ const interpolateString = (str, { envVars, collectionVariables, processEnvVars }
});
});
- // collectionVariables take precedence over envVars
+ // runtimeVariables take precedence over envVars
const combinedVars = {
...envVars,
- ...collectionVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index 886974c0f..c35456993 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -12,7 +12,7 @@ const getContentType = (headers = {}) => {
return contentType;
};
-const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
+const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
@@ -33,10 +33,10 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
return str;
}
- // collectionVariables take precedence over envVars
+ // runtimeVariables take precedence over envVars
const combinedVars = {
...envVars,
- ...collectionVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
@@ -82,11 +82,11 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.data = _interpolate(request.data);
}
- each(request.params, (param) => {
+ each(request?.pathParams, (param) => {
param.value = _interpolate(param.value);
});
- if (request?.params?.length) {
+ if (request?.pathParams?.length) {
let url = request.url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
@@ -107,7 +107,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
return '/' + path;
} else {
const name = path.slice(1);
- const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name);
+ const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name);
return existingPathParam ? '/' + existingPathParam.value : '';
}
})
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 5e34087df..30cfa4bd0 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -60,7 +60,8 @@ const prepareRequest = (request, collectionRoot) => {
let axiosRequest = {
method: request.method,
url: request.url,
- headers: headers
+ headers: headers,
+ pathParams: request?.params?.filter((param) => param.type === 'path')
};
const collectionAuth = get(collectionRoot, 'request.auth');
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index 33e8216c3..b260f6be9 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -21,15 +21,20 @@ const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'
const path = require('path');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
+const onConsoleLog = (type, args) => {
+ console[type](...args);
+};
+
const runSingleRequest = async function (
filename,
bruJson,
collectionPath,
- collectionVariables,
+ runtimeVariables,
envVariables,
processEnvVars,
brunoConfig,
- collectionRoot
+ collectionRoot,
+ runtime
) {
try {
let request;
@@ -38,6 +43,7 @@ const runSingleRequest = async function (
request = prepareRequest(bruJson.request, collectionRoot);
const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = runtime;
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
@@ -54,34 +60,20 @@ 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();
- varsRuntime.runPreRequestVars(
- preRequestVars,
- request,
- envVariables,
- collectionVariables,
- collectionPath,
- processEnvVars
- );
- }
-
// run pre request script
const requestScriptFile = compact([
get(collectionRoot, 'request.script.req'),
get(bruJson, 'request.script.req')
]).join(os.EOL);
if (requestScriptFile?.length) {
- const scriptRuntime = new ScriptRuntime();
+ const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
const result = await scriptRuntime.runRequestScript(
decomment(requestScriptFile),
request,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
- null,
+ onConsoleLog,
processEnvVars,
scriptingConfig
);
@@ -91,7 +83,7 @@ const runSingleRequest = async function (
}
// interpolate variables inside request
- interpolateVars(request, envVariables, collectionVariables, processEnvVars);
+ interpolateVars(request, envVariables, runtimeVariables, processEnvVars);
if (!protocolRegex.test(request.url)) {
request.url = `http://${request.url}`;
@@ -120,7 +112,7 @@ const runSingleRequest = async function (
const interpolationOptions = {
envVars: envVariables,
- collectionVariables,
+ runtimeVariables,
processEnvVars
};
@@ -270,19 +262,19 @@ 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
const postResponseVars = get(bruJson, 'request.vars.res');
if (postResponseVars?.length) {
- const varsRuntime = new VarsRuntime();
+ const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPostResponseVars(
postResponseVars,
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
processEnvVars
);
@@ -294,13 +286,13 @@ const runSingleRequest = async function (
get(bruJson, 'request.script.res')
]).join(os.EOL);
if (responseScriptFile?.length) {
- const scriptRuntime = new ScriptRuntime();
+ const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
const result = await scriptRuntime.runResponseScript(
decomment(responseScriptFile),
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
null,
processEnvVars,
@@ -315,13 +307,13 @@ const runSingleRequest = async function (
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
if (assertions) {
- const assertRuntime = new AssertRuntime();
+ const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
assertionResults = assertRuntime.runAssertions(
assertions,
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
processEnvVars
);
@@ -339,19 +331,23 @@ const runSingleRequest = async function (
let testResults = [];
const testFile = compact([get(collectionRoot, 'request.tests'), get(bruJson, 'request.tests')]).join(os.EOL);
if (typeof testFile === 'string') {
- const testRuntime = new TestRuntime();
+ const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const result = await testRuntime.runTests(
decomment(testFile),
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
null,
processEnvVars,
scriptingConfig
);
testResults = get(result, 'results', []);
+
+ if (result?.nextRequestName !== undefined) {
+ nextRequestName = result.nextRequestName;
+ }
}
if (testResults?.length) {
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index 262cca650..75707fef9 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -1,19 +1,12 @@
const _ = require('lodash');
-const Mustache = require('mustache');
const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
const collectionBruToJson = (bru) => {
try {
const json = _collectionBruToJson(bru);
const transformedJson = {
request: {
- params: _.get(json, 'params', []),
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
script: _.get(json, 'script', {}),
@@ -96,7 +89,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
_.each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json
index cc25f2337..1964f5ec4 100644
--- a/packages/bruno-common/package.json
+++ b/packages/bruno-common/package.json
@@ -22,13 +22,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
- "rollup": "3.2.5",
+ "rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
- "rollup": "3.2.5"
+ "rollup":"3.29.4"
}
}
diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts
index a1994d473..adfdf54cd 100644
--- a/packages/bruno-common/src/interpolate/index.spec.ts
+++ b/packages/bruno-common/src/interpolate/index.spec.ts
@@ -294,7 +294,7 @@ describe('interpolate - recursive', () => {
const result = interpolate(inputString, inputObject);
- expect(result).toBe('{{recursion2}}');
+ expect(result).toBe('{{recursion3}}');
});
it('should replace repetead placeholders with 1 level of recursion with values from the object', () => {
@@ -335,4 +335,22 @@ describe('interpolate - recursive', () => {
expect(result).toBe(new Array(24).fill('repetead4').join(' '));
});
+
+ it('should replace mutiple interdependent variables in the same input string', () => {
+ const inputString = `{
+ "x": "{{v2}} {{v1}}"
+ }`;
+ const inputObject = {
+ foo: 'bar',
+ v1: '{{foo}}',
+ v2: '{{bar}}',
+ bar: 'baz'
+ };
+
+ const result = interpolate(inputString, inputObject);
+
+ expect(result).toBe(`{
+ "x": "baz bar"
+ }`);
+ });
});
diff --git a/packages/bruno-common/src/interpolate/index.ts b/packages/bruno-common/src/interpolate/index.ts
index d4bd7cb6b..4a4092d88 100644
--- a/packages/bruno-common/src/interpolate/index.ts
+++ b/packages/bruno-common/src/interpolate/index.ts
@@ -27,32 +27,41 @@ const interpolate = (str: string, obj: Record): string => {
const replace = (
str: string,
flattenedObj: Record,
- visited = new Set(),
+ visited = new Set(),
results = new Map()
): string => {
- const patternRegex = /\{\{([^}]+)\}\}/g;
+ let resultStr = str;
+ let matchFound = true;
- return str.replace(patternRegex, (match, placeholder) => {
- const replacement = flattenedObj[placeholder];
+ while (matchFound) {
+ const patternRegex = /\{\{([^}]+)\}\}/g;
+ matchFound = false;
+ resultStr = resultStr.replace(patternRegex, (match, placeholder) => {
+ const replacement = flattenedObj[placeholder];
- if (results.has(match)) {
- return results.get(match);
- }
+ if (results.has(match)) {
+ return results.get(match);
+ }
+
+ if (patternRegex.test(replacement) && !visited.has(match)) {
+ visited.add(match);
+ const result = replace(replacement, flattenedObj, visited, results);
+ results.set(match, result);
+
+ matchFound = true;
+ return result;
+ }
- if (patternRegex.test(replacement) && !visited.has(match)) {
visited.add(match);
- const result = replace(replacement, flattenedObj, visited, results);
+ const result = replacement !== undefined ? replacement : match;
results.set(match, result);
+ matchFound = true;
return result;
- }
+ });
+ }
- visited.add(match);
- const result = replacement !== undefined ? replacement : match;
- results.set(match, result);
-
- return result;
- });
+ return resultStr;
};
export default interpolate;
diff --git a/packages/bruno-electron/electron-builder-config.js b/packages/bruno-electron/electron-builder-config.js
index 1b75e4d13..81e50a804 100644
--- a/packages/bruno-electron/electron-builder-config.js
+++ b/packages/bruno-electron/electron-builder-config.js
@@ -3,7 +3,7 @@ require('dotenv').config({ path: process.env.DOTENV_PATH });
const config = {
appId: 'com.usebruno.app',
productName: 'Bruno',
- electronVersion: '21.1.1',
+ electronVersion: '31.2.1',
directories: {
buildResources: 'resources',
output: 'out'
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index dda9a2ce7..2a0e9390b 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v1.20.0",
+ "version": "v1.28.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -16,13 +16,17 @@
"dist:rpm": "electron-builder --linux rpm --config electron-builder-config.js",
"dist:snap": "electron-builder --linux snap --config electron-builder-config.js",
"pack": "electron-builder --dir",
- "test": "jest"
+ "test": "node --experimental-vm-modules $(npx which jest)"
+ },
+ "jest": {
+ "modulePaths": ["node_modules"]
},
"dependencies": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
+ "@usebruno/node-machine-id": "^2.0.0",
"@usebruno/schema": "0.7.0",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
@@ -47,9 +51,7 @@
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
- "mustache": "^4.2.0",
"nanoid": "3.3.4",
- "node-machine-id": "^1.1.12",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
@@ -61,8 +63,7 @@
"dmg-license": "^1.0.11"
},
"devDependencies": {
- "electron": "21.1.1",
- "electron-builder": "23.0.2",
- "electron-icon-maker": "^0.0.5"
+ "electron": "31.2.1",
+ "electron-builder": "23.0.2"
}
}
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 2fbd4cc98..589cd29d8 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -428,17 +428,16 @@ class Watcher {
this.watchers = {};
}
- addWatcher(win, watchPath, collectionUid, brunoConfig) {
+ addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
const ignores = brunoConfig?.ignore || [];
- const self = this;
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
- usePolling: watchPath.startsWith('\\\\') ? true : false,
+ usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
ignored: (filepath) => {
const normalizedPath = filepath.replace(/\\/g, '/');
const relativePath = path.relative(watchPath, normalizedPath);
@@ -457,14 +456,36 @@ class Watcher {
depth: 20
});
+ let startedNewWatcher = false;
watcher
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
- .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath));
+ .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
+ .on('error', (error) => {
+ // `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627
+ // `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached.
+ // To prevent loops `!forcePolling` is checked.
+ if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {
+ // This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and
+ // Multiple watcher being started `startedNewWatcher` is set to prevent this.
+ startedNewWatcher = true;
+ watcher.close();
+ console.error(
+ `\nCould not start watcher for ${watchPath}:`,
+ 'ENOSPC: System limit for number of file watchers reached!',
+ 'Trying again with polling, this will be slower!\n',
+ 'Update you system config to allow more concurrently watched files with:',
+ '"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
+ );
+ this.addWatcher(win, watchPath, collectionUid, brunoConfig, true);
+ } else {
+ console.error(`An error occurred in the watcher for: ${watchPath}`, error);
+ }
+ });
- self.watchers[watchPath] = watcher;
+ this.watchers[watchPath] = watcher;
}, 100);
}
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 07041b93b..7fe43218a 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -14,7 +14,6 @@ const collectionBruToJson = (bru) => {
const transformedJson = {
request: {
- params: _.get(json, 'params', []),
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
script: _.get(json, 'script', {}),
@@ -29,8 +28,7 @@ const collectionBruToJson = (bru) => {
// in the future, all of this will be replaced by standard bru lang
if (json.meta) {
transformedJson.meta = {
- name: json.meta.name,
- seq: json.meta.seq
+ name: json.meta.name
};
}
@@ -40,12 +38,10 @@ const collectionBruToJson = (bru) => {
}
};
-const jsonToCollectionBru = (json) => {
+const jsonToCollectionBru = (json, isFolder) => {
try {
const collectionBruJson = {
- params: _.get(json, 'request.params', []),
headers: _.get(json, 'request.headers', []),
- auth: _.get(json, 'request.auth', {}),
script: {
req: _.get(json, 'request.script.req', ''),
res: _.get(json, 'request.script.res', '')
@@ -61,13 +57,16 @@ const jsonToCollectionBru = (json) => {
// add meta if it exists
// this is only for folder bru file
// in the future, all of this will be replaced by standard bru lang
- if (json.meta) {
+ if (json?.meta) {
collectionBruJson.meta = {
- name: json.meta.name,
- seq: json.meta.seq
+ name: json.meta.name
};
}
+ if (!isFolder) {
+ collectionBruJson.auth = _.get(json, 'request.auth', {});
+ }
+
return _jsonToCollectionBru(collectionBruJson);
} catch (error) {
return Promise.reject(error);
@@ -123,7 +122,6 @@ const bruToJson = (bru) => {
}
const sequence = _.get(json, 'meta.seq');
-
const transformedJson = {
type: requestType,
name: _.get(json, 'meta.name'),
@@ -170,11 +168,12 @@ const jsonToBru = (json) => {
type = 'http';
}
+ const sequence = _.get(json, 'seq');
const bruJson = {
meta: {
name: _.get(json, 'name'),
type: type,
- seq: _.get(json, 'seq')
+ seq: !isNaN(sequence) ? Number(sequence) : 1
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 7f4e58422..db5deecae 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -1,5 +1,15 @@
+const fs = require('fs');
const path = require('path');
const isDev = require('electron-is-dev');
+
+if (isDev) {
+ if(!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {
+ console.log('JS Sandbox libraries have not been bundled yet');
+ console.log('Please run the below command \nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');
+ throw new Error('JS Sandbox libraries have not been bundled yet');
+ }
+}
+
const { format } = require('url');
const { BrowserWindow, app, Menu, ipcMain } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
@@ -50,6 +60,7 @@ app.on('ready', async () => {
height,
minWidth: 1000,
minHeight: 640,
+ show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
@@ -67,6 +78,9 @@ app.on('ready', async () => {
mainWindow.maximize();
}
+ mainWindow.once('ready-to-show', () => {
+ mainWindow.show();
+ });
const url = isDev
? 'http://localhost:3000'
: format({
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 029129308..945c21559 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -13,15 +13,19 @@ const {
browseFiles,
createDirectory,
searchForBruFiles,
- sanitizeDirectoryName
+ sanitizeDirectoryName,
+ isWSLPath,
+ normalizeWslPath,
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
+const CollectionSecurityStore = require('../store/collection-security');
const environmentSecretsStore = new EnvironmentSecretsStore();
+const collectionSecurityStore = new CollectionSecurityStore();
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -161,7 +165,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
name: folderName
};
- const content = jsonToCollectionBru(folderRoot);
+ const content = jsonToCollectionBru(
+ folderRoot,
+ true // isFolder
+ );
await writeFile(folderBruFilePath, content);
} catch (error) {
return Promise.reject(error);
@@ -321,6 +328,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
try {
+ // Normalize paths if they are WSL paths
+ if (isWSLPath(oldPath)) {
+ oldPath = normalizeWslPath(oldPath);
+ }
+ if (isWSLPath(newPath)) {
+ newPath = normalizeWslPath(newPath);
+ }
+
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
}
@@ -439,6 +454,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath);
+ if (item?.root?.meta?.name) {
+ const folderBruFilePath = path.join(folderPath, 'folder.bru');
+ const folderContent = jsonToCollectionBru(
+ item.root,
+ true // isFolder
+ );
+ fs.writeFileSync(folderBruFilePath, folderContent);
+ }
+
if (item.items && item.items.length) {
parseCollectionItems(item.items, folderPath);
}
@@ -488,6 +512,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// Write the Bruno configuration to a file
await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);
+ const collectionContent = jsonToCollectionBru(collection.root);
+ await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);
+
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);
@@ -519,6 +546,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath);
+ // If folder has a root element, then I should write its folder.bru file
+ if (item.root) {
+ const folderContent = jsonToCollectionBru(item.root, true);
+ if (folderContent) {
+ const bruFolderPath = path.join(folderPath, `folder.bru`);
+ fs.writeFileSync(bruFolderPath, folderContent);
+ }
+ }
+
if (item.items && item.items.length) {
parseCollectionItems(item.items, folderPath);
}
@@ -528,6 +564,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(collectionPath);
+ // If initial folder has a root element, then I should write its folder.bru file
+ if (itemFolder.root) {
+ const folderContent = jsonToCollectionBru(itemFolder.root, true);
+ if (folderContent) {
+ const bruFolderPath = path.join(collectionPath, `folder.bru`);
+ fs.writeFileSync(bruFolderPath, folderContent);
+ }
+ }
+
// create folder and files based on another folder
await parseCollectionItems(itemFolder.items, collectionPath);
} catch (error) {
@@ -632,6 +677,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {
+ try {
+ collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {
+ jsSandboxMode: securityConfig.jsSandboxMode
+ });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:get-collection-security-config', async (event, collectionPath) => {
+ try {
+ return collectionSecurityStore.getSecurityConfigForCollection(collectionPath);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js
index dcc57a07e..a292973fe 100644
--- a/packages/bruno-electron/src/ipc/network/axios-instance.js
+++ b/packages/bruno-electron/src/ipc/network/axios-instance.js
@@ -49,7 +49,22 @@ const checkConnection = (host, port) =>
*/
function makeAxiosInstance() {
/** @type {axios.AxiosInstance} */
- const instance = axios.create();
+ const instance = axios.create({
+ transformRequest: function transformRequest(data, headers) {
+ // doesn't apply the default transformRequest if the data is a string, so that axios doesn't add quotes see :
+ // https://github.com/usebruno/bruno/issues/2043
+ // https://github.com/axios/axios/issues/4034
+ const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
+ const hasJSONContentType = contentType.includes('json');
+ if (typeof data === 'string' && hasJSONContentType) {
+ return data;
+ }
+
+ axios.defaults.transformRequest.forEach((tr) => data = tr(data, headers));
+ return data;
+ },
+ proxy: false
+ });
instance.interceptors.request.use(async (config) => {
const url = URL.parse(config.url);
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index fa4979678..768505e27 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -6,7 +6,6 @@ const tls = require('tls');
const axios = require('axios');
const path = require('path');
const decomment = require('decomment');
-const Mustache = require('mustache');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
@@ -29,7 +28,7 @@ const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
-const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
+const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
@@ -39,11 +38,6 @@ const {
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
const safeStringifyJSON = (data) => {
try {
return JSON.stringify(data);
@@ -71,7 +65,7 @@ const getEnvVars = (environment = {}) => {
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
- envVars[variable.name] = Mustache.escape(variable.value);
+ envVars[variable.name] = variable.value;
}
});
@@ -81,13 +75,34 @@ const getEnvVars = (environment = {}) => {
};
};
+const getJsSandboxRuntime = (collection) => {
+ const securityConfig = get(collection, 'securityConfig', {});
+ return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
+};
+
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,
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
collectionPath
) => {
@@ -118,7 +133,7 @@ const configureRequest = async (
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars
};
@@ -160,58 +175,99 @@ 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) {
let requestCopy = cloneDeep(request);
switch (request?.oauth2?.grantType) {
case 'authorization_code':
- interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
+ interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
request.method = 'POST';
@@ -220,7 +276,7 @@ const configureRequest = async (
request.url = authorizationCodeAccessTokenUrl;
break;
case 'client_credentials':
- interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
+ interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
await transformClientCredentialsRequest(requestCopy);
request.method = 'POST';
@@ -229,7 +285,7 @@ const configureRequest = async (
request.url = clientCredentialsAccessTokenUrl;
break;
case 'password':
- interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
+ interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
requestCopy
);
@@ -267,7 +323,7 @@ const configureRequest = async (
return axiosInstance;
};
-const parseDataFromResponse = (response) => {
+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
@@ -285,8 +341,10 @@ const parseDataFromResponse = (response) => {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
- data = JSON.parse(data);
- } catch {}
+ if (!disableParsingResponseJson) {
+ data = JSON.parse(data);
+ }
+ } catch { }
return { data, dataBuffer };
};
@@ -308,34 +366,20 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
) => {
- // run pre-request vars
- const preRequestVars = get(request, 'vars.req', []);
- if (preRequestVars?.length) {
- const varsRuntime = new VarsRuntime();
- varsRuntime.runPreRequestVars(
- preRequestVars,
- request,
- envVars,
- collectionVariables,
- collectionPath,
- processEnvVars
- );
- }
-
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
if (requestScript?.length) {
- const scriptRuntime = new ScriptRuntime();
+ const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runRequestScript(
decomment(requestScript),
request,
envVars,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
@@ -344,14 +388,14 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: scriptResult.envVariables,
- collectionVariables: scriptResult.collectionVariables,
+ runtimeVariables: scriptResult.runtimeVariables,
requestUid,
collectionUid
});
}
// interpolate variables inside request
- interpolateVars(request, envVars, collectionVariables, processEnvVars);
+ interpolateVars(request, envVars, runtimeVariables, processEnvVars);
// if this is a graphql request, parse the variables, only after interpolation
// https://github.com/usebruno/bruno/issues/884
@@ -375,20 +419,20 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
) => {
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) {
- const varsRuntime = new VarsRuntime();
+ const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
const result = varsRuntime.runPostResponseVars(
postResponseVars,
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
collectionPath,
processEnvVars
);
@@ -396,7 +440,7 @@ const registerNetworkIpc = (mainWindow) => {
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
- collectionVariables: result.collectionVariables,
+ runtimeVariables: result.runtimeVariables,
requestUid,
collectionUid
});
@@ -408,18 +452,21 @@ 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);
+
let scriptResult;
- const responseScript = compact([get(request, 'script.res'), get(collectionRoot, 'request.script.res')]).join(
- os.EOL
- );
if (responseScript?.length) {
- const scriptRuntime = new ScriptRuntime();
+ const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runResponseScript(
decomment(responseScript),
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
@@ -428,7 +475,7 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: scriptResult.envVariables,
- collectionVariables: scriptResult.collectionVariables,
+ runtimeVariables: scriptResult.runtimeVariables,
requestUid,
collectionUid
});
@@ -437,7 +484,7 @@ const registerNetworkIpc = (mainWindow) => {
};
// handler for sending http request
- ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
+ ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const cancelTokenUid = uuid();
@@ -457,6 +504,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = getJsSandboxRuntime(collection);
try {
const controller = new AbortController();
@@ -470,7 +518,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -479,7 +527,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid,
request,
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
collectionPath
);
@@ -531,24 +579,14 @@ const registerNetworkIpc = (mainWindow) => {
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
- const { data, dataBuffer } = parseDataFromResponse(response);
+ const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = responseTime;
// 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
@@ -564,7 +602,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -572,13 +610,13 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(request, 'assertions');
if (assertions) {
- const assertRuntime = new AssertRuntime();
+ const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
const results = assertRuntime.runAssertions(
assertions,
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars
);
@@ -592,18 +630,21 @@ const registerNetworkIpc = (mainWindow) => {
}
// run tests
- const testFile = compact([
- item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'),
- get(collectionRoot, 'request.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);
+
if (typeof testFile === 'string') {
- const testRuntime = new TestRuntime();
+ const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(
decomment(testFile),
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
@@ -620,7 +661,7 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: testResults.envVariables,
- collectionVariables: testResults.collectionVariables,
+ runtimeVariables: testResults.runtimeVariables,
requestUid,
collectionUid
});
@@ -642,7 +683,7 @@ const registerNetworkIpc = (mainWindow) => {
}
});
- ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, collectionVariables) => {
+ ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
try {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
@@ -655,6 +696,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
@@ -663,17 +705,17 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
- interpolateVars(request, envVars, collection.collectionVariables, processEnvVars);
+ interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
const axiosInstance = await configureRequest(
collection.uid,
request,
envVars,
- collection.collectionVariables,
+ collection.runtimeVariables,
processEnvVars,
collectionPath
);
@@ -688,7 +730,7 @@ const registerNetworkIpc = (mainWindow) => {
}
}
- const { data } = parseDataFromResponse(response);
+ const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
@@ -699,7 +741,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -756,10 +798,11 @@ const registerNetworkIpc = (mainWindow) => {
const requestUid = uuid();
const collectionPath = collection.pathname;
const collectionUid = collection.uid;
- const collectionVariables = collection.collectionVariables;
+ const runtimeVariables = collection.runtimeVariables;
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collection.uid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
@@ -768,17 +811,17 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
- interpolateVars(request, envVars, collection.collectionVariables, processEnvVars);
+ interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
const axiosInstance = await configureRequest(
collection.uid,
request,
envVars,
- collection.collectionVariables,
+ collection.runtimeVariables,
processEnvVars,
collectionPath
);
@@ -792,7 +835,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -819,13 +862,14 @@ const registerNetworkIpc = (mainWindow) => {
ipcMain.handle(
'renderer:run-collection-folder',
- async (event, folder, collection, environment, collectionVariables, recursive) => {
+ async (event, folder, collection, environment, runtimeVariables, recursive, delay) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const folderUid = folder ? folder.uid : null;
const cancelTokenUid = uuid();
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
+ scriptingConfig.runtime = getJsSandboxRuntime(collection);
const collectionRoot = get(collection, 'root', {});
const abortController = new AbortController();
@@ -902,7 +946,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -930,7 +974,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid,
request,
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
collectionPath
);
@@ -938,14 +982,36 @@ const registerNetworkIpc = (mainWindow) => {
timeStart = Date.now();
let response, responseTime;
try {
+ if (delay && !Number.isNaN(delay) && delay > 0) {
+ const delayPromise = new Promise((resolve) => setTimeout(resolve, delay));
+
+ const cancellationPromise = new Promise((_, reject) => {
+ abortController.signal.addEventListener('abort', () => {
+ reject(new Error('Cancelled'));
+ });
+ });
+
+ await Promise.race([delayPromise, cancellationPromise]);
+ }
+
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
timeEnd = Date.now();
- const { data, dataBuffer } = parseDataFromResponse(response);
+ const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
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: {
@@ -998,7 +1064,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
collectionRoot,
collectionUid,
- collectionVariables,
+ runtimeVariables,
processEnvVars,
scriptingConfig
);
@@ -1010,13 +1076,13 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(item, 'request.assertions');
if (assertions) {
- const assertRuntime = new AssertRuntime();
+ const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
const results = assertRuntime.runAssertions(
assertions,
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
processEnvVars
);
@@ -1029,24 +1095,31 @@ const registerNetworkIpc = (mainWindow) => {
}
// run tests
- const testFile = compact([
- get(collectionRoot, 'request.tests'),
- item.draft ? get(item.draft, 'request.tests') : get(item, 'request.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);
+
if (typeof testFile === 'string') {
- const testRuntime = new TestRuntime();
+ const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(
decomment(testFile),
request,
response,
envVars,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig
);
+ if (testResults?.nextRequestName !== undefined) {
+ nextRequestName = testResults.nextRequestName;
+ }
+
mainWindow.webContents.send('main:run-folder-event', {
type: 'test-results',
testResults: testResults.results,
@@ -1055,7 +1128,7 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: testResults.envVariables,
- collectionVariables: testResults.collectionVariables,
+ runtimeVariables: testResults.runtimeVariables,
collectionUid
});
}
@@ -1124,7 +1197,7 @@ const registerNetworkIpc = (mainWindow) => {
try {
const disposition = contentDispositionParser.parse(contentDisposition);
return disposition && disposition.parameters['filename'];
- } catch (error) {}
+ } catch (error) { }
};
const getFileNameFromUrlPath = () => {
@@ -1140,12 +1213,28 @@ const registerNetworkIpc = (mainWindow) => {
return `response.${extension}`;
};
- const fileName =
- getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader();
+ const getEncodingFormat = () => {
+ const contentType = getHeaderValue('content-type');
+ const extension = mime.extension(contentType) || 'txt';
+ return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64';
+ };
+ const determineFileName = () => {
+ return (
+ getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader()
+ );
+ };
+
+ const fileName = determineFileName();
const filePath = await chooseFileToSave(mainWindow, fileName);
if (filePath) {
- await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64'));
+ const encoding = getEncodingFormat();
+ const data = Buffer.from(response.dataBuffer, 'base64')
+ if (encoding === 'utf-8') {
+ await writeFile(filePath, data);
+ } else {
+ await writeBinaryFile(filePath, data);
+ }
}
} catch (error) {
return Promise.reject(error);
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-string.js b/packages/bruno-electron/src/ipc/network/interpolate-string.js
index 052041670..e210be339 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-string.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-string.js
@@ -1,13 +1,13 @@
const { forOwn, cloneDeep } = require('lodash');
const { interpolate } = require('@usebruno/common');
-const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
+const interpolateString = (str, { envVars, runtimeVariables, processEnvVars }) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
processEnvVars = processEnvVars || {};
- collectionVariables = collectionVariables || {};
+ runtimeVariables = runtimeVariables || {};
// we clone envVars because we don't want to modify the original object
envVars = envVars ? cloneDeep(envVars) : {};
@@ -24,10 +24,10 @@ const interpolateString = (str, { envVars, collectionVariables, processEnvVars }
});
});
- // collectionVariables take precedence over envVars
+ // runtimeVariables take precedence over envVars
const combinedVars = {
...envVars,
- ...collectionVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index f2f27f93e..b6aeaa078 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -12,15 +12,17 @@ const getContentType = (headers = {}) => {
return contentType;
};
-const interpolateVars = (request, envVars = {}, collectionVariables = {}, 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
@@ -34,11 +36,13 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
return str;
}
- // collectionVariables take precedence over envVars
+ // runtimeVariables take precedence over envVars
const combinedVars = {
- ...envVars,
- ...requestVariables,
...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
+ ...requestVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
@@ -59,14 +63,6 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
- if (typeof request.data === 'object') {
- try {
- let parsed = JSON.stringify(request.data);
- parsed = _interpolate(parsed);
- request.data = JSON.parse(parsed);
- } catch (err) {}
- }
-
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);
diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
index 33b845e59..144542418 100644
--- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js
+++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js
@@ -23,18 +23,14 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
let requestCopy = cloneDeep(request);
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
const oAuth = get(requestCopy, 'oauth2', {});
- const { clientId, clientSecret, callbackUrl, scope, state, pkce } = oAuth;
+ const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: callbackUrl,
client_id: clientId,
- client_secret: clientSecret,
- state: state
+ client_secret: clientSecret
};
- if (scope) {
- data['scope'] = scope;
- }
if (pkce) {
data['code_verifier'] = codeVerifier;
}
@@ -51,26 +47,26 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
const { oauth2 } = request;
const { callbackUrl, clientId, authorizationUrl, scope, state, pkce } = oauth2;
- let oauth2QueryParams =
- (authorizationUrl.indexOf('?') > -1 ? '&' : '?') + `client_id=${clientId}&response_type=code`;
+ const authorizationUrlWithQueryParams = new URL(authorizationUrl);
+ authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
+ authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);
if (callbackUrl) {
- oauth2QueryParams += `&redirect_uri=${callbackUrl}`;
+ authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl);
}
if (scope) {
- oauth2QueryParams += `&scope=${scope}`;
+ authorizationUrlWithQueryParams.searchParams.append('scope', scope);
}
if (pkce) {
- oauth2QueryParams += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
+ authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);
+ authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');
}
if (state) {
- oauth2QueryParams += `&state=${state}`;
+ authorizationUrlWithQueryParams.searchParams.append('state', state);
}
-
- const authorizationUrlWithQueryParams = authorizationUrl + oauth2QueryParams;
try {
const oauth2Store = new Oauth2Store();
const { authorizationCode } = await authorizeUserInWindow({
- authorizeUrl: authorizationUrlWithQueryParams,
+ authorizeUrl: authorizationUrlWithQueryParams.toString(),
callbackUrl,
session: oauth2Store.getSessionIdOfCollection(collectionUid)
});
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index fd15b08ba..ebdde596d 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,7 +1,6 @@
const os = require('os');
const { get, each, filter, extend, compact } = require('lodash');
const decomment = require('decomment');
-var JSONbig = require('json-bigint');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
@@ -18,8 +17,8 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
folderHeaders.set(header.name, header.value);
}
});
- } else {
- let headers = get(i, 'request.headers', []);
+ } 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);
@@ -45,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 {
- let vars = get(i, 'request.vars.req', []);
+ 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 {
- let vars = get(i, 'request.vars.res', []);
+ 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,
@@ -119,7 +120,7 @@ const mergeFolderLevelVars = (request, requestTreePath) => {
}));
};
-const mergeFolderLevelScripts = (request, requestTreePath) => {
+const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
let folderCombinedPreReqScript = [];
let folderCombinedPostResScript = [];
let folderCombinedTests = [];
@@ -147,11 +148,19 @@ const mergeFolderLevelScripts = (request, requestTreePath) => {
}
if (folderCombinedPostResScript.length) {
- request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
+ 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) {
- request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
+ if (scriptFlow === 'sequential') {
+ request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
+ } else {
+ request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
+ }
}
};
@@ -305,11 +314,13 @@ const prepareRequest = (item, collection) => {
}
});
+ // 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);
- mergeFolderLevelVars(request, requestTreePath);
+ mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
+ mergeVars(collection, request, requestTreePath);
}
each(request.headers, (h) => {
@@ -336,16 +347,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;
}
}
@@ -402,6 +407,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;
diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js
index f07c79c06..0486ead5e 100644
--- a/packages/bruno-electron/src/ipc/preferences.js
+++ b/packages/bruno-electron/src/ipc/preferences.js
@@ -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();
diff --git a/packages/bruno-electron/src/store/collection-security.js b/packages/bruno-electron/src/store/collection-security.js
new file mode 100644
index 000000000..5873c629a
--- /dev/null
+++ b/packages/bruno-electron/src/store/collection-security.js
@@ -0,0 +1,39 @@
+const _ = require('lodash');
+const Store = require('electron-store');
+
+class CollectionSecurityStore {
+ constructor() {
+ this.store = new Store({
+ name: 'collection-security',
+ clearInvalidConfig: true
+ });
+ }
+
+ setSecurityConfigForCollection(collectionPathname, securityConfig) {
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+
+ if (!collection) {
+ collections.push({
+ path: collectionPathname,
+ securityConfig: {
+ jsSandboxMode: securityConfig.jsSandboxMode
+ }
+ });
+
+ this.store.set('collections', collections);
+ return;
+ }
+
+ collection.securityConfig = securityConfig || {};
+ this.store.set('collections', collections);
+ }
+
+ getSecurityConfigForCollection(collectionPathname) {
+ const collections = this.store.get('collections') || [];
+ const collection = _.find(collections, (c) => c.path === collectionPathname);
+ return collection?.securityConfig || {};
+ }
+}
+
+module.exports = CollectionSecurityStore;
diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js
index f9497abee..33d7a02f8 100644
--- a/packages/bruno-electron/src/store/preferences.js
+++ b/packages/bruno-electron/src/store/preferences.js
@@ -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'.
@@ -23,10 +23,11 @@ const defaultPreferences = {
timeout: 0
},
font: {
- codeFont: 'default'
+ codeFont: 'default',
+ codeFontSize: 14
},
proxy: {
- enabled: false,
+ mode: 'off',
protocol: 'http',
hostname: '',
port: null,
@@ -54,10 +55,11 @@ const preferencesSchema = Yup.object().shape({
timeout: Yup.number()
}),
font: Yup.object().shape({
- codeFont: Yup.string().nullable()
+ codeFont: Yup.string().nullable(),
+ 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(),
@@ -79,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) {
@@ -134,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
+ };
}
};
diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js
index b73e437e6..89e33b1f9 100644
--- a/packages/bruno-electron/src/utils/encryption.js
+++ b/packages/bruno-electron/src/utils/encryption.js
@@ -1,5 +1,5 @@
const crypto = require('crypto');
-const { machineIdSync } = require('node-machine-id');
+const { machineIdSync } = require('@usebruno/node-machine-id');
const { safeStorage } = require('electron');
// Constants for algorithm identification
@@ -86,7 +86,11 @@ function decryptString(str) {
}
if (algo === ELECTRONSAFESTORAGE_ALGO) {
- return safeStorageDecrypt(encryptedString);
+ if (safeStorage && safeStorage.isEncryptionAvailable()) {
+ return safeStorageDecrypt(encryptedString);
+ } else {
+ return '';
+ }
}
if (algo === AES256_ALGO) {
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 8216bd9c9..752cb339c 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -50,6 +50,18 @@ const normalizeAndResolvePath = (pathname) => {
return path.resolve(pathname);
};
+function isWSLPath(pathname) {
+ // Check if the path starts with the WSL prefix
+ // eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru"
+ return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\');
+}
+
+function normalizeWslPath(pathname) {
+ // Replace the WSL path prefix and convert forward slashes to backslashes
+ // This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)
+ return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
+}
+
const writeFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content, {
@@ -143,6 +155,8 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
+// const isW
+
const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
};
@@ -154,6 +168,8 @@ module.exports = {
isFile,
isDirectory,
normalizeAndResolvePath,
+ isWSLPath,
+ normalizeWslPath,
writeFile,
writeBinaryFile,
hasJsonExtension,
diff --git a/packages/bruno-electron/tests/network/index.spec.js b/packages/bruno-electron/tests/network/index.spec.js
index 7c45c2538..02a9b9083 100644
--- a/packages/bruno-electron/tests/network/index.spec.js
+++ b/packages/bruno-electron/tests/network/index.spec.js
@@ -1,25 +1,15 @@
-// damn jest throws an error when no tests are found in a file
-// --passWithNoTests doesn't work
+const { configureRequest } = require('../../src/ipc/network/index');
-describe('dummy test', () => {
- it('should pass', () => {
- expect(true).toBe(true);
+describe('index: configureRequest', () => {
+ it("Should add 'http://' to the URL if no protocol is specified", async () => {
+ const request = { method: 'GET', url: 'test-domain', body: {} };
+ await configureRequest(null, request, null, null, null, null);
+ expect(request.url).toEqual('http://test-domain');
+ });
+
+ it("Should NOT add 'http://' to the URL if a protocol is specified", async () => {
+ const request = { method: 'GET', url: 'ftp://test-domain', body: {} };
+ await configureRequest(null, request, null, null, null, null);
+ expect(request.url).toEqual('ftp://test-domain');
});
});
-
-// todo: fix this failing test
-// const { configureRequest } = require('../../src/ipc/network/index');
-
-// describe('index: configureRequest', () => {
-// it("Should add 'http://' to the URL if no protocol is specified", async () => {
-// const request = { method: 'GET', url: 'test-domain', body: {} };
-// await configureRequest(null, request, null, null, null, null);
-// expect(request.url).toEqual('http://test-domain');
-// });
-
-// it("Should NOT add 'http://' to the URL if a protocol is specified", async () => {
-// const request = { method: 'GET', url: 'ftp://test-domain', body: {} };
-// await configureRequest(null, request, null, null, null, null);
-// expect(request.url).toEqual('ftp://test-domain');
-// });
-// });
diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js
index e3441953b..808a127d9 100644
--- a/packages/bruno-electron/tests/network/prepare-request.spec.js
+++ b/packages/bruno-electron/tests/network/prepare-request.spec.js
@@ -6,7 +6,7 @@ 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 = '{\n"test": "{{someVar}}" \n}';
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});
diff --git a/packages/bruno-graphql-docs/package.json b/packages/bruno-graphql-docs/package.json
index 393a3d792..ba609393b 100644
--- a/packages/bruno-graphql-docs/package.json
+++ b/packages/bruno-graphql-docs/package.json
@@ -22,7 +22,7 @@
"postcss": "^8.4.18",
"react": "18.2.0",
"react-dom": "18.2.0",
- "rollup": "3.2.5",
+ "rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
@@ -34,6 +34,6 @@
"markdown-it": "^13.0.1"
},
"overrides": {
- "rollup": "3.2.5"
+ "rollup":"3.29.4"
}
}
diff --git a/packages/bruno-js/.gitignore b/packages/bruno-js/.gitignore
new file mode 100644
index 000000000..22322eb1b
--- /dev/null
+++ b/packages/bruno-js/.gitignore
@@ -0,0 +1 @@
+src/sandbox/bundle-browser-rollup.js
\ No newline at end of file
diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json
index 901ab0073..fdd8bfbd9 100644
--- a/packages/bruno-js/package.json
+++ b/packages/bruno-js/package.json
@@ -11,7 +11,8 @@
"@n8n/vm2": "^3.9.23"
},
"scripts": {
- "test": "jest --testPathIgnorePatterns test.js"
+ "test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
+ "sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js"
},
"dependencies": {
"@usebruno/common": "0.1.0",
@@ -24,12 +25,29 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
+ "crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.4",
- "node-fetch": "2.*",
+ "node-fetch": "^2.7.0",
"node-vault": "^0.10.2",
+ "path": "^0.12.7",
+ "quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^23.0.2",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.0.1",
+ "rollup": "3.2.5",
+ "rollup-plugin-node-builtins": "^2.1.2",
+ "rollup-plugin-node-globals": "^1.4.0",
+ "rollup-plugin-polyfill-node": "^0.13.0",
+ "rollup-plugin-terser": "^7.0.2",
+ "stream": "^0.0.2",
+ "terser": "^5.31.1",
+ "uglify-js": "^3.18.0",
+ "util": "^0.12.5"
}
}
diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js
index b3d363b9a..7f24cea14 100644
--- a/packages/bruno-js/src/bru.js
+++ b/packages/bruno-js/src/bru.js
@@ -4,10 +4,12 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;
class Bru {
- constructor(envVariables, collectionVariables, processEnvVars, collectionPath, requestVariables) {
+ constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables) {
this.envVariables = envVariables || {};
- this.collectionVariables = collectionVariables || {};
+ this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
+ this.collectionVariables = collectionVariables || {};
+ this.folderVariables = folderVariables || {};
this.requestVariables = requestVariables || {};
this.collectionPath = collectionPath;
}
@@ -18,9 +20,11 @@ class Bru {
}
const combinedVars = {
- ...this.envVariables,
- ...this.requestVariables,
...this.collectionVariables,
+ ...this.envVariables,
+ ...this.folderVariables,
+ ...this.requestVariables,
+ ...this.runtimeVariables,
process: {
env: {
...this.processEnvVars
@@ -43,6 +47,10 @@ class Bru {
return this.processEnvVars[key];
}
+ hasEnvVar(key) {
+ return Object.hasOwn(this.envVariables, key);
+ }
+
getEnvVar(key) {
return this._interpolate(this.envVariables[key]);
}
@@ -55,6 +63,10 @@ class Bru {
this.envVariables[key] = value;
}
+ hasVar(key) {
+ return Object.hasOwn(this.runtimeVariables, key);
+ }
+
setVar(key, value) {
if (!key) {
throw new Error('Creating a variable without specifying a name is not allowed.');
@@ -63,24 +75,36 @@ 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, "-", "_", "."'
);
}
- this.collectionVariables[key] = value;
+ this.runtimeVariables[key] = value;
}
getVar(key) {
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, "-", "_", "."'
);
}
+ return this._interpolate(this.runtimeVariables[key]);
+ }
+
+ deleteVar(key) {
+ 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]);
}
@@ -88,6 +112,10 @@ class Bru {
setNextRequest(nextRequest) {
this.nextRequest = nextRequest;
}
+
+ sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
}
module.exports = Bru;
diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js
index 909adf92a..cf5f59aca 100644
--- a/packages/bruno-js/src/bruno-request.js
+++ b/packages/bruno-js/src/bruno-request.js
@@ -1,11 +1,34 @@
class BrunoRequest {
+ /**
+ * The following properties are available as shorthand:
+ * - req.url
+ * - req.method
+ * - req.headers
+ * - req.timeout
+ * - req.body
+ *
+ * Above shorthands are useful for accessing the request properties directly in the scripts
+ * It must be noted that the user cannot set these properties directly.
+ * They should use the respective setter methods to set these properties.
+ */
constructor(req) {
this.req = req;
this.url = req.url;
this.method = req.method;
this.headers = req.headers;
- this.body = req.data;
this.timeout = req.timeout;
+
+ /**
+ * We automatically parse the JSON body if the content type is JSON
+ * This is to make it easier for the user to access the body directly
+ *
+ * It must be noted that the request data is always a string and is what gets sent over the network
+ * If the user wants to access the raw data, they can use getBody({raw: true}) method
+ */
+ const isJson = this.hasJSONContentType(this.req.headers);
+ if (isJson) {
+ this.body = this.__safeParseJSON(req.data);
+ }
}
getUrl() {
@@ -13,6 +36,7 @@ class BrunoRequest {
}
setUrl(url) {
+ this.url = url;
this.req.url = url;
}
@@ -37,6 +61,7 @@ class BrunoRequest {
}
setMethod(method) {
+ this.method = method;
this.req.method = method;
}
@@ -45,6 +70,7 @@ class BrunoRequest {
}
setHeaders(headers) {
+ this.headers = headers;
this.req.headers = headers;
}
@@ -53,15 +79,60 @@ class BrunoRequest {
}
setHeader(name, value) {
+ this.headers[name] = value;
this.req.headers[name] = value;
}
- getBody() {
+ hasJSONContentType(headers) {
+ const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
+ return contentType.includes('json');
+ }
+
+ /**
+ * Get the body of the request
+ *
+ * We automatically parse and return the JSON body if the content type is JSON
+ * If the user wants the raw body, they can pass the raw option as true
+ */
+ getBody(options = {}) {
+ if (options.raw) {
+ return this.req.data;
+ }
+
+ const isJson = this.hasJSONContentType(this.req.headers);
+ if (isJson) {
+ return this.__safeParseJSON(this.req.data);
+ }
+
return this.req.data;
}
- setBody(data) {
+ /**
+ * If the content type is JSON and if the data is an object
+ * - We set the body property as the object itself
+ * - We set the request data as the stringified JSON as it is what gets sent over the network
+ * Otherwise
+ * - We set the request data as the data itself
+ * - We set the body property as the data itself
+ *
+ * If the user wants to override this behavior, they can pass the raw option as true
+ */
+ setBody(data, options = {}) {
+ if (options.raw) {
+ this.req.data = data;
+ this.body = data;
+ return;
+ }
+
+ const isJson = this.hasJSONContentType(this.req.headers);
+ if (isJson && this.__isObject(data)) {
+ this.body = data;
+ this.req.data = this.__safeStringifyJSON(data);
+ return;
+ }
+
this.req.data = data;
+ this.body = data;
}
setMaxRedirects(maxRedirects) {
@@ -73,8 +144,34 @@ class BrunoRequest {
}
setTimeout(timeout) {
+ this.timeout = timeout;
this.req.timeout = timeout;
}
+
+ __safeParseJSON(str) {
+ try {
+ return JSON.parse(str);
+ } catch (e) {
+ return str;
+ }
+ }
+
+ __safeStringifyJSON(obj) {
+ try {
+ return JSON.stringify(obj);
+ } catch (e) {
+ return obj;
+ }
+ }
+
+ __isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+
+ disableParsingResponseJson() {
+ this.req.__brunoDisableParsingResponseJson = true;
+ }
}
module.exports = BrunoRequest;
diff --git a/packages/bruno-js/src/interpolate-string.js b/packages/bruno-js/src/interpolate-string.js
index 7a0296b35..2692641c2 100644
--- a/packages/bruno-js/src/interpolate-string.js
+++ b/packages/bruno-js/src/interpolate-string.js
@@ -2,16 +2,18 @@ const { interpolate } = require('@usebruno/common');
const interpolateString = (
str,
- { envVariables = {}, collectionVariables = {}, processEnvVars = {}, requestVariables = {} }
+ { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} }
) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const combinedVars = {
- ...envVariables,
- ...requestVariables,
...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
+ ...requestVariables,
+ ...runtimeVariables,
process: {
env: {
...processEnvVars
diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js
index 49f1f588d..aafacfe8a 100644
--- a/packages/bruno-js/src/runtime/assert-runtime.js
+++ b/packages/bruno-js/src/runtime/assert-runtime.js
@@ -5,6 +5,7 @@ const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { interpolateString } = require('../interpolate-string');
+const { executeQuickJsVm } = require('../sandbox/quickjs');
const { expect } = chai;
chai.use(require('chai-string'));
@@ -161,14 +162,40 @@ const isUnaryOperator = (operator) => {
return unaryOperators.includes(operator);
};
-const evaluateRhsOperand = (rhsOperand, operator, context) => {
+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) => {
+ if (runtime === 'quickjs') {
+ return executeQuickJsVm({
+ script: expr,
+ context,
+ scriptType: 'expression'
+ });
+ }
+
+ return evaluateJsExpression(expr, context);
+};
+
+const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
if (isUnaryOperator(operator)) {
return;
}
const interpolationContext = {
- requestVariables: context.bru.requestVariables,
collectionVariables: context.bru.collectionVariables,
+ folderVariables: context.bru.folderVariables,
+ requestVariables: context.bru.requestVariables,
+ runtimeVariables: context.bru.runtimeVariables,
envVariables: context.bru.envVariables,
processEnvVars: context.bru.processEnvVars
};
@@ -181,13 +208,17 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return rhsOperand
.split(',')
- .map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
+ .map((v) =>
+ evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
+ );
}
if (operator === 'between') {
const [lhs, rhs] = rhsOperand
.split(',')
- .map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
+ .map((v) =>
+ evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
+ );
return [lhs, rhs];
}
@@ -200,18 +231,32 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return interpolateString(rhsOperand, interpolationContext);
}
- return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
+ return evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(rhsOperand, interpolationContext), context, runtime);
};
class AssertRuntime {
- runAssertions(assertions, request, response, envVariables, collectionVariables, processEnvVars) {
+ constructor(props) {
+ this.runtime = props?.runtime || 'vm2';
+ }
+
+ 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, collectionVariables, processEnvVars, undefined, requestVariables);
+ const bru = new Bru(
+ envVariables,
+ runtimeVariables,
+ processEnvVars,
+ undefined,
+ collectionVariables,
+ folderVariables,
+ requestVariables
+ );
const req = new BrunoRequest(request);
const res = createResponseParser(response);
@@ -222,9 +267,11 @@ class AssertRuntime {
};
const context = {
- ...envVariables,
- ...requestVariables,
...collectionVariables,
+ ...envVariables,
+ ...folderVariables,
+ ...requestVariables,
+ ...runtimeVariables,
...processEnvVars,
...bruContext
};
@@ -238,8 +285,8 @@ class AssertRuntime {
const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
try {
- const lhs = evaluateJsExpression(lhsExpr, context);
- const rhs = evaluateRhsOperand(rhsOperand, operator, context);
+ const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);
+ const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);
switch (operator) {
case 'eq':
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index 5089178ba..9dc47a29d 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -28,9 +28,12 @@ const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
+const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class ScriptRuntime {
- constructor() {}
+ constructor(props) {
+ this.runtime = props?.runtime || 'vm2';
+ }
// This approach is getting out of hand
// Need to refactor this to use a single arg (object) instead of 7
@@ -38,14 +41,16 @@ class ScriptRuntime {
script,
request,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig
) {
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, collectionVariables, 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', []);
@@ -86,6 +91,22 @@ class ScriptRuntime {
};
}
+ if (this.runtime === 'quickjs') {
+ await executeQuickJsVmAsync({
+ script: script,
+ context: context,
+ collectionPath
+ });
+
+ return {
+ request,
+ envVariables: cleanJson(envVariables),
+ runtimeVariables: cleanJson(runtimeVariables),
+ nextRequestName: bru.nextRequest
+ };
+ }
+
+ // default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
@@ -123,10 +144,11 @@ class ScriptRuntime {
});
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
await asyncVM();
+
return {
request,
envVariables: cleanJson(envVariables),
- collectionVariables: cleanJson(collectionVariables),
+ runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
@@ -136,14 +158,16 @@ class ScriptRuntime {
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig
) {
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, collectionVariables, 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);
@@ -176,10 +200,27 @@ class ScriptRuntime {
log: customLogger('log'),
info: customLogger('info'),
warn: customLogger('warn'),
- error: customLogger('error')
+ error: customLogger('error'),
+ debug: customLogger('debug')
};
}
+ if (this.runtime === 'quickjs') {
+ await executeQuickJsVmAsync({
+ script: script,
+ context: context,
+ collectionPath
+ });
+
+ return {
+ response,
+ envVariables: cleanJson(envVariables),
+ runtimeVariables: cleanJson(runtimeVariables),
+ nextRequestName: bru.nextRequest
+ };
+ }
+
+ // default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
@@ -221,7 +262,7 @@ class ScriptRuntime {
return {
response,
envVariables: cleanJson(envVariables),
- collectionVariables: cleanJson(collectionVariables),
+ runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index 0bfafec06..53fab05eb 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -30,23 +30,28 @@ const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
+const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class TestRuntime {
- constructor() {}
+ constructor(props) {
+ this.runtime = props?.runtime || 'vm2';
+ }
async runTests(
testsFile,
request,
response,
envVariables,
- collectionVariables,
+ runtimeVariables,
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig
) {
+ const collectionVariables = request?.collectionVariables || {};
+ const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
- const bru = new Bru(envVariables, collectionVariables, 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);
@@ -75,11 +80,13 @@ class TestRuntime {
return {
request,
envVariables,
- collectionVariables,
- results: __brunoTestResults.getResults()
+ runtimeVariables,
+ results: __brunoTestResults.getResults(),
+ nextRequestName: bru.nextRequest
};
}
+
const context = {
test,
bru,
@@ -100,54 +107,63 @@ class TestRuntime {
log: customLogger('log'),
info: customLogger('info'),
warn: customLogger('warn'),
+ debug: customLogger('debug'),
error: customLogger('error')
};
}
- const vm = new NodeVM({
- sandbox: context,
- require: {
- context: 'sandbox',
- external: true,
- root: [collectionPath, ...additionalContextRootsAbsolute],
- mock: {
- // node libs
- path,
- stream,
- util,
- url,
- http,
- https,
- punycode,
- zlib,
- // 3rd party libs
- ajv,
- 'ajv-formats': addFormats,
- btoa,
- atob,
- lodash,
- moment,
- uuid,
- nanoid,
- axios,
- chai,
- 'node-fetch': fetch,
- 'crypto-js': CryptoJS,
- ...whitelistedModules,
- fs: allowScriptFilesystemAccess ? fs : undefined,
- 'node-vault': NodeVault
+ if (this.runtime === 'quickjs') {
+ await executeQuickJsVmAsync({
+ script: testsFile,
+ context: context
+ });
+ } else {
+ // default runtime is vm2
+ const vm = new NodeVM({
+ sandbox: context,
+ require: {
+ context: 'sandbox',
+ external: true,
+ root: [collectionPath, ...additionalContextRootsAbsolute],
+ mock: {
+ // node libs
+ path,
+ stream,
+ util,
+ url,
+ http,
+ https,
+ punycode,
+ zlib,
+ // 3rd party libs
+ ajv,
+ 'ajv-formats': addFormats,
+ btoa,
+ atob,
+ lodash,
+ moment,
+ uuid,
+ nanoid,
+ axios,
+ chai,
+ 'node-fetch': fetch,
+ 'crypto-js': CryptoJS,
+ ...whitelistedModules,
+ fs: allowScriptFilesystemAccess ? fs : undefined,
+ 'node-vault': NodeVault
+ }
}
- }
- });
-
- const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
- await asyncVM();
+ });
+ const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
+ await asyncVM();
+ }
return {
request,
envVariables: cleanJson(envVariables),
- collectionVariables: cleanJson(collectionVariables),
- results: cleanJson(__brunoTestResults.getResults())
+ runtimeVariables: cleanJson(runtimeVariables),
+ results: cleanJson(__brunoTestResults.getResults()),
+ nextRequestName: bru.nextRequest
};
}
}
diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js
index 31eb102fa..1ed806000 100644
--- a/packages/bruno-js/src/runtime/vars-runtime.js
+++ b/packages/bruno-js/src/runtime/vars-runtime.js
@@ -1,46 +1,36 @@
const _ = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
-const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
+const { evaluateJsExpression, createResponseParser } = require('../utils');
-class VarsRuntime {
- runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath, processEnvVars) {
- if (!request?.requestVariables) {
- request.requestVariables = {};
- }
- const enabledVars = _.filter(vars, (v) => v.enabled);
- if (!enabledVars.length) {
- return;
- }
+const { executeQuickJsVm } = require('../sandbox/quickjs');
- const bru = new Bru(envVariables, collectionVariables, processEnvVars);
- const req = new BrunoRequest(request);
-
- const bruContext = {
- bru,
- req
- };
-
- const context = {
- ...envVariables,
- ...collectionVariables,
- ...bruContext
- };
-
- _.each(enabledVars, (v) => {
- const value = evaluateJsTemplateLiteral(v.value, context);
- request?.requestVariables && (request.requestVariables[v.name] = value);
+const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
+ if (runtime === 'quickjs') {
+ return executeQuickJsVm({
+ script: expr,
+ context,
+ scriptType: 'expression'
});
}
- runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath, processEnvVars) {
+ return evaluateJsExpression(expr, context);
+};
+
+class VarsRuntime {
+ constructor(props) {
+ this.runtime = props?.runtime || 'vm2';
+ this.mode = props?.mode || 'developer';
+ }
+
+ runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledVars = _.filter(vars, (v) => v.enabled);
if (!enabledVars.length) {
return;
}
- const bru = new Bru(envVariables, collectionVariables, processEnvVars, undefined, requestVariables);
+ const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
const req = new BrunoRequest(request);
const res = createResponseParser(response);
@@ -52,14 +42,14 @@ class VarsRuntime {
const context = {
...envVariables,
- ...collectionVariables,
+ ...runtimeVariables,
...bruContext
};
const errors = new Map();
_.each(enabledVars, (v) => {
try {
- const value = evaluateJsExpression(v.value, context);
+ const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);
bru.setVar(v.name, value);
} catch (error) {
errors.set(v.name, error);
@@ -75,7 +65,7 @@ class VarsRuntime {
return {
envVariables,
- collectionVariables,
+ runtimeVariables,
error
};
}
diff --git a/packages/bruno-js/src/sandbox/bundle-libraries.js b/packages/bruno-js/src/sandbox/bundle-libraries.js
new file mode 100644
index 000000000..6ecec818b
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/bundle-libraries.js
@@ -0,0 +1,88 @@
+const rollup = require('rollup');
+const { nodeResolve } = require('@rollup/plugin-node-resolve');
+const commonjs = require('@rollup/plugin-commonjs');
+const fs = require('fs');
+const { terser } = require('rollup-plugin-terser');
+
+const bundleLibraries = async () => {
+ const codeScript = `
+ import { expect, assert } from 'chai';
+ import { Buffer } from "buffer";
+ import moment from "moment";
+ import btoa from "btoa";
+ import atob from "atob";
+ import * as CryptoJS from "crypto-js-3.1.9-1";
+ globalThis.expect = expect;
+ globalThis.assert = assert;
+ globalThis.moment = moment;
+ globalThis.btoa = btoa;
+ globalThis.atob = atob;
+ globalThis.Buffer = Buffer;
+ globalThis.CryptoJS = CryptoJS;
+ globalThis.requireObject = {
+ ...(globalThis.requireObject || {}),
+ 'chai': { expect, assert },
+ 'moment': moment,
+ 'buffer': { Buffer },
+ 'btoa': btoa,
+ 'atob': atob,
+ 'crypto-js': CryptoJS
+ };
+`;
+
+ const config = {
+ input: {
+ input: 'inline-code',
+ plugins: [
+ {
+ name: 'inline-code-plugin',
+ resolveId(id) {
+ if (id === 'inline-code') {
+ return id;
+ }
+ return null;
+ },
+ load(id) {
+ if (id === 'inline-code') {
+ return codeScript;
+ }
+ return null;
+ }
+ },
+ nodeResolve({
+ preferBuiltins: false,
+ browser: false
+ }),
+ commonjs(),
+ terser()
+ ]
+ },
+ output: {
+ file: './src/sandbox/bundle-browser-rollup.js',
+ format: 'iife',
+ name: 'MyBundle'
+ }
+ };
+
+ try {
+ const bundle = await rollup.rollup(config.input);
+ const { output } = await bundle.generate(config.output);
+ fs.writeFileSync(
+ './src/sandbox/bundle-browser-rollup.js',
+ `
+ const getBundledCode = () => {
+ return function(){
+ ${output?.map((o) => o.code).join('\n')}
+ }()
+ }
+ module.exports = getBundledCode;
+ `
+ );
+ } catch (error) {
+ console.error('Error while bundling:', error);
+ }
+};
+
+bundleLibraries();
+
+module.exports = bundleLibraries;
diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js
new file mode 100644
index 000000000..d5fe5e8f3
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/index.js
@@ -0,0 +1,193 @@
+const addBruShimToContext = require('./shims/bru');
+const addBrunoRequestShimToContext = require('./shims/bruno-request');
+const addConsoleShimToContext = require('./shims/console');
+const addBrunoResponseShimToContext = require('./shims/bruno-response');
+const addTestShimToContext = require('./shims/test');
+const addLibraryShimsToContext = require('./shims/lib');
+const addLocalModuleLoaderShimToContext = require('./shims/local-module');
+const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscripten');
+
+// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
+const getBundledCode = require('../bundle-browser-rollup');
+const addPathShimToContext = require('./shims/lib/path');
+const { marshallToVm } = require('./utils');
+
+let QuickJSSyncContext;
+const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
+const getContext = (opts) => loader().then((mod) => (QuickJSSyncContext = mod.newContext(opts)));
+getContext();
+
+const toNumber = (value) => {
+ const num = Number(value);
+ return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
+};
+
+const removeQuotes = (str) => {
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
+ return str.slice(1, -1);
+ }
+ return str;
+};
+
+const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
+ if (!externalScript?.length || typeof externalScript !== 'string') {
+ return externalScript;
+ }
+ externalScript = externalScript?.trim();
+
+ if (!isNaN(Number(externalScript))) {
+ return Number(externalScript);
+ }
+
+ if (externalScript === 'true') return true;
+ if (externalScript === 'false') return false;
+ if (externalScript === 'null') return null;
+ if (externalScript === 'undefined') return undefined;
+
+ externalScript = removeQuotes(externalScript);
+
+ const vm = QuickJSSyncContext;
+
+ try {
+ const { bru, req, res, ...variables } = externalContext;
+
+ bru && addBruShimToContext(vm, bru);
+ req && addBrunoRequestShimToContext(vm, req);
+ res && addBrunoResponseShimToContext(vm, res);
+
+ Object.entries(variables)?.forEach(([key, value]) => {
+ vm.setProp(vm.global, key, marshallToVm(value, vm));
+ });
+
+ const templateLiteralText = `\`${externalScript}\``;
+ const jsExpressionText = `${externalScript}`;
+
+ let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
+
+ const result = vm.evalCode(scriptText);
+ if (result.error) {
+ let e = vm.dump(result.error);
+ result.error.dispose();
+ return e;
+ } else {
+ let v = vm.dump(result.value);
+ result.value.dispose();
+ return v;
+ }
+ } catch (error) {
+ console.error('Error executing the script!', error);
+ }
+};
+
+const executeQuickJsVmAsync = async ({ script: externalScript, context: externalContext, collectionPath }) => {
+ if (!externalScript?.length || typeof externalScript !== 'string') {
+ return externalScript;
+ }
+ externalScript = externalScript?.trim();
+
+ if (!isNaN(Number(externalScript))) {
+ return toNumber(externalScript);
+ }
+
+ if (externalScript === 'true') return true;
+ if (externalScript === 'false') return false;
+ if (externalScript === 'null') return null;
+ if (externalScript === 'undefined') return undefined;
+
+ externalScript = removeQuotes(externalScript);
+
+ try {
+ const module = await newQuickJSWASMModule();
+ const vm = module.newContext();
+
+ const bundledCode = getBundledCode?.toString() || '';
+ const moduleLoaderCode = function () {
+ return `
+ globalThis.require = (mod) => {
+ let lib = globalThis.requireObject[mod];
+ let isModuleAPath = (module) => (module?.startsWith('.') || module?.startsWith?.(bru.cwd()))
+ if (lib) {
+ return lib;
+ }
+ else if (isModuleAPath(mod)) {
+ // fetch local module
+ let localModuleCode = globalThis.__brunoLoadLocalModule(mod);
+
+ // compile local module as iife
+ (function (){
+ const initModuleExportsCode = "const module = { exports: {} };"
+ const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;";
+ const patchedRequire = ${`
+ "\\n;" +
+ "let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" +
+ "\\n;"
+ `}
+ eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode);
+ })();
+
+ // resolve module
+ return globalThis.requireObject[mod];
+ }
+ else {
+ throw new Error("Cannot find module " + mod);
+ }
+ }
+ `;
+ };
+
+ vm.evalCode(
+ `
+ (${bundledCode})()
+ ${moduleLoaderCode()}
+ `
+ );
+
+ const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
+
+ bru && addBruShimToContext(vm, bru);
+ req && addBrunoRequestShimToContext(vm, req);
+ res && addBrunoResponseShimToContext(vm, res);
+ consoleFn && addConsoleShimToContext(vm, consoleFn);
+ addLocalModuleLoaderShimToContext(vm, collectionPath);
+ addPathShimToContext(vm);
+
+ await addLibraryShimsToContext(vm);
+
+ test && __brunoTestResults && addTestShimToContext(vm, __brunoTestResults);
+
+ const script = `
+ (async () => {
+ const setTimeout = async(fn, timer) => {
+ v = await bru.sleep(timer);
+ fn.apply();
+ }
+ await bru.sleep(0);
+ try {
+ ${externalScript}
+ }
+ catch(error) {
+ console?.debug?.('quick-js:execution-end:with-error', error?.message);
+ throw new Error(error?.message);
+ }
+ return 'done';
+ })()
+ `;
+
+ const result = vm.evalCode(script);
+ const promiseHandle = vm.unwrapResult(result);
+ const resolvedResult = await vm.resolvePromise(promiseHandle);
+ promiseHandle.dispose();
+ const resolvedHandle = vm.unwrapResult(resolvedResult);
+ resolvedHandle.dispose();
+ // vm.dispose();
+ return;
+ } catch (error) {
+ console.error('Error executing the script!', error);
+ throw new Error(error);
+ }
+};
+
+module.exports = {
+ executeQuickJsVm,
+ executeQuickJsVmAsync
+};
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
new file mode 100644
index 000000000..f045b134b
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
@@ -0,0 +1,99 @@
+const { marshallToVm } = require('../utils');
+
+const addBruShimToContext = (vm, bru) => {
+ const bruObject = vm.newObject();
+
+ let cwd = vm.newFunction('cwd', function () {
+ return marshallToVm(bru.cwd(), vm);
+ });
+ vm.setProp(bruObject, 'cwd', cwd);
+ cwd.dispose();
+
+ let getEnvName = vm.newFunction('getEnvName', function () {
+ return marshallToVm(bru.getEnvName(), vm);
+ });
+ vm.setProp(bruObject, 'getEnvName', getEnvName);
+ getEnvName.dispose();
+
+ let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
+ return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
+ getProcessEnv.dispose();
+
+ let getEnvVar = vm.newFunction('getEnvVar', function (key) {
+ return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getEnvVar', getEnvVar);
+ getEnvVar.dispose();
+
+ let setEnvVar = vm.newFunction('setEnvVar', function (key, value) {
+ bru.setEnvVar(vm.dump(key), vm.dump(value));
+ });
+ vm.setProp(bruObject, 'setEnvVar', setEnvVar);
+ setEnvVar.dispose();
+
+ let getVar = vm.newFunction('getVar', function (key) {
+ return marshallToVm(bru.getVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getVar', getVar);
+ getVar.dispose();
+
+ let setVar = vm.newFunction('setVar', function (key, value) {
+ bru.setVar(vm.dump(key), vm.dump(value));
+ });
+ vm.setProp(bruObject, 'setVar', setVar);
+ setVar.dispose();
+
+ let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
+ bru.setNextRequest(vm.dump(nextRequest));
+ });
+ vm.setProp(bruObject, 'setNextRequest', setNextRequest);
+ setNextRequest.dispose();
+
+ let visualize = vm.newFunction('visualize', function (htmlString) {
+ bru.visualize(vm.dump(htmlString));
+ });
+ vm.setProp(bruObject, 'visualize', visualize);
+ visualize.dispose();
+
+ let getSecretVar = vm.newFunction('getSecretVar', function (key) {
+ return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'getSecretVar', getSecretVar);
+ getSecretVar.dispose();
+
+ let getRequestVar = vm.newFunction('getRequestVar', function (key) {
+ return marshallToVm(bru.getRequestVar(vm.dump(key)), vm);
+ });
+ 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();
+ setTimeout(() => {
+ promise.resolve(vm.newString('slept'));
+ }, t);
+ promise.settled.then(vm.runtime.executePendingJobs);
+ return promise.handle;
+ });
+ sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));
+
+ vm.setProp(vm.global, 'bru', bruObject);
+ bruObject.dispose();
+};
+
+module.exports = addBruShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
new file mode 100644
index 000000000..9be27fae7
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js
@@ -0,0 +1,112 @@
+const { marshallToVm } = require('../utils');
+
+const addBrunoRequestShimToContext = (vm, req) => {
+ const reqObject = vm.newObject();
+
+ const url = marshallToVm(req.getUrl(), vm);
+ const method = marshallToVm(req.getMethod(), vm);
+ const headers = marshallToVm(req.getHeaders(), vm);
+ const body = marshallToVm(req.getBody(), vm);
+ const timeout = marshallToVm(req.getTimeout(), vm);
+
+ vm.setProp(reqObject, 'url', url);
+ vm.setProp(reqObject, 'method', method);
+ vm.setProp(reqObject, 'headers', headers);
+ vm.setProp(reqObject, 'body', body);
+ vm.setProp(reqObject, 'timeout', timeout);
+
+ url.dispose();
+ method.dispose();
+ headers.dispose();
+ body.dispose();
+ timeout.dispose();
+
+ let getUrl = vm.newFunction('getUrl', function () {
+ return marshallToVm(req.getUrl(), vm);
+ });
+ vm.setProp(reqObject, 'getUrl', getUrl);
+ getUrl.dispose();
+
+ let setUrl = vm.newFunction('setUrl', function (url) {
+ req.setUrl(vm.dump(url));
+ });
+ vm.setProp(reqObject, 'setUrl', setUrl);
+ setUrl.dispose();
+
+ let getMethod = vm.newFunction('getMethod', function () {
+ return marshallToVm(req.getMethod(), vm);
+ });
+ vm.setProp(reqObject, 'getMethod', getMethod);
+ getMethod.dispose();
+
+ let getAuthMode = vm.newFunction('getAuthMode', function () {
+ return marshallToVm(req.getAuthMode(), vm);
+ });
+ vm.setProp(reqObject, 'getAuthMode', getAuthMode);
+ getAuthMode.dispose();
+
+ let setMethod = vm.newFunction('setMethod', function (method) {
+ req.setMethod(vm.dump(method));
+ });
+ vm.setProp(reqObject, 'setMethod', setMethod);
+ setMethod.dispose();
+
+ let getHeaders = vm.newFunction('getHeaders', function () {
+ return marshallToVm(req.getHeaders(), vm);
+ });
+ vm.setProp(reqObject, 'getHeaders', getHeaders);
+ getHeaders.dispose();
+
+ let setHeaders = vm.newFunction('setHeaders', function (headers) {
+ req.setHeaders(vm.dump(headers));
+ });
+ vm.setProp(reqObject, 'setHeaders', setHeaders);
+ setHeaders.dispose();
+
+ let getHeader = vm.newFunction('getHeader', function (name) {
+ return marshallToVm(req.getHeader(vm.dump(name)), vm);
+ });
+ vm.setProp(reqObject, 'getHeader', getHeader);
+ getHeader.dispose();
+
+ let setHeader = vm.newFunction('setHeader', function (name, value) {
+ req.setHeader(vm.dump(name), vm.dump(value));
+ });
+ vm.setProp(reqObject, 'setHeader', setHeader);
+ setHeader.dispose();
+
+ let getBody = vm.newFunction('getBody', function () {
+ return marshallToVm(req.getBody(), vm);
+ });
+ vm.setProp(reqObject, 'getBody', getBody);
+ getBody.dispose();
+
+ let setBody = vm.newFunction('setBody', function (data) {
+ req.setBody(vm.dump(data));
+ });
+ vm.setProp(reqObject, 'setBody', setBody);
+ setBody.dispose();
+
+ let setMaxRedirects = vm.newFunction('setMaxRedirects', function (maxRedirects) {
+ req.setMaxRedirects(vm.dump(maxRedirects));
+ });
+ vm.setProp(reqObject, 'setMaxRedirects', setMaxRedirects);
+ setMaxRedirects.dispose();
+
+ let getTimeout = vm.newFunction('getTimeout', function () {
+ return marshallToVm(req.getTimeout(), vm);
+ });
+ vm.setProp(reqObject, 'getTimeout', getTimeout);
+ getTimeout.dispose();
+
+ let setTimeout = vm.newFunction('setTimeout', function (timeout) {
+ req.setTimeout(vm.dump(timeout));
+ });
+ vm.setProp(reqObject, 'setTimeout', setTimeout);
+ setTimeout.dispose();
+
+ vm.setProp(vm.global, 'req', reqObject);
+ reqObject.dispose();
+};
+
+module.exports = addBrunoRequestShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
new file mode 100644
index 000000000..40736d342
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
@@ -0,0 +1,55 @@
+const { marshallToVm } = require('../utils');
+
+const addBrunoResponseShimToContext = (vm, res) => {
+ const resObject = vm.newObject();
+
+ const status = marshallToVm(res?.status, vm);
+ const headers = marshallToVm(res?.headers, vm);
+ const body = marshallToVm(res?.body, vm);
+ const responseTime = marshallToVm(res?.responseTime, vm);
+
+ vm.setProp(resObject, 'status', status);
+ vm.setProp(resObject, 'headers', headers);
+ vm.setProp(resObject, 'body', body);
+ vm.setProp(resObject, 'responseTime', responseTime);
+
+ status.dispose();
+ headers.dispose();
+ body.dispose();
+ responseTime.dispose();
+
+ let getStatus = vm.newFunction('getStatus', function () {
+ return marshallToVm(res.getStatus(), vm);
+ });
+ vm.setProp(resObject, 'getStatus', getStatus);
+ getStatus.dispose();
+
+ let getHeader = vm.newFunction('getHeader', function (name) {
+ return marshallToVm(res.getHeader(vm.dump(name)), vm);
+ });
+ vm.setProp(resObject, 'getHeader', getHeader);
+ getHeader.dispose();
+
+ let getHeaders = vm.newFunction('getHeaders', function () {
+ return marshallToVm(res.getHeaders(), vm);
+ });
+ vm.setProp(resObject, 'getHeaders', getHeaders);
+ getHeaders.dispose();
+
+ let getBody = vm.newFunction('getBody', function () {
+ return marshallToVm(res.getBody(), vm);
+ });
+ vm.setProp(resObject, 'getBody', getBody);
+ getBody.dispose();
+
+ let getResponseTime = vm.newFunction('getResponseTime', function () {
+ return marshallToVm(res.getResponseTime(), vm);
+ });
+ vm.setProp(resObject, 'getResponseTime', getResponseTime);
+ getResponseTime.dispose();
+
+ vm.setProp(vm.global, 'res', resObject);
+ resObject.dispose();
+};
+
+module.exports = addBrunoResponseShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/console.js b/packages/bruno-js/src/sandbox/quickjs/shims/console.js
new file mode 100644
index 000000000..984422893
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/console.js
@@ -0,0 +1,46 @@
+const addConsoleShimToContext = (vm, console) => {
+ if (!console) return;
+
+ const consoleHandle = vm.newObject();
+
+ const logHandle = vm.newFunction('log', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ console?.log?.(...nativeArgs);
+ });
+
+ const debugHandle = vm.newFunction('debug', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ console?.debug?.(...nativeArgs);
+ });
+
+ const infoHandle = vm.newFunction('info', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ console?.info?.(...nativeArgs);
+ });
+
+ const warnHandle = vm.newFunction('warn', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ console?.warn?.(...nativeArgs);
+ });
+
+ const errorHandle = vm.newFunction('error', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ console?.error?.(...nativeArgs);
+ });
+
+ vm.setProp(consoleHandle, 'log', logHandle);
+ vm.setProp(consoleHandle, 'debug', debugHandle);
+ vm.setProp(consoleHandle, 'info', infoHandle);
+ vm.setProp(consoleHandle, 'warn', warnHandle);
+ vm.setProp(consoleHandle, 'error', errorHandle);
+
+ vm.setProp(vm.global, 'console', consoleHandle);
+ consoleHandle.dispose();
+ logHandle.dispose();
+ debugHandle.dispose();
+ infoHandle.dispose();
+ warnHandle.dispose();
+ errorHandle.dispose();
+};
+
+module.exports = addConsoleShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.js
new file mode 100644
index 000000000..2f0fc0789
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.js
@@ -0,0 +1,72 @@
+const axios = require('axios');
+const { cleanJson } = require('../../../../utils');
+const { marshallToVm } = require('../../utils');
+
+const methods = ['get', 'post', 'put', 'patch', 'delete'];
+
+const addAxiosShimToContext = async (vm) => {
+ methods?.forEach((method) => {
+ const axiosHandle = vm.newFunction(method, (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ const promise = vm.newPromise();
+ axios[method](...nativeArgs)
+ .then((response) => {
+ const { status, headers, data } = response || {};
+ promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
+ })
+ .catch((err) => {
+ promise.resolve(
+ marshallToVm(
+ cleanJson({
+ message: err.message
+ }),
+ vm
+ )
+ );
+ });
+ promise.settled.then(vm.runtime.executePendingJobs);
+ return promise.handle;
+ });
+ axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios__${method}`, handle));
+ });
+
+ const axiosHandle = vm.newFunction('axios', (...args) => {
+ const nativeArgs = args.map(vm.dump);
+ const promise = vm.newPromise();
+ axios(...nativeArgs)
+ .then((response) => {
+ const { status, headers, data } = response || {};
+ promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
+ })
+ .catch((err) => {
+ promise.resolve(
+ marshallToVm(
+ cleanJson({
+ message: err.message
+ }),
+ vm
+ )
+ );
+ });
+ promise.settled.then(vm.runtime.executePendingJobs);
+ return promise.handle;
+ });
+ axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios`, handle));
+
+ vm.evalCode(
+ `
+ globalThis.axios = __bruno__axios;
+ ${methods
+ ?.map((method) => {
+ return `globalThis.axios.${method} = __bruno__axios__${method};`;
+ })
+ ?.join('\n')}
+ globalThis.requireObject = {
+ ...globalThis.requireObject,
+ axios: globalThis.axios,
+ }
+ `
+ );
+};
+
+module.exports = addAxiosShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/index.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/index.js
new file mode 100644
index 000000000..64f239c7f
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/index.js
@@ -0,0 +1,13 @@
+const addAxiosShimToContext = require('./axios');
+const addNanoidShimToContext = require('./nanoid');
+const addPathShimToContext = require('./path');
+const addUuidShimToContext = require('./uuid');
+
+const addLibraryShimsToContext = async (vm) => {
+ await addNanoidShimToContext(vm);
+ await addAxiosShimToContext(vm);
+ await addUuidShimToContext(vm);
+ await addPathShimToContext(vm);
+};
+
+module.exports = addLibraryShimsToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/nanoid.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/nanoid.js
new file mode 100644
index 000000000..7a83d37fe
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/nanoid.js
@@ -0,0 +1,24 @@
+const { nanoid } = require('nanoid');
+const { marshallToVm } = require('../../utils');
+
+const addNanoidShimToContext = async (vm) => {
+ let _nanoid = vm.newFunction('nanoid', function () {
+ let v = nanoid();
+ return marshallToVm(v, vm);
+ });
+ vm.setProp(vm.global, '__bruno__nanoid', _nanoid);
+ _nanoid.dispose();
+
+ vm.evalCode(
+ `
+ globalThis.nanoid = {};
+ globalThis.nanoid.nanoid = globalThis.__bruno__nanoid;
+ globalThis.requireObject = {
+ ...globalThis.requireObject,
+ 'nanoid': globalThis.nanoid
+ }
+ `
+ );
+};
+
+module.exports = addNanoidShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/path.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/path.js
new file mode 100644
index 000000000..8c9b2e02e
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/path.js
@@ -0,0 +1,28 @@
+const path = require('path');
+const { marshallToVm } = require('../../utils');
+
+const fns = ['resolve'];
+
+const addPathShimToContext = async (vm) => {
+ fns.forEach((fn) => {
+ let fnHandle = vm.newFunction(fn, function (...args) {
+ const nativeArgs = args.map(vm.dump);
+ return marshallToVm(path[fn](...nativeArgs), vm);
+ });
+ vm.setProp(vm.global, `__bruno__path__${fn}`, fnHandle);
+ fnHandle.dispose();
+ });
+
+ vm.evalCode(
+ `
+ globalThis.path = {};
+ ${fns?.map((fn, idx) => `globalThis.path.${fn} = __bruno__path__${fn}`).join('\n')}
+ globalThis.requireObject = {
+ ...(globalThis.requireObject || {}),
+ path: globalThis.path,
+ }
+ `
+ );
+};
+
+module.exports = addPathShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/lib/uuid.js b/packages/bruno-js/src/sandbox/quickjs/shims/lib/uuid.js
new file mode 100644
index 000000000..23f830311
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/lib/uuid.js
@@ -0,0 +1,30 @@
+const uuid = require('uuid');
+const { marshallToVm } = require('../../utils');
+
+const fns = ['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate'];
+
+const addUuidShimToContext = async (vm) => {
+ fns.forEach((fn) => {
+ let fnHandle = vm.newFunction(fn, function (...args) {
+ const nativeArgs = args.map(vm.dump);
+ return marshallToVm(uuid[fn](...nativeArgs), vm);
+ });
+ vm.setProp(vm.global, `__bruno__uuid__${fn}`, fnHandle);
+ fnHandle.dispose();
+ });
+
+ vm.evalCode(
+ `
+ globalThis.uuid = {};
+ ${['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']
+ ?.map((fn, idx) => `globalThis.uuid.${fn} = __bruno__uuid__${fn}`)
+ .join('\n')}
+ globalThis.requireObject = {
+ ...globalThis.requireObject,
+ uuid: globalThis.uuid,
+ }
+ `
+ );
+};
+
+module.exports = addUuidShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/local-module.js b/packages/bruno-js/src/sandbox/quickjs/shims/local-module.js
new file mode 100644
index 000000000..ca2b85730
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/local-module.js
@@ -0,0 +1,35 @@
+const path = require('path');
+const fs = require('fs');
+const { marshallToVm } = require('../utils');
+
+const addLocalModuleLoaderShimToContext = (vm, collectionPath) => {
+ let loadLocalModuleHandle = vm.newFunction('loadLocalModule', function (module) {
+ const filename = vm.dump(module);
+
+ // Check if the filename has an extension
+ const hasExtension = path.extname(filename) !== '';
+ const resolvedFilename = hasExtension ? filename : `${filename}.js`;
+
+ // Resolve the file path and check if it's within the collectionPath
+ const filePath = path.resolve(collectionPath, resolvedFilename);
+ const relativePath = path.relative(collectionPath, filePath);
+
+ // Ensure the resolved file path is inside the collectionPath
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+ throw new Error('Access to files outside of the collectionPath is not allowed.');
+ }
+
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Cannot find module ${filename}`);
+ }
+
+ let code = fs.readFileSync(filePath).toString();
+
+ return marshallToVm(code, vm);
+ });
+
+ vm.setProp(vm.global, '__brunoLoadLocalModule', loadLocalModuleHandle);
+ loadLocalModuleHandle.dispose();
+};
+
+module.exports = addLocalModuleLoaderShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/test.js b/packages/bruno-js/src/sandbox/quickjs/shims/test.js
new file mode 100644
index 000000000..9da224a39
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/test.js
@@ -0,0 +1,63 @@
+const { marshallToVm } = require('../utils');
+
+const addBruShimToContext = (vm, __brunoTestResults) => {
+ let addResult = vm.newFunction('addResult', function (v) {
+ __brunoTestResults.addResult(vm.dump(v));
+ });
+ vm.setProp(vm.global, '__bruno__addResult', addResult);
+ addResult.dispose();
+
+ let getResults = vm.newFunction('getResults', function () {
+ return marshallToVm(__brunoTestResults.getResults(), vm);
+ });
+ vm.setProp(vm.global, '__bruno__getResults', getResults);
+ getResults.dispose();
+
+ vm.evalCode(
+ `
+ globalThis.expect = require('chai').expect;
+ globalThis.assert = require('chai').assert;
+
+ globalThis.__brunoTestResults = {
+ addResult: globalThis.__bruno__addResult,
+ getResults: globalThis.__bruno__getResults,
+ }
+
+ globalThis.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {
+ constructor(message, props, ssf) {
+ super(message);
+ this.name = "AssertionError";
+ Object.assign(this, props);
+ }
+ }
+
+ globalThis.Test = (__brunoTestResults) => async (description, callback) => {
+ try {
+ await callback();
+ __brunoTestResults.addResult({ description, status: "pass" });
+ } catch (error) {
+ if (error instanceof DummyChaiAssertionError) {
+ const { message, actual, expected } = error;
+ __brunoTestResults.addResult({
+ description,
+ status: "fail",
+ error: message,
+ actual,
+ expected,
+ });
+ } else {
+ globalThis.__bruno__addResult({
+ description,
+ status: "fail",
+ error: error.message || "An unexpected error occurred.",
+ });
+ }
+ }
+ };
+
+ globalThis.test = Test(__brunoTestResults);
+ `
+ );
+};
+
+module.exports = addBruShimToContext;
diff --git a/packages/bruno-js/src/sandbox/quickjs/utils/index.js b/packages/bruno-js/src/sandbox/quickjs/utils/index.js
new file mode 100644
index 000000000..e376c3252
--- /dev/null
+++ b/packages/bruno-js/src/sandbox/quickjs/utils/index.js
@@ -0,0 +1,33 @@
+const marshallToVm = (value, vm) => {
+ if (value === undefined) {
+ return vm.undefined;
+ }
+ if (value === null) {
+ return vm.null;
+ }
+ if (typeof value === 'string') {
+ return vm.newString(value);
+ } else if (typeof value === 'number') {
+ return vm.newNumber(value);
+ } else if (typeof value === 'boolean') {
+ return value ? vm.true : vm.false;
+ } else if (typeof value === 'object') {
+ if (Array.isArray(value)) {
+ const arr = vm.newArray();
+ for (let i = 0; i < value.length; i++) {
+ vm.setProp(arr, i, marshallToVm(value[i], vm));
+ }
+ return arr;
+ } else {
+ const obj = vm.newObject();
+ for (const key in value) {
+ vm.setProp(obj, key, marshallToVm(value[key], vm));
+ }
+ return obj;
+ }
+ }
+};
+
+module.exports = {
+ marshallToVm
+};
diff --git a/packages/bruno-js/tests/runtime.spec.js b/packages/bruno-js/tests/runtime.spec.js
index 502cba27b..766569d03 100644
--- a/packages/bruno-js/tests/runtime.spec.js
+++ b/packages/bruno-js/tests/runtime.spec.js
@@ -35,7 +35,7 @@ describe('runtime', () => {
})
`;
- const runtime = new TestRuntime();
+ const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@@ -71,7 +71,7 @@ describe('runtime', () => {
})
`;
- const runtime = new TestRuntime();
+ const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@@ -114,9 +114,9 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
- const runtime = new ScriptRuntime();
+ const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);
- expect(result.collectionVariables.validation).toBeTruthy();
+ expect(result.runtimeVariables.validation).toBeTruthy();
});
});
@@ -160,7 +160,7 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
- const runtime = new ScriptRuntime();
+ const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runResponseScript(
script,
{ ...baseRequest },
@@ -171,7 +171,7 @@ describe('runtime', () => {
null,
process.env
);
- expect(result.collectionVariables.validation).toBeTruthy();
+ expect(result.runtimeVariables.validation).toBeTruthy();
});
});
});
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 4b0928604..9b00c330f 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -13,7 +13,7 @@ const stripLastLine = (text) => {
};
const getValueString = (value) => {
- const hasNewLines = value.includes('\n');
+ const hasNewLines = value?.includes('\n');
if (!hasNewLines) {
return value;
diff --git a/packages/bruno-query/package.json b/packages/bruno-query/package.json
index 140fdeafe..b7ff020bf 100644
--- a/packages/bruno-query/package.json
+++ b/packages/bruno-query/package.json
@@ -21,13 +21,13 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
- "rollup": "3.2.5",
+ "rollup":"3.29.4",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
- "rollup": "3.2.5"
+ "rollup":"3.29.4"
}
}
\ No newline at end of file
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 750fcaffb..0eb9492b5 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -48,7 +48,7 @@ const varsSchema = Yup.object({
const requestUrlSchema = Yup.string().min(0).defined();
const requestMethodSchema = Yup.string()
- .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])
+ .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'])
.required('method is required');
const graphqlBodySchema = Yup.object({
@@ -189,7 +189,8 @@ const authSchema = Yup.object({
oauth2: oauth2Schema.nullable()
})
.noUnknown(true)
- .strict();
+ .strict()
+ .nullable();
const requestParamsSchema = Yup.object({
uid: uidSchema,
@@ -232,6 +233,40 @@ const requestSchema = Yup.object({
.noUnknown(true)
.strict();
+const folderRootSchema = Yup.object({
+ request: Yup.object({
+ headers: Yup.array().of(keyValueSchema).nullable(),
+ auth: authSchema,
+ script: Yup.object({
+ req: Yup.string().nullable(),
+ res: Yup.string().nullable()
+ })
+ .noUnknown(true)
+ .strict()
+ .nullable(),
+ vars: Yup.object({
+ req: Yup.array().of(varsSchema).nullable(),
+ res: Yup.array().of(varsSchema).nullable()
+ })
+ .noUnknown(true)
+ .strict()
+ .nullable(),
+ tests: Yup.string().nullable()
+ })
+ .noUnknown(true)
+ .strict()
+ .nullable(),
+ docs: Yup.string().nullable(),
+ meta: Yup.object({
+ name: Yup.string().nullable()
+ })
+ .noUnknown(true)
+ .strict()
+ .nullable()
+})
+ .noUnknown(true)
+ .nullable();
+
const itemSchema = Yup.object({
uid: uidSchema,
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'),
@@ -249,6 +284,11 @@ const itemSchema = Yup.object({
// For all other types, the fileContent field is not required and can be null.
otherwise: Yup.string().nullable()
}),
+ root: Yup.mixed().when('type', {
+ is: 'folder',
+ then: folderRootSchema,
+ otherwise: Yup.mixed().nullable().notRequired()
+ }),
items: Yup.lazy(() => Yup.array().of(itemSchema)),
filename: Yup.string().nullable(),
pathname: Yup.string().nullable()
@@ -270,8 +310,9 @@ const collectionSchema = Yup.object({
runnerResult: Yup.object({
items: Yup.array()
}),
- collectionVariables: Yup.object(),
- brunoConfig: Yup.object()
+ runtimeVariables: Yup.object(),
+ brunoConfig: Yup.object(),
+ root: folderRootSchema
})
.noUnknown(true)
.strict();
diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js
index 87399c690..9fd223cb2 100644
--- a/packages/bruno-schema/src/collections/requestSchema.spec.js
+++ b/packages/bruno-schema/src/collections/requestSchema.spec.js
@@ -32,7 +32,7 @@ describe('Request Schema Validation', () => {
return Promise.all([
expect(requestSchema.validate(request)).rejects.toEqual(
validationErrorWithMessages(
- 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
+ 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE'
)
)
]);
diff --git a/packages/bruno-tests/collection/collection.bru b/packages/bruno-tests/collection/collection.bru
index ab9776995..d4b353eb8 100644
--- a/packages/bruno-tests/collection/collection.bru
+++ b/packages/bruno-tests/collection/collection.bru
@@ -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 🐶
diff --git a/packages/bruno-tests/collection/echo/echo bigint.bru b/packages/bruno-tests/collection/echo/echo bigint.bru
new file mode 100644
index 000000000..ef981c723
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo bigint.bru
@@ -0,0 +1,42 @@
+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
+}
diff --git a/packages/bruno-tests/collection/echo/echo json.bru b/packages/bruno-tests/collection/echo/echo json.bru
index 09a8ed90c..1749eda6b 100644
--- a/packages/bruno-tests/collection/echo/echo json.bru
+++ b/packages/bruno-tests/collection/echo/echo json.bru
@@ -1,7 +1,7 @@
meta {
name: echo json
type: http
- seq: 1
+ seq: 2
}
post {
diff --git a/packages/bruno-tests/collection/echo/echo plaintext.bru b/packages/bruno-tests/collection/echo/echo plaintext.bru
index e6c9b3fdc..56a23d345 100644
--- a/packages/bruno-tests/collection/echo/echo plaintext.bru
+++ b/packages/bruno-tests/collection/echo/echo plaintext.bru
@@ -1,7 +1,7 @@
meta {
name: echo plaintext
type: http
- seq: 2
+ seq: 3
}
post {
diff --git a/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru b/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru
new file mode 100644
index 000000000..d337cebb3
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru
@@ -0,0 +1,37 @@
+meta {
+ name: echo xml parsed(self closing tags)
+ type: http
+ seq: 6
+}
+
+post {
+ url: {{host}}/api/echo/xml-parsed
+ body: xml
+ auth: none
+}
+
+body:xml {
+
+ bruno
+
+
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return parsed xml", function() {
+ const data = res.getBody();
+ expect(res.getBody()).to.eql({
+ "hello": {
+ "world": [
+ "bruno",
+ ""
+ ]
+ }
+ });
+ });
+
+}
diff --git a/packages/bruno-tests/collection/echo/echo xml parsed.bru b/packages/bruno-tests/collection/echo/echo xml parsed.bru
index a8ff5e26a..586541664 100644
--- a/packages/bruno-tests/collection/echo/echo xml parsed.bru
+++ b/packages/bruno-tests/collection/echo/echo xml parsed.bru
@@ -1,7 +1,7 @@
meta {
name: echo xml parsed
type: http
- seq: 3
+ seq: 4
}
post {
diff --git a/packages/bruno-tests/collection/echo/echo xml raw.bru b/packages/bruno-tests/collection/echo/echo xml raw.bru
index 9773d4a3d..6a02ac238 100644
--- a/packages/bruno-tests/collection/echo/echo xml raw.bru
+++ b/packages/bruno-tests/collection/echo/echo xml raw.bru
@@ -1,7 +1,7 @@
meta {
name: echo xml raw
type: http
- seq: 4
+ seq: 5
}
post {
diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru
index e6286f3b6..4bea1e77a 100644
--- a/packages/bruno-tests/collection/environments/Prod.bru
+++ b/packages/bruno-tests/collection/environments/Prod.bru
@@ -5,4 +5,6 @@ vars {
env.var1: envVar1
env-var2: envVar2
bark: {{process.env.PROC_ENV_VAR}}
+ foo: bar
+ testSetEnvVar: bruno-29653
}
diff --git a/packages/bruno-tests/collection/lib/constants.js b/packages/bruno-tests/collection/lib/constants.js
new file mode 100644
index 000000000..2f0e62f37
--- /dev/null
+++ b/packages/bruno-tests/collection/lib/constants.js
@@ -0,0 +1,5 @@
+const PI = 3.14;
+
+module.exports = {
+ PI
+};
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/lib/math.js b/packages/bruno-tests/collection/lib/math.js
index da6a05ef3..c25446734 100644
--- a/packages/bruno-tests/collection/lib/math.js
+++ b/packages/bruno-tests/collection/lib/math.js
@@ -1,5 +1,9 @@
+const { PI } = require('./constants');
+
const sum = (a, b) => a + b;
+const areaOfCircle = (radius) => PI * radius * radius;
module.exports = {
- sum
+ sum,
+ areaOfCircle
};
diff --git a/packages/bruno-tests/collection/ping.bru b/packages/bruno-tests/collection/ping.bru
index bbefac464..3abc7a2d4 100644
--- a/packages/bruno-tests/collection/ping.bru
+++ b/packages/bruno-tests/collection/ping.bru
@@ -9,52 +9,3 @@ get {
body: none
auth: none
}
-
-auth:awsv4 {
- accessKeyId: a
- secretAccessKey: b
- sessionToken: c
- service: d
- region: e
- profileName: f
-}
-
-vars:pre-request {
- m4: true
- pong: pong
-}
-
-assert {
- res.status: eq 200
- res.responseTime: lte 2000
- res.body: eq {{pong}}
-}
-
-tests {
- test("should ping pong", function() {
- const data = res.getBody();
- expect(data).to.equal(bru.getRequestVar("pong"));
- });
-}
-
-docs {
- # API Documentation
-
- ## Introduction
-
- Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
-
- ## Authentication
-
- Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
-
- ### API Key
-
- To use API key authentication, include your API key in the request headers as follows:
-
- ```http
- GET /api/endpoint
- Host: api.example.com
- Authorization: Bearer YOUR_API_KEY
-
-}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/getEnvName.bru b/packages/bruno-tests/collection/scripting/api/bru/getEnvName.bru
new file mode 100644
index 000000000..4e7c37ec3
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/getEnvName.bru
@@ -0,0 +1,23 @@
+meta {
+ name: getEnvName
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ const envName = bru.getEnvName();
+ bru.setVar("testEnvName", envName);
+}
+
+tests {
+ test("should get env name in scripts", function() {
+ const testEnvName = bru.getVar("testEnvName");
+ expect(testEnvName).to.equal("Prod");
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/getEnvVar.bru b/packages/bruno-tests/collection/scripting/api/bru/getEnvVar.bru
new file mode 100644
index 000000000..6b0276415
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/getEnvVar.bru
@@ -0,0 +1,19 @@
+meta {
+ name: getEnvVar
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+tests {
+ test("should get env var in scripts", function() {
+ const host = bru.getEnvVar("host")
+ expect(host).to.equal("https://testbench-sanity.usebruno.com");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/bru/getProcessEnv.bru b/packages/bruno-tests/collection/scripting/api/bru/getProcessEnv.bru
new file mode 100644
index 000000000..b0836a504
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/getProcessEnv.bru
@@ -0,0 +1,19 @@
+meta {
+ name: getProcessEnv
+ type: http
+ seq: 6
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+tests {
+ test("bru.getProcessEnv()", function() {
+ const v = bru.getProcessEnv("PROC_ENV_VAR");
+ expect(v).to.equal("woof");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/bru/getVar.bru b/packages/bruno-tests/collection/scripting/api/bru/getVar.bru
new file mode 100644
index 000000000..96e7c365a
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/getVar.bru
@@ -0,0 +1,19 @@
+meta {
+ name: getVar
+ type: http
+ seq: 5
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+tests {
+ test("should get var in scripts", function() {
+ const testSetVar = bru.getVar("testSetVar");
+ expect(testSetVar).to.equal("bruno-test-87267");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/bru/setEnvVar.bru b/packages/bruno-tests/collection/scripting/api/bru/setEnvVar.bru
new file mode 100644
index 000000000..cd0e98151
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/setEnvVar.bru
@@ -0,0 +1,23 @@
+meta {
+ name: setEnvVar
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+script:post-response {
+ bru.setEnvVar("testSetEnvVar", "bruno-29653")
+}
+
+tests {
+ test("should set env var in scripts", function() {
+ const testSetEnvVar = bru.getEnvVar("testSetEnvVar")
+ expect(testSetEnvVar).to.equal("bruno-29653");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/bru/setVar.bru b/packages/bruno-tests/collection/scripting/api/bru/setVar.bru
new file mode 100644
index 000000000..a155117c9
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/setVar.bru
@@ -0,0 +1,22 @@
+meta {
+ name: setVar
+ type: http
+ seq: 4
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:post-response {
+ bru.setVar("testSetVar", "bruno-test-87267")
+}
+
+tests {
+ test("should get var in scripts", function() {
+ const testSetVar = bru.getVar("testSetVar");
+ expect(testSetVar).to.equal("bruno-test-87267");
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/api/req/getBody.bru b/packages/bruno-tests/collection/scripting/api/req/getBody.bru
new file mode 100644
index 000000000..926144ed7
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/getBody.bru
@@ -0,0 +1,40 @@
+meta {
+ name: getBody
+ type: http
+ seq: 9
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("req.getBody()", function() {
+ const data = res.getBody();
+ expect(data).to.eql({
+ "hello": "bruno"
+ });
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/req/getHeader.bru b/packages/bruno-tests/collection/scripting/api/req/getHeader.bru
new file mode 100644
index 000000000..77a2462cc
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/getHeader.bru
@@ -0,0 +1,28 @@
+meta {
+ name: getHeader
+ type: http
+ seq: 5
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+headers {
+ bruno: is-awesome
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+tests {
+ test("req.getHeader(name)", function() {
+ const h = req.getHeader('bruno');
+ expect(h).to.equal("is-awesome");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/getHeaders.bru b/packages/bruno-tests/collection/scripting/api/req/getHeaders.bru
new file mode 100644
index 000000000..3ab422615
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/getHeaders.bru
@@ -0,0 +1,30 @@
+meta {
+ name: getHeaders
+ type: http
+ seq: 7
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+headers {
+ bruno: is-awesome
+ della: is-beautiful
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+tests {
+ test("req.getHeaders()", function() {
+ const h = req.getHeaders();
+ expect(h.bruno).to.equal("is-awesome");
+ expect(h.della).to.equal("is-beautiful");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/getMethod.bru b/packages/bruno-tests/collection/scripting/api/req/getMethod.bru
new file mode 100644
index 000000000..eb730405c
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/getMethod.bru
@@ -0,0 +1,24 @@
+meta {
+ name: getMethod
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+tests {
+ test("req.getMethod()()", function() {
+ const method = req.getMethod();
+ expect(method).to.equal("GET");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/getUrl.bru b/packages/bruno-tests/collection/scripting/api/req/getUrl.bru
new file mode 100644
index 000000000..155a40b7a
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/getUrl.bru
@@ -0,0 +1,23 @@
+meta {
+ name: getUrl
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+tests {
+ test("req.getUrl()", function() {
+ const url = req.getUrl();
+ expect(url).to.equal("https://testbench-sanity.usebruno.com/ping");
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/api/req/setBody.bru b/packages/bruno-tests/collection/scripting/api/req/setBody.bru
new file mode 100644
index 000000000..ee609bd0b
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/setBody.bru
@@ -0,0 +1,46 @@
+meta {
+ name: setBody
+ type: http
+ seq: 10
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+script:pre-request {
+ req.setBody({
+ "bruno": "is awesome"
+ });
+}
+
+tests {
+ test("req.setBody()", function() {
+ const data = res.getBody();
+ expect(data).to.eql({
+ "bruno": "is awesome"
+ });
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/req/setHeader.bru b/packages/bruno-tests/collection/scripting/api/req/setHeader.bru
new file mode 100644
index 000000000..be33894c5
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/setHeader.bru
@@ -0,0 +1,32 @@
+meta {
+ name: setHeader
+ type: http
+ seq: 6
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+headers {
+ bruno: is-awesome
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+script:pre-request {
+ req.setHeader('bruno', 'is-the-future');
+}
+
+tests {
+ test("req.setHeader(name)", function() {
+ const h = req.getHeader('bruno');
+ expect(h).to.equal("is-the-future");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/setHeaders.bru b/packages/bruno-tests/collection/scripting/api/req/setHeaders.bru
new file mode 100644
index 000000000..b4d9532dc
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/setHeaders.bru
@@ -0,0 +1,37 @@
+meta {
+ name: setHeaders
+ type: http
+ seq: 8
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+headers {
+ bruno: is-awesome
+ della: is-beautiful
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+script:pre-request {
+ req.setHeaders({
+ "content-type": "application/text",
+ "transaction-id": "foobar"
+ });
+}
+
+tests {
+ test("req.setHeaders()", function() {
+ const h = req.getHeaders();
+ expect(h['content-type']).to.equal("application/text");
+ expect(h['transaction-id']).to.equal("foobar");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/setMethod.bru b/packages/bruno-tests/collection/scripting/api/req/setMethod.bru
new file mode 100644
index 000000000..45aa435bb
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/setMethod.bru
@@ -0,0 +1,28 @@
+meta {
+ name: setMethod
+ type: http
+ seq: 4
+}
+
+post {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+script:pre-request {
+ req.setMethod("GET");
+}
+
+tests {
+ test("req.setMethod()()", function() {
+ const method = req.getMethod();
+ expect(method).to.equal("GET");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/req/setUrl.bru b/packages/bruno-tests/collection/scripting/api/req/setUrl.bru
new file mode 100644
index 000000000..a0c429690
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/req/setUrl.bru
@@ -0,0 +1,28 @@
+meta {
+ name: setUrl
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{host}}/ping/invalid
+ body: none
+ auth: none
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+script:pre-request {
+ req.setUrl("https://testbench-sanity.usebruno.com/ping");
+}
+
+tests {
+ test("req.setUrl()", function() {
+ const url = req.getUrl();
+ expect(url).to.equal("https://testbench-sanity.usebruno.com/ping");
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/api/res/getBody.bru b/packages/bruno-tests/collection/scripting/api/res/getBody.bru
new file mode 100644
index 000000000..521d36a01
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getBody.bru
@@ -0,0 +1,40 @@
+meta {
+ name: getBody
+ type: http
+ seq: 4
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("res.getBody()", function() {
+ const data = res.getBody();
+ expect(data).to.eql({
+ "hello": "bruno"
+ });
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/res/getHeader.bru b/packages/bruno-tests/collection/scripting/api/res/getHeader.bru
new file mode 100644
index 000000000..1ab640726
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getHeader.bru
@@ -0,0 +1,38 @@
+meta {
+ name: getHeader
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("res.getHeader(name)", function() {
+ const server = res.getHeader('x-powered-by');
+ expect(server).to.eql('Express');
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/res/getHeaders.bru b/packages/bruno-tests/collection/scripting/api/res/getHeaders.bru
new file mode 100644
index 000000000..58dc7495d
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getHeaders.bru
@@ -0,0 +1,39 @@
+meta {
+ name: getHeaders
+ type: http
+ seq: 3
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("res.getHeaders(name)", function() {
+ const h = res.getHeaders();
+ expect(h['x-powered-by']).to.eql('Express');
+ expect(h['content-length']).to.eql('17');
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/res/getResponseTime.bru b/packages/bruno-tests/collection/scripting/api/res/getResponseTime.bru
new file mode 100644
index 000000000..236a5eff1
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getResponseTime.bru
@@ -0,0 +1,39 @@
+meta {
+ name: getResponseTime
+ type: http
+ seq: 5
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("res.getResponseTime()", function() {
+ const responseTime = res.getResponseTime();
+ expect(typeof responseTime).to.eql("number");
+ expect(responseTime > 0).to.be.true;
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/res/getStatus.bru b/packages/bruno-tests/collection/scripting/api/res/getStatus.bru
new file mode 100644
index 000000000..3c511754c
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getStatus.bru
@@ -0,0 +1,24 @@
+meta {
+ name: getStatus
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+
+assert {
+ res.status: eq 200
+ res.body: eq pong
+}
+
+tests {
+ test("res.getStatus()", function() {
+ const status = res.getStatus()
+ expect(status).to.equal(200);
+ });
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/collection/scripting/get-env-name.bru b/packages/bruno-tests/collection/scripting/get-env-name.bru
deleted file mode 100644
index bce2d1973..000000000
--- a/packages/bruno-tests/collection/scripting/get-env-name.bru
+++ /dev/null
@@ -1,54 +0,0 @@
-meta {
- name: get-env-name
- type: http
- seq: 1
-}
-
-get {
- url: {{host}}/ping
- body: none
- auth: none
-}
-
-auth:awsv4 {
- accessKeyId: a
- secretAccessKey: b
- sessionToken: c
- service: d
- region: e
- profileName: f
-}
-
-script:pre-request {
- const envName = bru.getEnvName();
- bru.setVar("testEnvName", envName);
-}
-
-tests {
- test("should get env name in scripts", function() {
- const testEnvName = bru.getVar("testEnvName");
- expect(testEnvName).to.equal("Prod");
- });
-}
-
-docs {
- # API Documentation
-
- ## Introduction
-
- Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
-
- ## Authentication
-
- Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
-
- ### API Key
-
- To use API key authentication, include your API key in the request headers as follows:
-
- ```http
- GET /api/endpoint
- Host: api.example.com
- Authorization: Bearer YOUR_API_KEY
-
-}
diff --git a/packages/bruno-tests/collection/scripting/get-env-var.bru b/packages/bruno-tests/collection/scripting/get-env-var.bru
deleted file mode 100644
index 5c9d8ec5d..000000000
--- a/packages/bruno-tests/collection/scripting/get-env-var.bru
+++ /dev/null
@@ -1,49 +0,0 @@
-meta {
- name: get-env-var
- type: http
- seq: 2
-}
-
-get {
- url: {{host}}/ping
- body: none
- auth: none
-}
-
-auth:awsv4 {
- accessKeyId: a
- secretAccessKey: b
- sessionToken: c
- service: d
- region: e
- profileName: f
-}
-
-tests {
- test("should get env var in scripts", function() {
- const host = bru.getEnvVar("host")
- expect(host).to.equal("https://testbench-sanity.usebruno.com");
- });
-}
-
-docs {
- # API Documentation
-
- ## Introduction
-
- Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
-
- ## Authentication
-
- Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
-
- ### API Key
-
- To use API key authentication, include your API key in the request headers as follows:
-
- ```http
- GET /api/endpoint
- Host: api.example.com
- Authorization: Bearer YOUR_API_KEY
-
-}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/axios/axios-pre-req-script.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/axios/axios-pre-req-script.bru
new file mode 100644
index 000000000..1998c9665
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/axios/axios-pre-req-script.bru
@@ -0,0 +1,34 @@
+meta {
+ name: axios-pre-req-script
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ const axios = require("axios");
+
+ const url = "https://testbench-sanity.usebruno.com/api/echo/json";
+ const response = await axios.post(url, {
+ "hello": "bruno"
+ });
+
+ req.setBody(response.data);
+ req.setMethod("POST");
+ req.setUrl(url);
+}
+
+tests {
+ test("req.getBody()", function() {
+ const data = res.getBody();
+ expect(data).to.eql({
+ "hello": "bruno"
+ });
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru
new file mode 100644
index 000000000..8385847c9
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru
@@ -0,0 +1,33 @@
+meta {
+ name: crypto-js-pre-request-script
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ var CryptoJS = require("crypto-js");
+
+ // Encrypt
+ var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();
+
+ // Decrypt
+ var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');
+ var originalText = bytes.toString(CryptoJS.enc.Utf8);
+
+ bru.setVar('crypto-test-message', originalText);
+}
+
+tests {
+ test("crypto message", function() {
+ const data = bru.getVar('crypto-test-message');
+ bru.setVar('crypto-test-message', null);
+ expect(data).to.eql('my message');
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/nanoid/nanoid.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/nanoid/nanoid.bru
new file mode 100644
index 000000000..14aa35172
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/nanoid/nanoid.bru
@@ -0,0 +1,26 @@
+meta {
+ name: nanoid
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ const { nanoid } = require("nanoid");
+
+ bru.setVar("nanoid-test-id", nanoid());
+}
+
+tests {
+ test("nanoid var", function() {
+ const id = bru.getVar('nanoid-test-id');
+ let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)
+ bru.setVar('nanoid-test-id', null);
+ expect(isValidNanoid).to.eql(true);
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/uuid/uuid.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/uuid/uuid.bru
new file mode 100644
index 000000000..ba0c2edb5
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/uuid/uuid.bru
@@ -0,0 +1,26 @@
+meta {
+ name: uuid
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ const { v4 } = require("uuid");
+
+ bru.setVar("uuid-test-id", v4());
+}
+
+tests {
+ test("uuid var", function() {
+ const id = bru.getVar('uuid-test-id');
+ let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);
+ bru.setVar('uuid-test-id', null);
+ expect(isValidUuid).to.eql(true);
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/js/data types - request vars.bru b/packages/bruno-tests/collection/scripting/js/data types - request vars.bru
new file mode 100644
index 000000000..a0f7c91a8
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/js/data types - request vars.bru
@@ -0,0 +1,44 @@
+meta {
+ name: data types - request vars
+ type: http
+ seq: 3
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "boolean": false,
+ "number_1": 1,
+ "number_2": 0,
+ "number_3": -1,
+ "string": "bruno",
+ "array": [1, 2, 3, 4, 5],
+ "object": {
+ "hello": "bruno"
+ },
+ "null": null
+ }
+}
+
+assert {
+ req.body.boolean: isBoolean false
+ req.body.number_1: isNumber 1
+ req.body.undefined: isUndefined undefined
+ req.body.string: isString bruno
+ req.body.null: isNull null
+ req.body.array: isArray
+ req.body.boolean: eq false
+ req.body.number_1: eq 1
+ req.body.undefined: eq undefined
+ req.body.string: eq bruno
+ req.body.null: eq null
+ req.body.number_2: eq 0
+ req.body.number_3: eq -1
+ req.body.number_2: isNumber
+ req.body.number_3: isNumber
+}
diff --git a/packages/bruno-tests/collection/scripting/js/data types.bru b/packages/bruno-tests/collection/scripting/js/data types.bru
new file mode 100644
index 000000000..a08c68b8f
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/js/data types.bru
@@ -0,0 +1,54 @@
+meta {
+ name: data types
+ type: http
+ seq: 2
+}
+
+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
+ }
+}
+
+script:pre-request {
+ const reqBody = req.getBody();
+
+ bru.setVar("dataTypeVarTest", {
+ ...reqBody,
+ "undefined": undefined
+ });
+}
+
+tests {
+ test("data types check via bru var", function() {
+ let v = bru.getVar("dataTypeVarTest");
+ v = {
+ ...v,
+ "undefined": undefined
+ };
+ expect(v).to.eql({
+ "boolean": false,
+ "number": 1,
+ "string": "bruno",
+ "array": [1, 2, 3, 4, 5],
+ "object": {
+ "hello": "bruno"
+ },
+ "null": null,
+ "undefined": undefined
+ })
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/js/setTimeout.bru b/packages/bruno-tests/collection/scripting/js/setTimeout.bru
new file mode 100644
index 000000000..8b136a113
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/js/setTimeout.bru
@@ -0,0 +1,32 @@
+meta {
+ name: setTimeout
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ bru.setVar("test-js-set-timeout", "");
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ bru.setVar("test-js-set-timeout", "bruno");
+ resolve();
+ }, 1000);
+ });
+
+ const v = bru.getVar("test-js-set-timeout");
+ bru.setVar("test-js-set-timeout", v + "-is-awesome");
+
+}
+
+tests {
+ test("setTimeout()", function() {
+ const v = bru.getVar("test-js-set-timeout")
+ expect(v).to.eql("bruno-is-awesome");
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/local modules/invalid and valid module imports.bru b/packages/bruno-tests/collection/scripting/local modules/invalid and valid module imports.bru
new file mode 100644
index 000000000..89c3ad23d
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/local modules/invalid and valid module imports.bru
@@ -0,0 +1,37 @@
+meta {
+ name: invalid and valid module imports
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+assert {
+ invalid_module_error_thrown: eq true
+ valid_module_no_error: eq true
+}
+
+script:pre-request {
+ try {
+ bru.setVar('invalid_module_error_thrown', false);
+ // should throw an error
+ const invalid = require("./lib/invalid");
+ }
+ catch(error) {
+ bru.setVar('invalid_module_error_thrown', true);
+ }
+
+
+ try {
+ bru.setVar('valid_module_no_error', true);
+ // should not throw an error
+ const math = require("./lib/math");
+ }
+ catch(error) {
+ bru.setVar('valid_module_no_error', false);
+ }
+}
diff --git a/packages/bruno-tests/collection/scripting/local modules/sum (without js extn).bru b/packages/bruno-tests/collection/scripting/local modules/sum (without js extn).bru
new file mode 100644
index 000000000..819d61c56
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/local modules/sum (without js extn).bru
@@ -0,0 +1,45 @@
+meta {
+ name: sum (without js extn)
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "a": 1,
+ "b": 2
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+script:pre-request {
+ const math = require("./lib/math");
+ console.log(math, 'math');
+
+ const body = req.getBody();
+ body.sum = math.sum(body.a, body.b);
+ body.areaOfCircle = math.areaOfCircle(2);
+
+ req.setBody(body);
+}
+
+tests {
+ test("should return json", function() {
+ const data = res.getBody();
+ expect(res.getBody()).to.eql({
+ "a": 1,
+ "b": 2,
+ "sum": 3,
+ "areaOfCircle": 12.56
+ });
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/local modules/sum.bru b/packages/bruno-tests/collection/scripting/local modules/sum.bru
index c0c9a1aeb..eeccab181 100644
--- a/packages/bruno-tests/collection/scripting/local modules/sum.bru
+++ b/packages/bruno-tests/collection/scripting/local modules/sum.bru
@@ -22,10 +22,9 @@ assert {
}
script:pre-request {
- const math = require("./lib/math");
-
+ const math = require("./lib/math.js");
const body = req.getBody();
- body.sum = body.a + body.b;
+ body.sum = math.sum(body.a, body.b);
req.setBody(body);
}
@@ -39,4 +38,22 @@ tests {
"sum": 3
});
});
+
+ test("should return json", function() {
+ const data = res.getBody();
+ expect(res.getBody()).to.eql({
+ "a": 1,
+ "b": 2,
+ "sum": 3
+ });
+ });
+
+ test("should return json", function() {
+ const data = res.getBody();
+ expect(res.getBody()).to.eql({
+ "a": 1,
+ "b": 2,
+ "sum": 3
+ });
+ });
}
diff --git a/packages/bruno-tests/collection/scripting/set-env-var.bru b/packages/bruno-tests/collection/scripting/set-env-var.bru
deleted file mode 100644
index f193dbe88..000000000
--- a/packages/bruno-tests/collection/scripting/set-env-var.bru
+++ /dev/null
@@ -1,54 +0,0 @@
-meta {
- name: set-env-var
- type: http
- seq: 3
-}
-
-get {
- url: {{host}}/ping
- body: none
- auth: none
-}
-
-auth:awsv4 {
- accessKeyId: a
- secretAccessKey: b
- sessionToken: c
- service: d
- region: e
- profileName: f
-}
-
-script:post-response {
- bru.setEnvVar("testSetEnvVar", "bruno-29653")
-}
-
-tests {
- test("should set env var in scripts", function() {
- const testSetEnvVar = bru.getEnvVar("testSetEnvVar")
- console.log(testSetEnvVar);
- expect(testSetEnvVar).to.equal("bruno-29653");
- });
-}
-
-docs {
- # API Documentation
-
- ## Introduction
-
- Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
-
- ## Authentication
-
- Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
-
- ### API Key
-
- To use API key authentication, include your API key in the request headers as follows:
-
- ```http
- GET /api/endpoint
- Host: api.example.com
- Authorization: Bearer YOUR_API_KEY
-
-}
diff --git a/packages/bruno-tests/collection/string interpolation/folder.bru b/packages/bruno-tests/collection/string interpolation/folder.bru
new file mode 100644
index 000000000..8aef24cb2
--- /dev/null
+++ b/packages/bruno-tests/collection/string interpolation/folder.bru
@@ -0,0 +1,8 @@
+meta {
+ name: string interpolation
+}
+
+vars:pre-request {
+ folder_pre_var: folder_pre_var_value
+ folder_pre_var_2: {{env.var1}}
+}
diff --git a/readme.md b/readme.md
index c55b2dd7a..045be6058 100644
--- a/readme.md
+++ b/readme.md
@@ -27,6 +27,7 @@
| [正體中文](docs/readme/readme_zhtw.md)
| [العربية](docs/readme/readme_ar.md)
| [日本語](docs/readme/readme_ja.md)
+| [ქართული](docs/readme/readme_ka.md)
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
@@ -42,14 +43,34 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
![bruno](assets/images/landing-2.png)
-### Golden Edition ✨
+## Golden Edition ✨
Majority of our features are free and open source.
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19**!
-### Installation
+## Table of Contents
+- [Installation](#installation)
+- [Features](#features)
+ - [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
+ - [Collaborate via Git 👩💻🧑💻](#collaborate-via-git-)
+- [Sponsors](#sponsors)
+ - [Gold Sponsors](#gold-sponsors)
+ - [Silver Sponsors](#silver-sponsors)
+ - [Bronze Sponsors](#bronze-sponsors)
+- [Important Links 📌](#important-links-)
+- [Showcase 🎥](#showcase-)
+- [Support ❤️](#support-%EF%B8%8F)
+- [Share Testimonials 📣](#share-testimonials-)
+- [Publishing to New Package Managers](#publishing-to-new-package-managers)
+- [Stay in touch 🌐](#stay-in-touch-)
+- [Trademark](#trademark)
+- [Contribute 👩💻🧑💻](#contribute-)
+- [Authors](#authors)
+- [License 📄](#license-)
+
+## Installation
Bruno is available as binary download [on our website](https://www.usebruno.com/downloads) for Mac, Windows and Linux.
@@ -85,6 +106,8 @@ sudo apt update
sudo apt install bruno
```
+## Features
+
### Run across multiple platforms 🖥️
![bruno](assets/images/run-anywhere.png)
@@ -95,7 +118,7 @@ Or any version control system of your choice
![bruno](assets/images/version-control.png)
-### Sponsors
+## Sponsors
#### Gold Sponsors
@@ -111,7 +134,7 @@ Or any version control system of your choice
-### Important Links 📌
+## Important Links 📌
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
@@ -122,32 +145,32 @@ Or any version control system of your choice
- [Download](https://www.usebruno.com/downloads)
- [GitHub Sponsors](https://github.com/sponsors/helloanoop).
-### Showcase 🎥
+## Showcase 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
-### Support ❤️
+## Support ❤️
If you like Bruno and want to support our opensource work, consider sponsoring us via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
-### Share Testimonials 📣
+## Share Testimonials 📣
If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)
-### Publishing to New Package Managers
+## Publishing to New Package Managers
Please see [here](publishing.md) for more information.
-### Stay in touch 🌐
+## Stay in touch 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno)
-### Trademark
+## Trademark
**Name**
@@ -157,13 +180,13 @@ Please see [here](publishing.md) for more information.
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
-### Contribute 👩💻🧑💻
+## Contribute 👩💻🧑💻
I am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md)
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
-### Authors
+## Authors
-### License 📄
+## License 📄
[MIT](license.md)