Compare commits

..

64 Commits

Author SHA1 Message Date
Anoop M D
0f1e330dc5 feat(#119): ui placeholders for basic and bearer auths 2023-09-29 02:11:04 +05:30
Anoop M D
51ee37cf96 feat(#119): bru lang support for basic and bearer auth 2023-09-29 01:35:22 +05:30
Anoop M D
a6b19605b5 Merge pull request #238 from jsoref/spelling
Spelling
2023-09-29 00:25:43 +05:30
Josh Soref
7ba471f26a spelling: serialization
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
f23dcf50a4 spelling: separator
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
86cda2cf5a spelling: sample
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
00b6e007af spelling: people
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
7313d1b4d7 spelling: occurred
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
8f803234ce spelling: javascript
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
76a743b74e spelling: interpreted
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
c623aa0909 spelling: header
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
3eb26834c7 spelling: github
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
64a5852227 spelling: evaluated
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
6471ca74c3 spelling: ephemeral
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 14:11:49 -04:00
Josh Soref
f77d955839 spelling: environments
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Josh Soref
9947a55b8d spelling: bottom
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Josh Soref
a71555725c spelling: being
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-09-28 13:24:00 -04:00
Anoop M D
c9ec6902a5 Merge pull request #234 from brahma-dev/main
Allow tabs in tablists to wrap.
2023-09-28 18:58:10 +05:30
Brahma Dev
c9c675e187 Allow tabs in tablists to wrap. 2023-09-28 13:11:21 +00:00
Anoop M D
0517b2685e fix(#233): bru cli fix for content header parsing issue 2023-09-28 18:31:42 +05:30
Anoop M D
5d01c0a765 chore: bump version to 0.16.2 2023-09-28 18:25:15 +05:30
Anoop M D
f3925923c9 fix(#233): fixed content type env var parsing issue 2023-09-28 18:24:10 +05:30
Anoop M D
6facdfd66b chore: bump version to v0.16.1 2023-09-28 10:27:45 +05:30
Anoop M D
0f211131b1 feat(#224): proxy config support in collection runner 2023-09-28 10:20:31 +05:30
Anoop M D
cd3b8a948e fix(#227): fixed json formatting issue 2023-09-28 10:15:54 +05:30
Anoop M D
f695036721 feat(#224): Bru CLI support for proxying requests 2023-09-28 05:26:09 +05:30
Anoop M D
3661fa7df3 chore: published libs 2023-09-28 05:17:59 +05:30
Anoop M D
559fcb0806 Merge pull request #225 from mirkogolze/feature/191-interpolate-header-names
#191 interpolate header names with variables
2023-09-28 04:38:37 +05:30
Anoop M D
d5da8a9e2f chore: bump version to v0.16.0 2023-09-28 04:34:34 +05:30
Anoop M D
a3050db6c4 fix(#216): fixed issue where .env vars were not injected into bru.getEnvVar() 2023-09-28 04:32:07 +05:30
Anoop M D
c27f090583 feat(#95): runner runs inside a tab of a collection view 2023-09-28 04:02:20 +05:30
Anoop M D
487dd73040 fix: fixed screen crash when collection was removed 2023-09-28 03:26:13 +05:30
Anoop M D
665428a2d0 feat(#224): proxy support feature - gui layer 2023-09-28 03:06:53 +05:30
Mirko Golze
6a2ba0f746 try other way to retrieve icon path for about window 2023-09-27 22:39:22 +02:00
Mirko Golze
36f9902f2e #191 interpolate header names with variables 2023-09-27 22:36:27 +02:00
Anoop M D
c0b7dad030 feat(#224): proxy support feature - electron layer 2023-09-28 00:58:05 +05:30
Anoop M D
8780d309ac feat: exposing chai library in script and test runtimes 2023-09-27 23:47:56 +05:30
Anoop M D
08c1563a7a chore: bump version to v0.15.3 2023-09-27 14:37:13 +05:30
Anoop M D
07ad1f9f60 fix(#217): Merge pull request #218 from tpyle/bug/no-environments
Adds fallback when no environments are defined
2023-09-27 14:35:26 +05:30
Thomas Pyle
8df6b241bb Adds fallback when no environments are defined 2023-09-26 19:39:46 -04:00
Anoop M D
50e0558d7d Merge pull request #215 from Cibico99/feature/XML-Format
Feature/xml format
2023-09-26 22:58:33 +05:30
Anoop M D
cbe84cc512 Merge pull request #213 from BrentShikoski/feature/license_all_modules
Add license to published npm modules.
2023-09-26 22:50:18 +05:30
Anoop M D
cbb975d81d Merge branch 'main' into feature/license_all_modules 2023-09-26 22:49:19 +05:30
Anoop M D
30ee472c40 release(#212): bru cli v0.9.0 2023-09-26 22:37:55 +05:30
Anoop M D
c7aecbea79 Merge pull request #212 from tpyle/feature/output-collection
Adds an option to collect output from cli runs
2023-09-26 22:09:57 +05:30
pedward99
b814c84411 Clean Up 2023-09-26 09:22:41 -04:00
Brent Shikoski
6306ad17c3 Add license information to modules. 2023-09-25 20:57:51 -05:00
Brent Shikoski
4b800e30e4 Merge branch 'usebruno:main' into feature/add_license_to_all_cli_dependent_modules 2023-09-25 19:37:10 -05:00
Thomas Pyle
89f418a114 Adds an option to collect output from cli runs 2023-09-25 17:48:53 -04:00
pedward99
9c8ef09d01 XML Format working 2023-09-25 13:13:14 -04:00
Brent Shikoski
83d354c25c Add license to modules the cli is dependent on.
- bruno-js
- bruno-lang
- bruno-query
2023-09-24 22:29:10 -05:00
Anoop M D
bb31ddc5d2 chore: release v0.15.2 2023-09-25 04:42:41 +05:30
Anoop M D
ff40178c8c fix(#210): fixing bruno libraries dep issues 2023-09-25 04:41:39 +05:30
Anoop M D
1c549f7faf fix: fixed issue related about-window dep breaking build 2023-09-25 02:54:36 +05:30
Anoop M D
eb6b75ff98 feat(#199): bru cli updates to load .env vars 2023-09-25 02:10:12 +05:30
Anoop M D
eb010adeac chore: added collection variables feature note 2023-09-25 01:09:25 +05:30
Anoop M D
7e5e22cfcf chore: release v0.15.0 2023-09-25 00:58:24 +05:30
Anoop M D
2515e78a10 feat(#200): req.setMaxRedirects() api 2023-09-25 00:09:29 +05:30
Anoop M D
511854369f feat(#205): collection properties dropdown 2023-09-24 23:53:31 +05:30
Anoop M D
18f185d37c chore: fixed env table styling issue 2023-09-24 23:31:48 +05:30
Anoop M D
7a0322d09e Merge pull request #209 from usebruno/feature/env-secrets
Feature/env secrets
2023-09-24 23:22:40 +05:30
Paul Edwards
aeb29393c5 Code Editor Mode and formatting 2023-07-19 20:06:37 -04:00
Paul Edwards
0866d33858 Merge branch 'main' of https://github.com/usebruno/bruno into main 2023-05-17 00:09:23 -04:00
Paul Edwards
ad905d1a0a XML Indenting with header check 2023-04-26 22:06:52 -04:00
98 changed files with 1808 additions and 362 deletions

View File

@@ -1,6 +1,6 @@
## Development
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
### Dependencies
* NodeJS v18

View File

@@ -18,7 +18,6 @@
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1",
"about-window": "^1.15.2",
"husky": "^8.0.3",
"jest": "^29.2.0",
"pretty-quick": "^3.1.3",
@@ -39,5 +38,6 @@
},
"overrides": {
"rollup": "3.2.5"
}
},
"dependencies": {}
}

View File

@@ -18,7 +18,7 @@
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.3.1",
"@usebruno/schema": "0.5.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -53,6 +53,7 @@
"sass": "^1.46.0",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19",
"xml-formatter": "^3.5.0",
"yup": "^0.32.11"
},
"devDependencies": {

View File

@@ -29,7 +29,7 @@ const BrunoSupport = ({ onClose }) => {
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
<span className="label ml-2">GitHub</span>
</a>
</div>
<div className="mt-2">

View File

@@ -80,7 +80,7 @@ export default class CodeEditor extends React.Component {
}
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -0,0 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.settings-label {
width: 80px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
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};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,190 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import StyledWrapper from './StyledWrapper';
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const formik = useFormik({
initialValues: {
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
},
validationSchema: Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https']),
hostname: Yup.string().max(1024),
port: Yup.number().min(0).max(65535),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
})
}),
onSubmit: (values) => {
onUpdate(values);
}
});
useEffect(() => {
formik.setValues({
enabled: proxyConfig.enabled || false,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
}
});
}, [proxyConfig]);
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center mr-4">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
http
</label>
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
https
</label>
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<input
id="auth.password"
type="text"
name="auth.password"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default ProxySettings;

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
table {
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
li {
background-color: ${(props) => props.theme.bg} !important;
}
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
import StyledWrapper from './StyledWrapper';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
return (
<StyledWrapper className="px-4 py-4">
<h1 className="font-semibold mb-4">Collection Settings</h1>
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
</StyledWrapper>
);
};
export default CollectionSettings;

View File

@@ -43,7 +43,7 @@ const Wrapper = styled.div`
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
}
}
}

View File

@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occured while created the environment'));
.catch(() => toast.error('An error occurred while created the environment'));
}
});

View File

@@ -14,7 +14,7 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
toast.success('Environment deleted successfully');
onClose();
})
.catch(() => toast.error('An error occured while deleting the environment'));
.catch(() => toast.error('An error occurred while deleting the environment'));
};
return (

View File

@@ -12,12 +12,14 @@ const Wrapper = styled.div`
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
padding: 4px 10px;
&:nth-child(1) {
width: 30%;
&:nth-child(1),
&:nth-child(4),
&:nth-child(5) {
width: 70px;
}
&:nth-child(3) {
width: 70px;
&:nth-child(2) {
width: 25%;
}
}

View File

@@ -23,7 +23,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
type: 'CHANGES_SAVED'
});
})
.catch(() => toast.error('An error occured while saving the changes'));
.catch(() => toast.error('An error occurred while saving the changes'));
};
const addVariable = () => {

View File

@@ -27,7 +27,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
toast.success('Environment renamed successfully');
onClose();
})
.catch(() => toast.error('An error occured while renaming the environment'));
.catch(() => toast.error('An error occurred while renaming the environment'));
}
});

View File

@@ -3,7 +3,7 @@ import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel }) => (
<div className="bruno-modal-header">
{title ? <div className="bruno-modal-heade-title">{title}</div> : null}
{title ? <div className="bruno-modal-header-title">{title}</div> : null}
{handleCancel ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×

View File

@@ -27,7 +27,7 @@ const Support = () => {
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
<span className="label ml-2">GitHub</span>
</a>
</div>
<div className="mt-2">

View File

@@ -0,0 +1,25 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
.auth-mode-selector {
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
border-radius: 3px;
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
.label-item {
padding: 0.2rem 0.6rem !important;
}
}
.caret {
color: rgb(140, 140, 140);
fill: rgb(140 140 140);
}
`;
export default Wrapper;

View File

@@ -0,0 +1,70 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
import { humanizeRequestAuthMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const AuthMode = ({ item, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>
);
});
const onModeChange = (value) => {
dispatch(
updateRequestAuthMode({
itemUid: item.uid,
collectionUid: collection.uid,
mode: value
})
);
};
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('basic');
}}
>
Basic Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('bearer');
}}
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Auth
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default AuthMode;

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const Wrapper = styled.div``;
export default Wrapper;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const onEdit = (value) => {
// dispatch(
// updateRequestBody({
// content: value,
// itemUid: item.uid,
// collectionUid: collection.uid
// })
// );
};
if (authMode === 'basic') {
return <div>Basic Auth</div>;
}
if (authMode === 'bearer') {
return <div>Bearer Token</div>;
}
return <StyledWrapper className="w-full">No Auth</StyledWrapper>;
};
export default RequestBody;

View File

@@ -114,7 +114,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
@@ -125,7 +125,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
Query
</div>

View File

@@ -40,7 +40,7 @@ const useGraphqlSchema = (endpoint, environment) => {
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error('Error occured while loading GraphQL Schema');
toast.error('Error occurred while loading GraphQL Schema');
});
};

View File

@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
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 Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
@@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'auth': {
return <Auth item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
@@ -62,7 +67,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
const getTabClassname = (tabName) => {
@@ -73,7 +78,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center tabs" role="tablist">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Query
</div>
@@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>
@@ -95,13 +103,16 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
</div>
{/* Moved to post mvp */}
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
{focusedTab.requestPaneTab === 'body' ? (
<div className="flex flex-grow justify-end items-center">
<RequestBodyMode item={item} collection={collection} />
</div>
) : null}
{focusedTab.requestPaneTab === 'auth' ? (
<div className="flex flex-grow justify-end items-center">
<AuthMode item={item} collection={collection} />
</div>
) : null}
</div>
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
{getTabPanel(focusedTab.requestPaneTab)}

View File

@@ -142,7 +142,7 @@ export default class QueryEditor extends React.Component {
}
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -72,19 +72,27 @@ const RequestHeaders = ({ item, collection }) => {
</thead>
<tbody>
{headers && headers.length
? headers.map((header, index) => {
? headers.map((header) => {
return (
<tr key={header.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
<SingleLineEditor
value={header.name}
className="mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'name')}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'name'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>

View File

@@ -14,6 +14,7 @@ import QueryUrl from 'components/RequestPane/QueryUrl';
import NetworkError from 'components/ResponsePane/NetworkError';
import RunnerResults from 'components/RunnerResults';
import VariablesEditor from 'components/VariablesEditor';
import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
@@ -111,7 +112,7 @@ const RequestTabPanel = () => {
}
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
return <div className="pb-4 px-4">An error occured!</div>;
return <div className="pb-4 px-4">An error occurred!</div>;
}
let collection = find(collections, (c) => c.uid === focusedTab.collectionUid);
@@ -119,8 +120,7 @@ const RequestTabPanel = () => {
return <div className="pb-4 px-4">Collection not found!</div>;
}
const showRunner = collection.showRunner;
if (showRunner) {
if (focusedTab.type === 'collection-runner') {
return <RunnerResults collection={collection} />;
}
@@ -128,6 +128,10 @@ const RequestTabPanel = () => {
return <VariablesEditor collection={collection} />;
}
if (focusedTab.type === 'collection-settings') {
return <CollectionSettings collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { uuid } from 'utils/common';
import { IconFiles, IconRun, IconEye } from '@tabler/icons';
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 { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
const CollectionToolBar = ({ collection }) => {
@@ -12,8 +11,10 @@ const CollectionToolBar = ({ collection }) => {
const handleRun = () => {
dispatch(
toggleRunnerView({
collectionUid: collection.uid
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
};
@@ -28,6 +29,16 @@ const CollectionToolBar = ({ collection }) => {
);
};
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
return (
<StyledWrapper>
<div className="flex items-center p-2">
@@ -42,6 +53,9 @@ const CollectionToolBar = ({ collection }) => {
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</span>
<span className="mr-3">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
</span>
<EnvironmentSelector collection={collection} />
</div>
</div>

View File

@@ -1,13 +1,39 @@
import React from 'react';
import { IconVariable } from '@tabler/icons';
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type }) => {
const getTabInfo = (type) => {
switch (type) {
case 'collection-settings': {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Settings</span>
</>
);
}
case 'variables': {
return (
<>
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Variables</span>
</>
);
}
case 'collection-runner': {
return (
<>
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Runner</span>
</>
);
}
}
};
const SpecialTab = ({ handleCloseClick, text }) => {
return (
<>
<div className="flex items-center tab-label pl-2">
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">{text}</span>
</div>
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path

View File

@@ -57,10 +57,10 @@ const RequestTab = ({ tab, collection }) => {
return color;
};
if (tab.type === 'variables') {
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
<SpecialTab handleCloseClick={handleCloseClick} text="Variables" />
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
</StyledWrapper>
);
}

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
border-bottom: 1px solid ${(props) => props.theme.requestTabs.borromBorder};
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
ul {
padding: 0;

View File

@@ -76,9 +76,7 @@ const RequestTabs = () => {
});
};
const showRunner = activeCollection && activeCollection.showRunner;
// Todo: Must support ephermal requests
// Todo: Must support ephemeral requests
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && (
@@ -87,72 +85,70 @@ const RequestTabs = () => {
{collectionRequestTabs && collectionRequestTabs.length ? (
<>
<CollectionToolBar collection={activeCollection} />
{!showRunner ? (
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li
key={tab.uid}
className={getTabClassname(tab, index)}
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li>
) : null}
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
<div className="flex items-center pl-4">
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={leftSlide}>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
<IconChevronLeft size={18} strokeWidth={1.5} />
</div>
</li>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
) : null}
{/* Moved to post mvp */}
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
<div className="flex items-center home-icon-container">
<IconHome2 size={18} strokeWidth={1.5}/>
</div>
</li> */}
</ul>
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
{collectionRequestTabs && collectionRequestTabs.length
? collectionRequestTabs.map((tab, index) => {
return (
<li
key={tab.uid}
className={getTabClassname(tab, index)}
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
</li>
);
})
: null}
</ul>
<ul role="tablist">
{showChevrons ? (
<li className="select-none short-tab" onClick={rightSlide}>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
<IconChevronRight size={18} strokeWidth={1.5} />
</div>
</li> */}
</ul>
</div>
) : null}
</li>
) : null}
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
<div className="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
</div>
</li>
{/* Moved to post mvp */}
{/* <li className="select-none new-tab choose-request">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</div>
</li> */}
</ul>
</div>
</>
) : null}
</StyledWrapper>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { safeStringifyJSON } from 'utils/common';
import { useDispatch, useSelector } from 'react-redux';
import { getContentType, formatResponse } from 'utils/common';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
@@ -41,9 +41,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
item={item}
collection={collection}
width={rightPaneWidth}
value={
response.data ? (isJson(response.headers) ? safeStringifyJSON(response.data, true) : response.data) : ''
}
value={response.data ? formatResponse(response) : ''}
mode={getContentType(response.headers)}
/>
);
@@ -95,31 +93,13 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
});
};
const getContentType = (headers) => {
if (headers && headers.length) {
let contentType = headers
.filter((header) => header[0].toLowerCase() === 'content-type')
.map((header) => {
return header[1];
});
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
}
}
return '';
};
const isJson = (headers) => {
return getContentType(headers) === 'application/ld+json';
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex items-center px-3 tabs" role="tablist">
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
Response
</div>

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { useDispatch } from 'react-redux';
import { get, each, cloneDeep } from 'lodash';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { closeCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
import slash from 'utils/common/slash';
@@ -69,9 +69,9 @@ export default function RunnerResults({ collection }) {
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
};
const closeRunner = () => {
const resetRunner = () => {
dispatch(
closeCollectionRunner({
resetCollectionRunner({
collectionUid: collection.uid
})
);
@@ -101,8 +101,8 @@ export default function RunnerResults({ collection }) {
Run Collection
</button>
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
Close
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
Reset
</button>
</StyledWrapper>
);
@@ -202,8 +202,8 @@ export default function RunnerResults({ collection }) {
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" onClick={runCollection}>
Run Collection
</button>
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={closeRunner}>
Close
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
Reset
</button>
</div>
) : null}

View File

@@ -28,7 +28,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
onClose();
})
.catch((err) => {
toast.error(err ? err.message : 'An error occured while cloning the request');
toast.error(err ? err.message : 'An error occurred while cloning the request');
});
}
});

View File

@@ -1,9 +1,10 @@
import React from 'react';
import get from 'lodash/get';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
@@ -12,8 +13,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const onSubmit = (recursive) => {
dispatch(
showRunnerView({
collectionUid: collection.uid
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));

View File

@@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -86,11 +86,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
const handleClick = (event) => {
dispatch(
hideRunnerView({
collectionUid: collection.uid
})
);
if (isItemARequest(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(

View File

@@ -0,0 +1,50 @@
import React from 'react';
import Modal from 'components/Modal';
function countRequests(items) {
let count = 0;
function recurse(item) {
if (item && typeof item === 'object') {
if (item.type !== 'folder') {
count++;
}
if (Array.isArray(item.items)) {
item.items.forEach(recurse);
}
}
}
items.forEach(recurse);
return count;
}
const CollectionProperties = ({ collection, onClose }) => {
return (
<Modal size="sm" title="Collection Properties" hideFooter={true} handleCancel={onClose}>
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-right">Name&nbsp;:</td>
<td className="py-2 px-2">{collection.name}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Location&nbsp;:</td>
<td className="py-2 px-2">{collection.pathname}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Environments&nbsp;:</td>
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Requests&nbsp;:</td>
<td className="py-2 px-2">{countRequests(collection.items)}</td>
</tr>
</tbody>
</table>
</Modal>
);
};
export default CollectionProperties;

View File

@@ -13,7 +13,7 @@ const RemoveCollection = ({ onClose, collection }) => {
toast.success('Collection removed');
onClose();
})
.catch(() => toast.error('An error occured while removing the collection'));
.catch(() => toast.error('An error occurred while removing the collection'));
};
return (

View File

@@ -1,5 +1,6 @@
import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import { useDrop } from 'react-dnd';
@@ -8,11 +9,12 @@ import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import RunCollectionItem from './CollectionItem/RunCollectionItem';
import CollectionProperties from './CollectionProperties';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
import exportCollection from 'utils/collections/export';
@@ -25,7 +27,7 @@ const Collection = ({ collection, searchText }) => {
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [showRunCollectionModal, setShowRunCollectionModal] = useState(false);
const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false);
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
const dispatch = useDispatch();
@@ -39,6 +41,16 @@ const Collection = ({ collection, searchText }) => {
);
});
const handleRun = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-runner'
})
);
};
useEffect(() => {
if (searchText && searchText.length) {
setCollectionIsCollapsed(false);
@@ -103,8 +115,8 @@ const Collection = ({ collection, searchText }) => {
{showRemoveCollectionModal && (
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
)}
{showRunCollectionModal && (
<RunCollectionItem collection={collection} onClose={() => setShowRunCollectionModal(false)} />
{collectionPropertiesModal && (
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
)}
<div className="flex py-1 collection-name items-center" ref={drop}>
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
@@ -142,7 +154,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowRunCollectionModal(true);
handleRun();
}}
>
Run
@@ -165,6 +177,15 @@ const Collection = ({ collection, searchText }) => {
>
Export
</div>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setCollectionPropertiesModal(true);
}}
>
Properties
</div>
<div
className="dropdown-item"
onClick={(e) => {

View File

@@ -19,7 +19,7 @@ const CreateOrOpenCollection = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};
const CreateLink = () => (

View File

@@ -36,7 +36,7 @@ const CreateCollection = ({ onClose }) => {
toast.success('Collection created');
onClose();
})
.catch(() => toast.error('An error occured while creating the collection'));
.catch(() => toast.error('An error occurred while creating the collection'));
}
});

View File

@@ -32,7 +32,7 @@ const NewFolder = ({ collection, item, onClose }) => {
onSubmit: (values) => {
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
}
});

View File

@@ -5,14 +5,14 @@ import toast from 'react-hot-toast';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
@@ -34,10 +34,10 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
})
}),
onSubmit: (values) => {
if (isEphermal) {
if (isEphemeral) {
const uid = uuid();
dispatch(
newEphermalHttpRequest({
newEphemeralHttpRequest({
uid: uid,
requestName: values.requestName,
requestType: values.requestType,
@@ -56,7 +56,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
);
onClose();
})
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else {
dispatch(
newHttpRequest({
@@ -69,7 +69,7 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
})
)
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occured while adding the request'));
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
}
}
});

View File

@@ -46,7 +46,7 @@ const TitleBar = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};

View File

@@ -116,7 +116,7 @@ const Sidebar = () => {
</GitHubButton>
)}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.14.1</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.2</div>
</div>
</div>
</div>

View File

@@ -88,7 +88,7 @@ class SingleLineEditor extends Component {
};
componentDidUpdate(prevProps) {
// Ensure the changes caused by this update are not interpretted as
// Ensure the changes caused by this update are not interpreted as
// user-input changes which could otherwise result in an infinite
// event loop.
this.ignoreChangeEvent = true;

View File

@@ -34,7 +34,7 @@ const EnvVariables = ({ collection, theme }) => {
return (
<>
<h1 className="font-semibold mt-4 mb-2">Environment Variables</h1>
<div className="muted">No environment selected</div>
<div className="muted text-xs">No environment selected</div>
</>
);
}
@@ -55,7 +55,7 @@ const EnvVariables = ({ collection, theme }) => {
{enabledEnvVars.length > 0 ? (
<KeyValueExplorer data={envVarsObj} theme={theme} />
) : (
<div className="muted">No environment variables found</div>
<div className="muted text-xs">No environment variables found</div>
)}
</>
);
@@ -70,7 +70,7 @@ const CollectionVariables = ({ collection, theme }) => {
{collectionVariablesFound ? (
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
) : (
<div className="muted">No collection variables found</div>
<div className="muted text-xs">No collection variables found</div>
)}
</>
);
@@ -85,6 +85,12 @@ const VariablesEditor = ({ collection }) => {
<StyledWrapper className="px-4 py-4">
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
<EnvVariables collection={collection} theme={reactInspectorTheme} />
<div className="mt-8 muted text-xs">
Note: As of today, collection variables can only be set via the api -{' '}
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
In the next release, we will add a UI to set and modify collection variables.
</div>
</StyledWrapper>
);
};

View File

@@ -19,7 +19,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occured while opening the collection')
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
);
};
@@ -93,7 +93,7 @@ const Welcome = () => {
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">Github</span>
<span className="label ml-2">GitHub</span>
</a>
</div>
</div>

View File

@@ -11,7 +11,8 @@ import {
processEnvUpdateEvent,
collectionRenamedEvent,
runRequestEvent,
runFolderEvent
runFolderEvent,
brunoConfigUpdateEvent
} from 'providers/ReduxStore/slices/collections';
import toast from 'react-hot-toast';
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
@@ -27,8 +28,8 @@ const useCollectionTreeSync = () => {
const { ipcRenderer } = window;
const _openCollection = (pathname, uid, name) => {
dispatch(openCollectionEvent(uid, pathname, name));
const _openCollection = (pathname, uid, brunoConfig) => {
dispatch(openCollectionEvent(uid, pathname, brunoConfig));
};
const _collectionTreeUpdated = (type, val) => {
@@ -128,6 +129,7 @@ const useCollectionTreeSync = () => {
const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args);
});
const removeListener11 = ipcRenderer.on('main:bruno-config-update', (val) => dispatch(brunoConfigUpdateEvent(val)));
return () => {
removeListener1();
@@ -140,6 +142,7 @@ const useCollectionTreeSync = () => {
removeListener8();
removeListener9();
removeListener10();
removeListener11();
};
}, [isElectron]);
};

View File

@@ -93,7 +93,7 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid, tabs, saveRequest, collections]);
// edit environmentss (ctrl/cmd + e)
// edit environments (ctrl/cmd + e)
useEffect(() => {
Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);

View File

@@ -42,8 +42,8 @@ import {
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
} from './index';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { isLocalCollection, resolveRequestFilename } from 'utils/common/platform';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
const PATH_SEPARATOR = path.sep;
@@ -723,11 +723,7 @@ export const removeCollection = (collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:remove-collection', collection.pathname)
.then(() => {
dispatch(
closeTabs({
tabUids: recursivelyGetAllItemUids(collection.items)
})
);
dispatch(closeAllCollectionTabs({ collectionUid }));
})
.then(waitForNextTick)
.then(() => {
@@ -750,15 +746,31 @@ export const browseDirectory = () => (dispatch, getState) => {
});
};
export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState) => {
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
return reject(new Error('Collection not found'));
}
return new Promise((resolve, reject) => {
ipcRenderer
.invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collectionUid)
.then(resolve)
.catch(reject);
});
};
export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {
const collection = {
version: '1',
uid: uid,
name: name,
name: brunoConfig.name,
pathname: pathname,
items: [],
showRunner: false,
collectionVariables: {}
collectionVariables: {},
brunoConfig: brunoConfig
};
return new Promise((resolve, reject) => {

View File

@@ -52,6 +52,14 @@ export const collectionsSlice = createSlice({
state.collections.push(collection);
}
},
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.brunoConfig = brunoConfig;
}
},
renameCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -221,7 +229,7 @@ export const collectionsSlice = createSlice({
}
}
},
newEphermalHttpRequest: (state, action) => {
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection && collection.items && collection.items.length) {
@@ -555,6 +563,20 @@ export const collectionsSlice = createSlice({
}
}
},
updateRequestAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection && collection.items && collection.items.length) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.auth.mode = action.payload.mode;
}
}
},
updateRequestBodyMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1008,30 +1030,6 @@ export const collectionsSlice = createSlice({
collection.name = newName;
}
},
toggleRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = !collection.showRunner;
}
},
showRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = true;
}
},
hideRunnerView: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.showRunner = false;
}
},
resetRunResults: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -1141,13 +1139,12 @@ export const collectionsSlice = createSlice({
}
}
},
closeCollectionRunner: (state, action) => {
resetCollectionRunner: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.runnerResult = null;
collection.showRunner = false;
}
}
}
@@ -1155,6 +1152,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
brunoConfigUpdateEvent,
renameCollection,
removeCollection,
updateLastAction,
@@ -1170,7 +1168,7 @@ export const {
requestCancelled,
responseReceived,
saveRequest,
newEphermalHttpRequest,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
@@ -1186,6 +1184,7 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
updateRequestBody,
updateRequestGraphqlQuery,
@@ -1207,13 +1206,10 @@ export const {
collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent,
collectionRenamedEvent,
toggleRunnerView,
showRunnerView,
hideRunnerView,
resetRunResults,
runRequestEvent,
runFolderEvent,
closeCollectionRunner
resetCollectionRunner
} = collectionsSlice.actions;
export default collectionsSlice.reducer;

View File

@@ -10,6 +10,10 @@ const initialState = {
activeTabUid: null
};
const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
};
export const tabsSlice = createSlice({
name: 'tabs',
initialState,
@@ -20,8 +24,8 @@ export const tabsSlice = createSlice({
return;
}
if (action.payload.type === 'variables') {
const tab = find(state.tabs, (t) => t.collectionUid === action.payload.collectionUid && t.type === 'variables');
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;
return;
@@ -92,11 +96,23 @@ export const tabsSlice = createSlice({
if (!state.tabs || !state.tabs.length) {
state.activeTabUid = null;
}
},
closeAllCollectionTabs: (state, action) => {
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
state.activeTabUid = null;
}
}
});
export const { addTab, focusTab, updateRequestPaneTabWidth, updateRequestPaneTab, updateResponsePaneTab, closeTabs } =
tabsSlice.actions;
export const {
addTab,
focusTab,
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
closeAllCollectionTabs
} = tabsSlice.actions;
export default tabsSlice.reducer;

View File

@@ -70,7 +70,7 @@ const darkTheme = {
bg: 'rgb(48, 48, 49)',
hoverBg: '#185387',
shadow: 'rgb(0 0 0 / 36%) 0px 2px 8px',
seperator: '#444',
separator: '#444',
labelBg: '#4a4949'
},
@@ -174,7 +174,7 @@ const darkTheme = {
requestTabs: {
color: '#ccc',
bg: '#2A2D2F',
borromBorder: '#444',
bottomBorder: '#444',
icon: {
color: '#9f9f9f',
hoverColor: 'rgb(204, 204, 204)',

View File

@@ -70,7 +70,7 @@ const lightTheme = {
bg: '#fff',
hoverBg: '#e9e9e9',
shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',
seperator: '#e7e7e7',
separator: '#e7e7e7',
labelBg: '#f3f3f3'
},
@@ -178,7 +178,7 @@ const lightTheme = {
requestTabs: {
color: 'rgb(52, 52, 52)',
bg: '#f7f7f7',
borromBorder: '#efefef',
bottomBorder: '#efefef',
icon: {
color: '#9f9f9f',
hoverColor: 'rgb(76 76 76)',

View File

@@ -445,6 +445,22 @@ export const humanizeRequestBodyMode = (mode) => {
return label;
};
export const humanizeRequestAuthMode = (mode) => {
let label = 'No Auth';
switch (mode) {
case 'basic': {
label = 'Basic Auth';
break;
}
case 'bearer': {
label = 'Bearer Token';
break;
}
}
return label;
};
export const refreshUidsInItem = (item) => {
item.uid = uuid();

View File

@@ -1,4 +1,5 @@
import { customAlphabet } from 'nanoid';
import xmlFormat from 'xml-formatter';
// a customized version of nanoid without using _ and -
export const uuid = () => {
@@ -61,3 +62,32 @@ export const normalizeFileName = (name) => {
return formattedName;
};
export const getContentType = (headers) => {
if (headers && headers.length) {
let contentType = headers
.filter((header) => header[0].toLowerCase() === 'content-type')
.map((header) => {
return header[1];
});
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
}
}
return '';
};
export const formatResponse = (response) => {
let type = getContentType(response.headers);
if (type.includes('json')) {
return safeStringifyJSON(response.data, true);
}
if (type.includes('xml')) {
return xmlFormat(response.data, { collapseContent: true });
}
return response.data;
};

View File

@@ -1,9 +1,26 @@
# Changelog
## 0.10.1
- fix(#233) Fixed Issue related to content header parsing
## 0.10.0
- Support for proxying requests through a proxy server
## 0.9.0
- `--output` flag to collect the results of your API tests
## 0.8.0
- `--env-var` flag to set environment variables
- loading environment variables from `.env` file
## 0.7.1
* `--cacert` flag to support custom CA certificates
- `--cacert` flag to support custom CA certificates
## 0.7.0
* `--insecure` flag to disable SSL verification
- `--insecure` flag to disable SSL verification

View File

@@ -0,0 +1,238 @@
{
"summary": {
"totalAssertions": 4,
"passedAssertions": 4,
"failedAssertions": 0,
"totalTests": 0,
"passedTests": 0,
"failedTests": 0
},
"results": [
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v4",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-08gGpUcq2NTnMCVT5AuXxQ0DzGE\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v4",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "mTrKBl5YU6jiAVG-phKT4",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v2",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-lMqxZgVOJiQXjF5yk3AFEU8O9Ro\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v2",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "XsjjGx9cjt5t8tE_t69ZB",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "GET",
"url": "http://localhost:8080/test/v3",
"headers": {}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "497",
"etag": "W/\"1f1-tSiYu0/vWz3r+NYRCaed0aW1waw\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v3",
"headers": {
"accept": "application/json, text/plain, */*",
"user-agent": "axios/1.5.0",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {}
}
},
"assertionResults": [
{
"uid": "i_8MmDMtJA9YfvB_FrW15",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
},
{
"request": {
"method": "POST",
"url": "http://localhost:8080/test/v1",
"headers": {
"content-type": "application/json"
},
"data": {
"test": "hello"
}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"x-powered-by": "Express",
"content-type": "application/json; charset=utf-8",
"content-length": "623",
"etag": "W/\"26f-ku5QGz4p9f02u79vJIve7JH3QYM\"",
"date": "Mon, 25 Sep 2023 21:43:02 GMT",
"connection": "close"
},
"data": {
"path": "/test/v1",
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
"user-agent": "axios/1.5.0",
"content-length": "16",
"accept-encoding": "gzip, compress, deflate, br",
"host": "localhost:8080",
"connection": "close"
},
"method": "POST",
"body": "{\"test\":\"hello\"}",
"fresh": false,
"hostname": "localhost",
"ip": "",
"ips": [],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "05512cb2102c"
},
"connection": {},
"json": {
"test": "hello"
}
}
},
"assertionResults": [
{
"uid": "hNBSF_GBdSTFHNiyCcOn9",
"lhsExpr": "res.status",
"rhsExpr": "200",
"rhsOperand": "200",
"operator": "eq",
"status": "pass"
}
],
"testResults": []
}
]
}

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/cli",
"version": "0.7.1",
"version": "0.10.1",
"license": "MIT",
"main": "src/index.js",
"bin": {
"bru": "./bin/bru.js"
@@ -20,13 +21,14 @@
"package.json"
],
"dependencies": {
"@usebruno/js": "0.4.0",
"@usebruno/lang": "0.3.0",
"axios": "^1.3.2",
"@usebruno/js": "0.6.0",
"@usebruno/lang": "0.4.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"handlebars": "^4.7.8",
"inquirer": "^9.1.4",
"lodash": "^4.17.21",
"mustache": "^4.2.0",

View File

@@ -5,16 +5,21 @@ With Bruno CLI, you can now run your API collections with ease using simple comm
This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.
## Installation
To install the Bruno CLI, use the node package manager of your choice, such as NPM:
```bash
npm install -g @usebruno/cli
```
## Getting started
Navigate to the directory where your API collection resides, and then run:
```bash
bru run
```
This command will run all the requests in your collection. You can also run a single request by specifying its filename:
```bash
@@ -22,25 +27,37 @@ bru run request.bru
```
Or run all requests in a collection's subfolder:
```bash
bru run folder
```
If you need to use an environment, you can specify it with the --env option:
```bash
bru run folder --env Local
```
If you need to collect the results of your API tests, you can specify the --output option:
```bash
bru run folder --output results.json
```
## Demo
![demo](assets/images/cli-demo.png)
## Support
If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)
Thank you for using Bruno CLI!
## Changelog
See [here](packages/bruno-cli/changelog.md)
## License
[MIT](license.md)
[MIT](license.md)

View File

@@ -1,11 +1,13 @@
const fs = require('fs');
const chalk = require('chalk');
const path = require('path');
const { forOwn } = require('lodash');
const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
const { rpad } = require('../utils/common');
const { bruToJson, getOptions } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -125,6 +127,11 @@ const builder = async (yargs) => {
describe: 'Overwrite a single environment variable, multiple usages possible',
type: 'string'
})
.option('output', {
alias: 'o',
describe: 'Path to write JSON results to',
type: 'string'
})
.option('insecure', {
type: 'boolean',
description: 'Allow insecure server connections'
@@ -136,12 +143,16 @@ const builder = async (yargs) => {
.example(
'$0 run request.bru --env local --env-var secret=xxx',
'Run a request with the environment set to local and overwrite the variable secret with value xxx'
)
.example(
'$0 run request.bru --output results.json',
'Run a request and write the results to results.json in the current directory'
);
};
const handler = async function (argv) {
try {
let { filename, cacert, env, envVar, insecure, r: recursive } = argv;
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
const collectionPath = process.cwd();
// todo
@@ -154,6 +165,9 @@ const handler = async function (argv) {
return;
}
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
const brunoConfig = JSON.parse(brunoConfigFile);
if (filename && filename.length) {
const pathExists = await exists(filename);
if (!pathExists) {
@@ -225,30 +239,40 @@ const handler = async function (argv) {
}
}
// load .env file at root of collection if it exists
const dotEnvPath = path.join(collectionPath, '.env');
const dotEnvExists = await exists(dotEnvPath);
const processEnvVars = {
...process.env
};
if (dotEnvExists) {
const content = fs.readFileSync(dotEnvPath, 'utf8');
const jsonData = dotenvToJson(content);
forOwn(jsonData, (value, key) => {
processEnvVars[key] = value;
});
}
const _isFile = await isFile(filename);
let assertionResults = [];
let testResults = [];
let testrunResults = [];
let bruJsons = [];
if (_isFile) {
console.log(chalk.yellow('Running Request \n'));
const bruContent = fs.readFileSync(filename, 'utf8');
const bruJson = bruToJson(bruContent);
const result = await runSingleRequest(filename, bruJson, collectionPath, collectionVariables, envVars);
if (result) {
const { assertionResults, testResults } = result;
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Done.')));
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
process.exit(1);
}
} else {
process.exit(1);
}
bruJsons.push({
bruFilepath: filename,
bruJson
});
}
const _isDirectory = await isDirectory(filename);
if (_isDirectory) {
let bruJsons = [];
if (!recursive) {
console.log(chalk.yellow('Running Folder \n'));
const files = fs.readdirSync(filename);
@@ -263,8 +287,6 @@ const handler = async function (argv) {
bruJson
});
}
// order requests by sequence
bruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
@@ -275,28 +297,51 @@ const handler = async function (argv) {
bruJsons = getBruFilesRecursively(filename);
}
}
let assertionResults = [];
let testResults = [];
for (const iter of bruJsons) {
const { bruFilepath, bruJson } = iter;
const result = await runSingleRequest(
bruFilepath,
bruJson,
collectionPath,
collectionVariables,
envVars,
processEnvVars,
brunoConfig
);
for (const iter of bruJsons) {
const { bruFilepath, bruJson } = iter;
const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars);
if (result) {
testrunResults.push(result);
const { assertionResults: _assertionResults, testResults: _testResults } = result;
if (result) {
const { assertionResults: _assertionResults, testResults: _testResults } = result;
assertionResults = assertionResults.concat(_assertionResults);
testResults = testResults.concat(_testResults);
}
assertionResults = assertionResults.concat(_assertionResults);
testResults = testResults.concat(_testResults);
}
}
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Ran all requests.')));
const summary = printRunSummary(assertionResults, testResults);
console.log(chalk.dim(chalk.grey('Ran all requests.')));
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
if (outputPath && outputPath.length) {
const outputDir = path.dirname(outputPath);
const outputDirExists = await exists(outputDir);
if (!outputDirExists) {
console.error(chalk.red(`Output directory ${outputDir} does not exist`));
process.exit(1);
}
const outputJson = {
summary,
results: testrunResults
};
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
}
if (summary.failedAssertions > 0 || summary.failedTests > 0) {
process.exit(1);
}
} catch (err) {
console.log('Something went wrong');

View File

@@ -1,33 +1,74 @@
const Mustache = require('mustache');
const { each, forOwn } = require('lodash');
const Handlebars = require('handlebars');
const { each, forOwn, cloneDeep } = require('lodash');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
const getContentType = (headers = {}) => {
let contentType = '';
forOwn(headers, (value, key) => {
if (key && key.toLowerCase() === 'content-type') {
contentType = value;
}
});
return contentType;
};
const interpolateVars = (request, envVars = {}, collectionVariables = {}) => {
const interpolateEnvVars = (str, processEnvVars) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
return template({
process: {
env: {
...processEnvVars
}
}
});
};
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
// 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] = interpolateEnvVars(value, processEnvVars);
});
const interpolate = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
// collectionVariables take precedence over envVars
const combinedVars = {
...envVars,
...collectionVariables
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
};
return Mustache.render(str, combinedVars);
return template(combinedVars);
};
request.url = interpolate(request.url);
forOwn(request.headers, (value, key) => {
request.headers[key] = interpolate(value);
delete request.headers[key];
request.headers[interpolate(key)] = interpolate(value);
});
if (request.headers['content-type'] === 'application/json') {
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
@@ -41,7 +82,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}) => {
request.data = interpolate(request.data);
}
}
} else if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
} else if (contentType === 'application/x-www-form-urlencoded') {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
@@ -57,6 +98,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}) => {
param.value = interpolate(param.value);
});
if (request.proxy) {
request.proxy.protocol = interpolate(request.proxy.protocol);
request.proxy.hostname = interpolate(request.proxy.hostname);
request.proxy.port = interpolate(request.proxy.port);
if (request.proxy.auth) {
request.proxy.auth.username = interpolate(request.proxy.auth.username);
request.proxy.auth.password = interpolate(request.proxy.auth.password);
}
}
return request;
};

View File

@@ -11,7 +11,15 @@ const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@use
const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru');
const runSingleRequest = async function (filename, bruJson, collectionPath, collectionVariables, envVariables) {
const runSingleRequest = async function (
filename,
bruJson,
collectionPath,
collectionVariables,
envVariables,
processEnvVars,
brunoConfig
) {
let request;
try {
@@ -32,7 +40,14 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars && preRequestVars.length) {
const varsRuntime = new VarsRuntime();
varsRuntime.runPreRequestVars(preRequestVars, request, envVariables, collectionVariables, collectionPath);
varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVariables,
collectionVariables,
collectionPath,
processEnvVars
);
}
// run pre request script
@@ -44,12 +59,39 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
request,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
}
// set proxy if enabled
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
const proxyHostname = get(brunoConfig, 'proxy.hostname');
const proxyPort = get(brunoConfig, 'proxy.port');
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const proxyConfig = {
protocol: proxyProtocol,
hostname: proxyHostname,
port: proxyPort
};
if (proxyAuthEnabled) {
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
proxyConfig.auth = {
username: proxyAuthUsername,
password: proxyAuthPassword
};
}
request.proxy = proxyConfig;
}
// interpolate variables inside request
interpolateVars(request, envVariables, collectionVariables);
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
const options = getOptions();
const insecure = get(options, 'insecure', false);
@@ -95,7 +137,8 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
}
@@ -109,7 +152,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
}
@@ -148,7 +193,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}
@@ -164,6 +211,18 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
}
return {
request: {
method: request.method,
url: request.url,
headers: request.headers,
data: request.data
},
response: {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
},
assertionResults,
testResults
};
@@ -183,7 +242,8 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
}
@@ -197,7 +257,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
}
@@ -236,7 +298,9 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}

View File

@@ -1,5 +1,5 @@
{
"version": "v0.14.1",
"version": "v0.16.2",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -14,10 +14,11 @@
"test": "jest"
},
"dependencies": {
"@usebruno/js": "0.4.0",
"@usebruno/lang": "0.3.0",
"@usebruno/schema": "0.3.1",
"axios": "^0.26.0",
"@usebruno/js": "0.6.0",
"@usebruno/lang": "0.4.0",
"@usebruno/schema": "0.5.0",
"about-window": "^1.15.2",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
"dotenv": "^16.0.3",

View File

@@ -5,20 +5,12 @@ const Yup = require('yup');
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
// uid inside collections is deprecated, but we still need to validate it
// for backward compatibility
const uidSchema = Yup.string()
.length(21, 'uid must be 21 characters in length')
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric');
// todo: bruno.json config schema validation errors must be propagated to the UI
const configSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less'),
name: Yup.string().max(256, 'name must be 256 characters or less').required('name is required'),
type: Yup.string().oneOf(['collection']).required('type is required'),
version: Yup.string().oneOf(['1']).required('type is required')
})
.noUnknown(true)
.strict();
});
const readConfigFile = async (pathname) => {
try {
@@ -67,15 +59,15 @@ const openCollectionDialog = async (win, watcher) => {
const openCollection = async (win, watcher, collectionPath, options = {}) => {
if (!watcher.hasWatcher(collectionPath)) {
try {
const { name } = await getCollectionConfigFile(collectionPath);
const brunoConfig = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
win.webContents.send('main:collection-opened', collectionPath, uid, name);
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', win, collectionPath, uid);
} catch (err) {
if (!options.dontSendDisplayErrors) {
win.webContents.send('main:display-error', {
error: err.message || 'An error occured while opening the local collection'
error: err.message || 'An error occurred while opening the local collection'
});
}
}

View File

@@ -51,7 +51,7 @@ const template = [
click: () =>
openAboutWindow({
product_name: 'Bruno',
icon_path: join(__dirname, '../../resources/icons/png/256x256.png'),
icon_path: join(process.cwd(), '/resources/icons/png/256x256.png'),
homepage: 'https://www.usebruno.com/',
package_json_dir: join(__dirname, '../..')
})

View File

@@ -12,6 +12,7 @@ const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -30,6 +31,13 @@ const isDotEnvFile = (pathname, collectionPath) => {
return dirname === collectionPath && basename === '.env';
};
const isBrunoConfigFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === 'bruno.json';
};
const isBruEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments');
@@ -167,6 +175,17 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
const add = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher add: ${pathname}`);
if (isBrunoConfigFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const brunoConfig = JSON.parse(content);
setBrunoConfig(collectionUid, brunoConfig);
} catch (err) {
console.error(err);
}
}
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
@@ -281,6 +300,23 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
};
const change = async (win, pathname, collectionUid, collectionPath) => {
if (isBrunoConfigFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const brunoConfig = JSON.parse(content);
const payload = {
collectionUid,
brunoConfig: brunoConfig
};
setBrunoConfig(collectionUid, brunoConfig);
win.webContents.send('main:bruno-config-update', payload);
} catch (err) {
console.error(err);
}
}
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
@@ -378,7 +414,7 @@ class Watcher {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
usePolling: false,
ignored: (path) => ['node_modules', '.git', 'bruno.json'].some((s) => path.includes(s)),
ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
persistent: true,
ignorePermissionErrors: true,
awaitWriteFinish: {

View File

@@ -7,7 +7,7 @@ const bruToEnvJson = (bru) => {
const json = bruToEnvJsonV2(bru);
// the app env format requires each variable to have a type
// this need to be evaulated and safely removed
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if (json && json.variables && json.variables.length) {
each(json.variables, (v) => (v.type = 'text'));
@@ -61,6 +61,7 @@ const bruToJson = (bru) => {
url: _.get(json, 'http.url'),
params: _.get(json, 'query', []),
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
body: _.get(json, 'body', {}),
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
@@ -69,6 +70,7 @@ const bruToJson = (bru) => {
}
};
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
return transformedJson;
@@ -104,10 +106,12 @@ const jsonToBru = (json) => {
http: {
method: _.lowerCase(_.get(json, 'request.method')),
url: _.get(json, 'request.url'),
auth: _.get(json, 'request.auth.mode', 'none'),
body: _.get(json, 'request.body.mode', 'none')
},
query: _.get(json, 'request.params', []),
headers: _.get(json, 'request.headers', []),
auth: _.get(json, 'request.auth', {}),
body: _.get(json, 'request.body', {}),
script: _.get(json, 'request.script', {}),
vars: {

View File

@@ -57,14 +57,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath);
const content = await stringifyJson({
const brunoConfig = {
version: '1',
name: collectionName,
type: 'collection'
});
};
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(dirPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', dirPath, uid, collectionName);
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
return;
@@ -356,14 +357,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
const content = await stringifyJson({
const brunoConfig = {
version: '1',
name: collection.name,
type: 'collection'
});
};
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(collectionPath, 'bruno.json'), content);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, collectionName);
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid);
lastOpenedCollections.add(collectionPath);
@@ -451,6 +453,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:set-preferences', async (event, preferences) => {
setPreferences(preferences);
});
ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionUid) => {
try {
const brunoConfigPath = path.join(collectionPath, 'bruno.json');
const content = await stringifyJson(brunoConfig);
await writeFile(brunoConfigPath, content);
} catch (error) {
return Promise.reject(error);
}
});
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {

View File

@@ -14,6 +14,7 @@ const interpolateVars = require('./interpolate-vars');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { getPreferences } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@@ -101,6 +102,7 @@ const registerNetworkIpc = (mainWindow) => {
const _request = item.draft ? item.draft.request : item.request;
const request = prepareRequest(_request);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
try {
// make axios work in node using form data
@@ -127,7 +129,8 @@ const registerNetworkIpc = (mainWindow) => {
request,
envVars,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
if (result) {
@@ -150,7 +153,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -161,7 +165,31 @@ const registerNetworkIpc = (mainWindow) => {
});
}
const processEnvVars = getProcessEnvVars(collectionUid);
// proxy configuration
const brunoConfig = getBrunoConfig(collectionUid);
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
const proxyHostname = get(brunoConfig, 'proxy.hostname');
const proxyPort = get(brunoConfig, 'proxy.port');
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const proxyConfig = {
protocol: proxyProtocol,
hostname: proxyHostname,
port: proxyPort
};
if (proxyAuthEnabled) {
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
proxyConfig.auth = {
username: proxyAuthUsername,
password: proxyAuthPassword
};
}
request.proxy = proxyConfig;
}
interpolateVars(request, envVars, collectionVariables, processEnvVars);
@@ -224,7 +252,8 @@ const registerNetworkIpc = (mainWindow) => {
response,
envVars,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
if (result) {
@@ -248,7 +277,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -292,7 +322,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:run-request-event', {
@@ -365,7 +396,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:run-request-event', {
@@ -510,6 +542,7 @@ const registerNetworkIpc = (mainWindow) => {
const _request = item.draft ? item.draft.request : item.request;
const request = prepareRequest(_request);
const processEnvVars = getProcessEnvVars(collectionUid);
try {
// make axios work in node using form data
@@ -554,7 +587,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -564,7 +598,31 @@ const registerNetworkIpc = (mainWindow) => {
});
}
const processEnvVars = getProcessEnvVars(collectionUid);
// proxy configuration
const brunoConfig = getBrunoConfig(collectionUid);
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
const proxyProtocol = get(brunoConfig, 'proxy.protocol');
const proxyHostname = get(brunoConfig, 'proxy.hostname');
const proxyPort = get(brunoConfig, 'proxy.port');
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const proxyConfig = {
protocol: proxyProtocol,
hostname: proxyHostname,
port: proxyPort
};
if (proxyAuthEnabled) {
const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
proxyConfig.auth = {
username: proxyAuthUsername,
password: proxyAuthPassword
};
}
request.proxy = proxyConfig;
}
// interpolate variables inside request
interpolateVars(request, envVars, collectionVariables, processEnvVars);
@@ -607,7 +665,8 @@ const registerNetworkIpc = (mainWindow) => {
response,
envVars,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
if (result) {
@@ -630,7 +689,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -672,7 +732,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:run-folder-event', {
@@ -750,7 +811,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
collectionVariables,
collectionPath,
onConsoleLog
onConsoleLog,
processEnvVars
);
mainWindow.webContents.send('main:run-folder-event', {

View File

@@ -1,6 +1,17 @@
const Handlebars = require('handlebars');
const { each, forOwn, cloneDeep } = require('lodash');
const getContentType = (headers = {}) => {
let contentType = '';
forOwn(headers, (value, key) => {
if (key && key.toLowerCase() === 'content-type') {
contentType = value;
}
});
return contentType;
};
const interpolateEnvVars = (str, processEnvVars) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
@@ -51,10 +62,13 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.url = interpolate(request.url);
forOwn(request.headers, (value, key) => {
request.headers[key] = interpolate(value);
delete request.headers[key];
request.headers[interpolate(key)] = interpolate(value);
});
if (request.headers['content-type'] === 'application/json') {
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
@@ -68,7 +82,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
request.data = interpolate(request.data);
}
}
} else if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
} else if (contentType === 'application/x-www-form-urlencoded') {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
@@ -84,6 +98,17 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
param.value = interpolate(param.value);
});
if (request.proxy) {
request.proxy.protocol = interpolate(request.proxy.protocol);
request.proxy.hostname = interpolate(request.proxy.hostname);
request.proxy.port = interpolate(request.proxy.port);
if (request.proxy.auth) {
request.proxy.auth.username = interpolate(request.proxy.auth.username);
request.proxy.auth.password = interpolate(request.proxy.auth.password);
}
}
return request;
};

View File

@@ -0,0 +1,19 @@
/**
* This modules stores the configs loaded from bruno.json
*/
const config = {};
// collectionUid is a hash based on the collection path)
const getBrunoConfig = (collectionUid) => {
return config[collectionUid] || {};
};
const setBrunoConfig = (collectionUid, brunoConfig) => {
config[collectionUid] = brunoConfig;
};
module.exports = {
getBrunoConfig,
setBrunoConfig
};

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/graphql-docs",
"version": "0.1.0",
"license" : "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/js",
"version": "0.4.0",
"version": "0.6.0",
"license": "MIT",
"main": "src/index.js",
"files": [
"src",
@@ -18,7 +19,9 @@
"atob": "^2.1.2",
"axios": "^0.26.0",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"crypto-js": "^4.1.1",
"handlebars": "^4.7.8",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",

View File

@@ -1,19 +1,39 @@
const Handlebars = require('handlebars');
const { cloneDeep } = require('lodash');
class Bru {
constructor(envVariables, collectionVariables) {
constructor(envVariables, collectionVariables, processEnvVars) {
this.envVariables = envVariables;
this.collectionVariables = collectionVariables;
this.processEnvVars = cloneDeep(processEnvVars || {});
}
_interpolateEnvVar = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
const template = Handlebars.compile(str, { noEscape: true });
return template({
process: {
env: {
...this.processEnvVars
}
}
});
};
getEnvName() {
return this.envVariables.__name__;
}
getProcessEnv(key) {
return process.env[key];
return this.processEnvVars[key];
}
getEnvVar(key) {
return this.envVariables[key];
return this._interpolateEnvVar(this.envVariables[key]);
}
setEnvVar(key, value) {

View File

@@ -46,6 +46,10 @@ class BrunoRequest {
setBody(data) {
this.req.data = data;
}
setMaxRedirects(maxRedirects) {
this.req.maxRedirects = maxRedirects;
}
}
module.exports = BrunoRequest;

View File

@@ -21,13 +21,22 @@ const uuid = require('uuid');
const nanoid = require('nanoid');
const axios = require('axios');
const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
class ScriptRuntime {
constructor() {}
async runRequestScript(script, request, envVariables, collectionVariables, collectionPath, onConsoleLog) {
const bru = new Bru(envVariables, collectionVariables);
async runRequestScript(
script,
request,
envVariables,
collectionVariables,
collectionPath,
onConsoleLog,
processEnvVars
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
const req = new BrunoRequest(request);
const context = {
@@ -73,6 +82,7 @@ class ScriptRuntime {
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS
}
@@ -87,8 +97,17 @@ class ScriptRuntime {
};
}
async runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog) {
const bru = new Bru(envVariables, collectionVariables);
async runResponseScript(
script,
request,
response,
envVariables,
collectionVariables,
collectionPath,
onConsoleLog,
processEnvVars
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);

View File

@@ -20,8 +20,17 @@ const CryptoJS = require('crypto-js');
class TestRuntime {
constructor() {}
runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath, onConsoleLog) {
const bru = new Bru(envVariables, collectionVariables);
runTests(
testsFile,
request,
response,
envVariables,
collectionVariables,
collectionPath,
onConsoleLog,
processEnvVars
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
@@ -74,6 +83,7 @@ class TestRuntime {
moment,
uuid,
nanoid,
chai,
'crypto-js': CryptoJS
}
}

View File

@@ -4,13 +4,13 @@ const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
class VarsRuntime {
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) {
runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath, processEnvVars) {
const enabledVars = _.filter(vars, (v) => v.enabled);
if (!enabledVars.length) {
return;
}
const bru = new Bru(envVariables, collectionVariables);
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
const req = new BrunoRequest(request);
const bruContext = {
@@ -34,13 +34,13 @@ class VarsRuntime {
};
}
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath) {
runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath, processEnvVars) {
const enabledVars = _.filter(vars, (v) => v.enabled);
if (!enabledVars.length) {
return;
}
const bru = new Bru(envVariables, collectionVariables);
const bru = new Bru(envVariables, collectionVariables, processEnvVars);
const req = new BrunoRequest(request);
const res = createResponseParser(response);

View File

@@ -11,7 +11,7 @@ const JS_KEYWORDS = `
.filter((word) => word.length > 0);
/**
* Creates a function from a Javascript expression
* Creates a function from a JavaScript expression
*
* When the function is called, the variables used in this expression are picked up from the context
*
@@ -119,7 +119,7 @@ const createResponseParser = (response = {}) => {
};
/**
* Objects that are created inside vm2 execution context result in an serilaization error when sent to the renderer process
* Objects that are created inside vm2 execution context result in an serialization error when sent to the renderer process
* Error sending from webFrameMain: Error: Failed to serialize arguments
* at s.send (node:electron/js2c/browser_init:169:631)
* at g.send (node:electron/js2c/browser_init:165:2156)

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/lang",
"version": "0.3.0",
"version": "0.4.0",
"license" : "MIT",
"main": "src/index.js",
"files": [
"src",

View File

@@ -66,7 +66,7 @@ const bodyXmlTag = between(bodyXmlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((
* We have deprecated form-url-encoded type in body tag, it was a misspelling on my part
* The new type is form-urlencoded
*
* Very few peope would have used this. I launched this to the public on 22 Jan 2023
* Very few people would have used this. I launched this to the public on 22 Jan 2023
* And I am making the change on 23 Jan 2023
*
* This deprecated tag can be removed on 1 April 2023

View File

@@ -22,7 +22,8 @@ const { outdentString } = require('../../v1/src/utils');
*
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | headers | bodies | varsandassert | script | tests | docs)*
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
auths = authbasic | authbearer
bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
@@ -75,6 +76,9 @@ const grammar = ohm.grammar(`Bru {
varsres = "vars:post-response" dictionary
assert = "assert" assertdictionary
authbasic = "auth:basic" dictionary
authbearer = "auth:bearer" dictionary
body = "body" st* "{" nl* textblock tagend
bodyjson = "body:json" st* "{" nl* textblock tagend
bodytext = "body:text" st* "{" nl* textblock tagend
@@ -92,13 +96,21 @@ const grammar = ohm.grammar(`Bru {
docs = "docs" st* "{" nl* textblock tagend
}`);
const mapPairListToKeyValPairs = (pairList = []) => {
const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
if (!pairList.length) {
return [];
}
return _.map(pairList[0], (pair) => {
let name = _.keys(pair)[0];
let value = pair[name];
if (!parseEnabled) {
return {
name,
value
};
}
let enabled = true;
if (name && name.length && name.charAt(0) === '~') {
name = name.slice(1);
@@ -282,6 +294,33 @@ const sem = grammar.createSemantics().addAttribute('ast', {
headers: mapPairListToKeyValPairs(dictionary.ast)
};
},
authbasic(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const usernameKey = _.find(auth, { name: 'username' });
const passwordKey = _.find(auth, { name: 'password' });
const username = usernameKey ? usernameKey.value : '';
const password = passwordKey ? passwordKey.value : '';
return {
auth: {
basic: {
username,
password
}
}
};
},
authbearer(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const tokenKey = _.find(auth, { name: 'token' });
const token = tokenKey ? tokenKey.value : '';
return {
auth: {
bearer: {
token
}
}
};
},
bodyformurlencoded(_1, dictionary) {
return {
body: {

View File

@@ -13,7 +13,7 @@ const stripLastLine = (text) => {
};
const jsonToBru = (json) => {
const { meta, http, query, headers, body, script, tests, vars, assertions, docs } = json;
const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json;
let bru = '';
@@ -82,6 +82,23 @@ const jsonToBru = (json) => {
bru += '\n}\n\n';
}
if (auth && auth.basic) {
bru += `auth:basic {
${indentString(`username: ${auth.basic.username}`)}
${indentString(`password: ${auth.basic.password}`)}
}
`;
}
if (auth && auth.bearer) {
bru += `auth:bearer {
${indentString(`token: ${auth.bearer.token}`)}
}
`;
}
if (body && body.json && body.json.length) {
bru += `body:json {
${indentString(body.json)}

View File

@@ -21,6 +21,15 @@ headers {
~transaction-id: {{transactionId}}
}
auth:basic {
username: john
password: secret
}
auth:bearer {
token: 123
}
body:json {
{
"hello": "world"

View File

@@ -43,6 +43,15 @@
"enabled": false
}
],
"auth": {
"basic": {
"username": "john",
"password": "secret"
},
"bearer": {
"token": "123"
}
},
"body": {
"json": "{\n \"hello\": \"world\"\n}",
"text": "This is a text body",

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/query",
"version": "0.1.0",
"license" : "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,7 @@
{
"name": "@usebruno/schema",
"version": "0.3.1",
"version": "0.5.0",
"license": "MIT",
"main": "src/index.js",
"files": [
"src",

View File

@@ -69,6 +69,27 @@ const requestBodySchema = Yup.object({
.noUnknown(true)
.strict();
const authBasicSchema = Yup.object({
username: Yup.string().nullable(),
password: Yup.string().nullable()
})
.noUnknown(true)
.strict();
const authBearerSchema = Yup.object({
token: Yup.string().nullable()
})
.noUnknown(true)
.strict();
const authSchema = Yup.object({
mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'),
basic: authBasicSchema.nullable(),
bearer: authBearerSchema.nullable()
})
.noUnknown(true)
.strict();
// Right now, the request schema is very tightly coupled with http request
// As we introduce more request types in the future, we will improve the definition to support
// schema structure based on other request type
@@ -77,6 +98,7 @@ const requestSchema = Yup.object({
method: requestMethodSchema,
headers: Yup.array().of(keyValueSchema).required('headers are required'),
params: Yup.array().of(keyValueSchema).required('params are required'),
auth: authSchema,
body: requestBodySchema,
script: Yup.object({
req: Yup.string().nullable(),
@@ -124,11 +146,11 @@ const collectionSchema = Yup.object({
.nullable(),
environments: environmentsSchema,
pathname: Yup.string().nullable(),
showRunner: Yup.boolean(),
runnerResult: Yup.object({
items: Yup.array()
}),
collectionVariables: Yup.object()
collectionVariables: Yup.object(),
brunoConfig: Yup.object()
})
.noUnknown(true)
.strict();

View File

@@ -3,6 +3,7 @@
"displayName": "Bruno",
"description": "Bruno support for Visual Studio Code.",
"version": "0.0.1",
"license" : "MIT",
"engines": {
"vscode": "^1.74.0"
},

View File

@@ -10,7 +10,7 @@ exports.HomePage = class HomePage {
// sample collection
this.loadSampleCollectionSuccessToast = page.getByText('Sample Collection loaded successfully');
this.sampeCollectionSelector = page.locator('#sidebar-collection-name');
this.sampleCollectionSelector = page.locator('#sidebar-collection-name');
this.getUsersSelector = page.getByText('Users');
this.getSingleUserSelector = page.getByText('Single User');
this.getUserNotFoundSelector = page.getByText('User Not Found');
@@ -43,7 +43,7 @@ exports.HomePage = class HomePage {
}
async getUsers() {
await this.sampeCollectionSelector.click();
await this.sampleCollectionSelector.click();
await this.getUsersSelector.click();
await this.sendRequestButton.click();
}