Compare commits

...

27 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
59 changed files with 476 additions and 81 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,7 @@
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.4.0",
"@usebruno/schema": "0.5.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",

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

@@ -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

@@ -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

@@ -112,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);

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,7 +76,7 @@ const RequestTabs = () => {
});
};
// Todo: Must support ephermal requests
// Todo: Must support ephemeral requests
return (
<StyledWrapper className={getRootClassname()}>
{newRequestModalOpen && (

View File

@@ -99,7 +99,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
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

@@ -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

@@ -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

@@ -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.16.0</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

@@ -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

@@ -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

@@ -229,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) {
@@ -563,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);
@@ -1154,7 +1168,7 @@ export const {
requestCancelled,
responseReceived,
saveRequest,
newEphermalHttpRequest,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
@@ -1170,6 +1184,7 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
updateRequestBody,
updateRequestGraphqlQuery,

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

@@ -84,7 +84,7 @@ export const getContentType = (headers) => {
export const formatResponse = (response) => {
let type = getContentType(response.headers);
if (type.includes('json')) {
return safeStringifyJSON(response.data);
return safeStringifyJSON(response.data, true);
}
if (type.includes('xml')) {
return xmlFormat(response.data, { collapseContent: true });

View File

@@ -1,5 +1,13 @@
# 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

View File

@@ -1,7 +1,7 @@
{
"name": "@usebruno/cli",
"version": "0.9.0",
"license" : "MIT",
"version": "0.10.1",
"license": "MIT",
"main": "src/index.js",
"bin": {
"bru": "./bin/bru.js"
@@ -21,9 +21,9 @@
"package.json"
],
"dependencies": {
"@usebruno/js": "0.5.0",
"@usebruno/js": "0.6.0",
"@usebruno/lang": "0.4.0",
"axios": "^1.3.2",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"form-data": "^4.0.0",

View File

@@ -165,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) {
@@ -304,7 +307,8 @@ const handler = async function (argv) {
collectionPath,
collectionVariables,
envVars,
processEnvVars
processEnvVars,
brunoConfig
);
if (result) {

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;
@@ -55,7 +66,9 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
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);
@@ -69,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);
@@ -85,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

@@ -17,7 +17,8 @@ const runSingleRequest = async function (
collectionPath,
collectionVariables,
envVariables,
processEnvVars
processEnvVars,
brunoConfig
) {
let request;
@@ -39,7 +40,14 @@ const runSingleRequest = async function (
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
@@ -51,10 +59,37 @@ const runSingleRequest = async function (
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, processEnvVars);
@@ -102,7 +137,8 @@ const runSingleRequest = async function (
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
}
@@ -116,7 +152,9 @@ const runSingleRequest = async function (
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
}
@@ -155,7 +193,9 @@ const runSingleRequest = async function (
response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}
@@ -202,7 +242,8 @@ const runSingleRequest = async function (
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
processEnvVars
);
}
@@ -216,7 +257,9 @@ const runSingleRequest = async function (
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
}
@@ -255,7 +298,9 @@ const runSingleRequest = async function (
err.response,
envVariables,
collectionVariables,
collectionPath
collectionPath,
null,
processEnvVars
);
testResults = get(result, 'results', []);
}

View File

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

View File

@@ -67,7 +67,7 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => {
} 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

@@ -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

@@ -165,6 +165,7 @@ const registerNetworkIpc = (mainWindow) => {
});
}
// proxy configuration
const brunoConfig = getBrunoConfig(collectionUid);
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
@@ -597,6 +598,32 @@ const registerNetworkIpc = (mainWindow) => {
});
}
// 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);

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;
@@ -55,7 +66,9 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
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);
@@ -69,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);

View File

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

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

@@ -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

@@ -1,7 +1,7 @@
{
"name": "@usebruno/schema",
"version": "0.4.0",
"license" : "MIT",
"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(),

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();
}